[423130] use an IContentDescriber to associate Vex with files

Vex should be associated only with files containing documents of
registered document types. Therefor the VexContentDescriber reads the
DTD and the root element to decide if a file contains such a document.

Furthermore it is not essentially required for bundles that register a
document type to also register a new content type. This is only
necessary if a custom file extension (!= xml) should be used.

Bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=423130
Change-Id: I695e512c03f49f87f6221d3c3c6529ea04935477
Signed-off-by: Florian Thienel <florian@thienel.org>
diff --git a/org.eclipse.vex.dita/plugin.xml b/org.eclipse.vex.dita/plugin.xml
index 872155d..8703fb7 100644
--- a/org.eclipse.vex.dita/plugin.xml
+++ b/org.eclipse.vex.dita/plugin.xml
@@ -9,28 +9,28 @@
         file-extensions="dita,ditamap,bookmap,ditaval"
         id="org.eclipse.vex.ui.dita"
         name="%contentType.name"
-        priority="normal">
+        priority="low">
 		</content-type>
 		<content-type
         base-type="org.eclipse.vex.ui.dita.map"
         file-extensions="ditamap,bookmap"
         id="org.eclipse.vex.ui.dita.map"
         name="%contentType.map.name"
-        priority="low">
+        priority="normal">
 		</content-type>
 		<content-type
         base-type="org.eclipse.vex.ui.dita.ditaval"
         file-extensions="ditaval"
         id="org.eclipse.vex.ui.dita.ditaval"
         name="%contentType.ditaval.name"
-        priority="low">
+        priority="normal">
 		</content-type>
 		<content-type
         base-type="org.eclipse.vex.ui.dita.topic"
         file-extensions="dita"
         id="org.eclipse.vex.ui.dita.topic"
         name="%contentType.topic.name"
-        priority="low">
+        priority="normal">
 		</content-type>
 	</extension>
 
diff --git a/org.eclipse.vex.docbook/plugin.properties b/org.eclipse.vex.docbook/plugin.properties
index 6750db3..f42b603 100644
--- a/org.eclipse.vex.docbook/plugin.properties
+++ b/org.eclipse.vex.docbook/plugin.properties
@@ -15,5 +15,4 @@
 doctype.docbook5_0_DTD=DocBook v5.0 (DTD)
 doctype.docbook5_0_XSD2=DocBook v5.0 (XML Schema)
 
-style.docbook-plain=DocBook Plain
-contentType.name=DocBook XML Document

+style.docbook-plain=DocBook Plain

diff --git a/org.eclipse.vex.docbook/plugin.xml b/org.eclipse.vex.docbook/plugin.xml
index 65a9911..4d7fecb 100644
--- a/org.eclipse.vex.docbook/plugin.xml
+++ b/org.eclipse.vex.docbook/plugin.xml
@@ -2,15 +2,6 @@
 <?eclipse version="3.2"?>
 <plugin>
 
-	<extension point="org.eclipse.core.contenttype.contentTypes">
-		<content-type
-        base-type="org.eclipse.vex.ui.XmlDocument"
-        file-extensions="xml,docbook"
-        id="org.eclipse.vex.ui.docbook"
-        name="%contentType.name"
-        priority="low">
-		</content-type>
-	</extension>
  <extension
        point="org.eclipse.wst.xml.core.catalogContributions">
     <catalogContribution
diff --git a/org.eclipse.vex.projectplan/plugin.properties b/org.eclipse.vex.projectplan/plugin.properties
index c203949..d3605f5 100644
--- a/org.eclipse.vex.projectplan/plugin.properties
+++ b/org.eclipse.vex.projectplan/plugin.properties
@@ -13,5 +13,4 @@
 
 doctype.plan=Eclipse Project Plan
 
-style.plain=Plain
-contentType.name=Eclipse Project Plan

+style.plain=Plain

diff --git a/org.eclipse.vex.projectplan/plugin.xml b/org.eclipse.vex.projectplan/plugin.xml
index d8cfc60..b03dbdf 100644
--- a/org.eclipse.vex.projectplan/plugin.xml
+++ b/org.eclipse.vex.projectplan/plugin.xml
@@ -2,16 +2,6 @@
 <?eclipse version="3.4"?>

 <plugin>

    <extension

-         point="org.eclipse.core.contenttype.contentTypes">

-      <content-type

-            base-type="org.eclipse.vex.ui.XmlDocument"

-            file-extensions="xml,projectplan"

-            id="org.eclipse.vex.projectplan"

-            name="%contentType.name"

-            priority="low">

-      </content-type>

-   </extension>

-   <extension

          point="org.eclipse.wst.xml.core.catalogContributions">

       <catalogContribution>

          <uri

diff --git a/org.eclipse.vex.ui.tests/plugin.properties b/org.eclipse.vex.ui.tests/plugin.properties
index 3d85c6a..d342287 100644
--- a/org.eclipse.vex.ui.tests/plugin.properties
+++ b/org.eclipse.vex.ui.tests/plugin.properties
@@ -9,5 +9,4 @@
 #     David Carver - initial API and implementation
 ###############################################################################
 pluginName= Vex UI Tests
-providerName= Eclipse.org
-contentType.name=Vex Test XML Document Type

+providerName= Eclipse.org

diff --git a/org.eclipse.vex.ui.tests/plugin.xml b/org.eclipse.vex.ui.tests/plugin.xml
index dd04ca3..8bbfd6d 100644
--- a/org.eclipse.vex.ui.tests/plugin.xml
+++ b/org.eclipse.vex.ui.tests/plugin.xml
@@ -1,11 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>

 <?eclipse version="3.4"?>

 <plugin>

-	<extension point="org.eclipse.core.contenttype.contentTypes">

-		<content-type id="org.eclipse.vex.ui.tests" name="%contentType.name"

-			base-type="org.eclipse.vex.ui.XmlDocument" file-extensions="xml">

-		</content-type>

-	</extension>

  <extension

        point="org.eclipse.wst.xml.core.catalogContributions">

     <catalogContribution

diff --git a/org.eclipse.vex.ui.tests/src/org/eclipse/vex/ui/internal/contenttype/tests/VexContentDescriberTest.java b/org.eclipse.vex.ui.tests/src/org/eclipse/vex/ui/internal/contenttype/tests/VexContentDescriberTest.java
new file mode 100644
index 0000000..bca79cf
--- /dev/null
+++ b/org.eclipse.vex.ui.tests/src/org/eclipse/vex/ui/internal/contenttype/tests/VexContentDescriberTest.java
@@ -0,0 +1,86 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Florian Thienel and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * 		Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.ui.internal.contenttype.tests;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.core.runtime.content.IContentDescriber;
+import org.eclipse.core.runtime.content.IContentDescription;
+import org.eclipse.core.runtime.content.IContentType;
+import org.eclipse.vex.ui.internal.contenttype.VexContentDescriber;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class VexContentDescriberTest {
+
+	private static final String REGISTERED_VEX_DOCTYPE = "<?xml version=\"1.0\"?><!DOCTYPE section PUBLIC \"-//Vex//DTD Test//EN\" \"test.dtd\"><section/>";
+	private static final String ARBITRARY_XML = "<?xml version=\"1.0\"?><someRootElement/>";
+	private static final String SIMPLE_TEXT = "Hello World";
+
+	@Test
+	public void givenRegisteredVexDoctype_shouldDescribeAsValid() throws Exception {
+		assertDescribesAs(IContentDescriber.VALID, REGISTERED_VEX_DOCTYPE);
+	}
+
+	@Test
+	public void givenArbitraryXml_shouldDescribeAsIndeterminate() throws Exception {
+		assertDescribesAs(IContentDescriber.INDETERMINATE, ARBITRARY_XML);
+	}
+
+	@Test
+	public void givenSimpleText_shouldDescribeAsInvalid() throws Exception {
+		assertDescribesAs(IContentDescriber.INVALID, SIMPLE_TEXT);
+	}
+
+	private static void assertDescribesAs(final int expectedDescription, final String contents) throws Exception {
+		final VexContentDescriber describer = new VexContentDescriber();
+		final IContentDescription description = new DummyContentDescription();
+		assertEquals(expectedDescription, describer.describe(new ByteArrayInputStream(contents.getBytes()), description));
+	}
+
+	private static class DummyContentDescription implements IContentDescription {
+
+		private final Map<QualifiedName, Object> properties = new HashMap<QualifiedName, Object>();
+
+		@Override
+		public boolean isRequested(final QualifiedName key) {
+			return true;
+		}
+
+		@Override
+		public String getCharset() {
+			return System.getProperty("file.encoding");
+		}
+
+		@Override
+		public IContentType getContentType() {
+			return null;
+		}
+
+		@Override
+		public Object getProperty(final QualifiedName key) {
+			return properties.get(key);
+		}
+
+		@Override
+		public void setProperty(final QualifiedName key, final Object value) {
+			properties.put(key, value);
+		}
+
+	}
+}
diff --git a/org.eclipse.vex.ui/META-INF/MANIFEST.MF b/org.eclipse.vex.ui/META-INF/MANIFEST.MF
index 31e9886..b7d471c 100644
--- a/org.eclipse.vex.ui/META-INF/MANIFEST.MF
+++ b/org.eclipse.vex.ui/META-INF/MANIFEST.MF
@@ -24,6 +24,7 @@
  org.eclipse.core.filesystem;bundle-version="[1.3.0,2.0.0)"
 Export-Package: org.eclipse.vex.ui.internal;x-friends:="org.eclipse.vex.ui.tests",
  org.eclipse.vex.ui.internal.config;x-friends:="org.eclipse.vex.ui.tests",
+ org.eclipse.vex.ui.internal.contenttype;x-friends:="org.eclipse.vex.ui.tests",
  org.eclipse.vex.ui.internal.editor;
   x-friends:="org.eclipse.vex.docbook,
    org.eclipse.vex.ui.tests,
diff --git a/org.eclipse.vex.ui/plugin.xml b/org.eclipse.vex.ui/plugin.xml
index 5a1ce81..363cf22 100644
--- a/org.eclipse.vex.ui/plugin.xml
+++ b/org.eclipse.vex.ui/plugin.xml
@@ -9,6 +9,8 @@
 	<extension point="org.eclipse.core.contenttype.contentTypes">
 		<content-type
         base-type="org.eclipse.core.runtime.xml"
+        describer="org.eclipse.vex.ui.internal.contenttype.VexContentDescriber"
+        file-extensions="xml"
         id="org.eclipse.vex.ui.XmlDocument"
         name="%contentType.XmlDocument.name"
         priority="low">
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/contenttype/InferXmlContentTypeHandler.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/contenttype/InferXmlContentTypeHandler.java
new file mode 100644
index 0000000..ae08a14
--- /dev/null
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/contenttype/InferXmlContentTypeHandler.java
@@ -0,0 +1,141 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Florian Thienel and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * 		Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.ui.internal.contenttype;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.eclipse.core.runtime.QualifiedName;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.ext.LexicalHandler;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * @author Florian Thienel
+ */
+public class InferXmlContentTypeHandler extends DefaultHandler implements LexicalHandler {
+
+	private String dtdPublicId;
+	private String dtdSystemId;
+	private QualifiedName rootElementName;
+
+	public String getMainDocumentTypeIdentifier() {
+		if (dtdPublicId != null) {
+			return dtdPublicId;
+		}
+		if (dtdSystemId != null) {
+			return dtdSystemId;
+		}
+		return rootElementName.getQualifier();
+	}
+
+	public QualifiedName getRootElementName() {
+		return rootElementName;
+	}
+
+	public boolean parseContents(final InputSource contents) {
+		try {
+			final SAXParserFactory factory = SAXParserFactory.newInstance();
+			if (factory == null) {
+				return false;
+			}
+			factory.setNamespaceAware(true);
+			final SAXParser parser = createParser(factory);
+			contents.setSystemId("/"); //$NON-NLS-1$
+			parser.parse(contents, this);
+		} catch (final AbortParsingException e) {
+			// Abort the parsing normally. Fall through...
+		} catch (final ParserConfigurationException e) {
+			return false;
+		} catch (final SAXException e) {
+			return false;
+		} catch (final IOException e) {
+			return false;
+		}
+		return true;
+	}
+
+	private SAXParser createParser(final SAXParserFactory parserFactory) throws ParserConfigurationException, SAXException, SAXNotRecognizedException, SAXNotSupportedException {
+		final SAXParser parser = parserFactory.newSAXParser();
+		final XMLReader reader = parser.getXMLReader();
+		reader.setProperty("http://xml.org/sax/properties/lexical-handler", this); //$NON-NLS-1$
+		try {
+			reader.setFeature("http://xml.org/sax/features/validation", false); //$NON-NLS-1$
+			reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); //$NON-NLS-1$
+		} catch (final SAXNotRecognizedException e) {
+			// not a big deal if the parser does not recognize the features
+		} catch (final SAXNotSupportedException e) {
+			// not a big deal if the parser does not support the features
+		}
+		return parser;
+	}
+
+	@Override
+	public void startDTD(final String name, final String publicId, final String systemId) throws SAXException {
+		dtdPublicId = publicId;
+		dtdSystemId = systemId;
+	}
+
+	@Override
+	public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException {
+		rootElementName = new QualifiedName(uri, localName);
+		throw new AbortParsingException();
+	}
+
+	@Override
+	public void endDTD() throws SAXException {
+		// ignore
+	}
+
+	@Override
+	public void startEntity(final String name) throws SAXException {
+		// ignore
+	}
+
+	@Override
+	public void endEntity(final String name) throws SAXException {
+		// ignore
+	}
+
+	@Override
+	public void startCDATA() throws SAXException {
+		// ignore
+	}
+
+	@Override
+	public void endCDATA() throws SAXException {
+		// ignore
+	}
+
+	@Override
+	public void comment(final char[] ch, final int start, final int length) throws SAXException {
+		// ignore
+	}
+
+	@Override
+	public InputSource resolveEntity(final String publicId, final String systemId) throws IOException, SAXException {
+		return new InputSource(new StringReader(""));
+	}
+
+	private static class AbortParsingException extends SAXException {
+		private static final long serialVersionUID = 1L;
+	}
+
+}
diff --git a/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/contenttype/VexContentDescriber.java b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/contenttype/VexContentDescriber.java
new file mode 100644
index 0000000..8319497
--- /dev/null
+++ b/org.eclipse.vex.ui/src/org/eclipse/vex/ui/internal/contenttype/VexContentDescriber.java
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * Copyright (c) 2014 Florian Thienel and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * 		Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.ui.internal.contenttype;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
+import org.eclipse.core.runtime.content.IContentDescription;
+import org.eclipse.core.runtime.content.XMLContentDescriber;
+import org.eclipse.vex.ui.internal.VexPlugin;
+import org.eclipse.vex.ui.internal.config.DocumentType;
+import org.xml.sax.InputSource;
+
+/**
+ * @author Florian Thienel
+ */
+public class VexContentDescriber extends XMLContentDescriber {
+
+	@Override
+	public int describe(final InputStream contents, final IContentDescription description) throws IOException {
+		if (super.describe(contents, description) == INVALID) {
+			return INVALID;
+		}
+		contents.reset();
+		return checkCriteria(new InputSource(contents), description);
+	}
+
+	@Override
+	public int describe(final Reader contents, final IContentDescription description) throws IOException {
+		if (super.describe(contents, description) == INVALID) {
+			return INVALID;
+		}
+		contents.reset();
+		return checkCriteria(new InputSource(contents), description);
+	}
+
+	private int checkCriteria(final InputSource contents, final IContentDescription description) {
+		final InferXmlContentTypeHandler handler = new InferXmlContentTypeHandler();
+		if (!handler.parseContents(contents)) {
+			return INVALID;
+		}
+
+		final DocumentType documentType = VexPlugin.getDefault().getConfigurationRegistry().getDocumentType(handler.getMainDocumentTypeIdentifier(), "");
+		if (documentType != null) {
+			return VALID;
+		}
+		return INDETERMINATE;
+	}
+}
diff --git a/org.eclipse.vex.xhtml/plugin.xml b/org.eclipse.vex.xhtml/plugin.xml
index 18c38e2..46ff03e 100644
--- a/org.eclipse.vex.xhtml/plugin.xml
+++ b/org.eclipse.vex.xhtml/plugin.xml
@@ -4,7 +4,7 @@
 	<extension point="org.eclipse.core.contenttype.contentTypes">

 		<content-type

         base-type="org.eclipse.vex.ui.XmlDocument"

-        file-extensions="xml,xhtml"

+        file-extensions="xhtml"

         id="org.eclipse.vex.ui.xhtml"

         name="%contentType.name"

         priority="low">