/*******************************************************************************
 * Copyright (c) 2004, 2009 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Jesper Steen Moeller - added namespace support
 *******************************************************************************/
package org.eclipse.core.runtime.content;

import java.io.*;
import java.util.*;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.core.internal.content.ContentMessages;
import org.eclipse.core.internal.content.XMLRootHandler;
import org.eclipse.core.internal.runtime.RuntimeLog;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * A content describer for detecting the name of the top-level element,
 * its namespace and the DTD system identifier in an XML file.
 * <p>
 * This executable extension supports "element" parameter, that
 * can be specified more than once. If the
 * <code>":-"</code> method is used, then the value is treated as
 * "element" (always just one)
 * </p>
 * <p>
 * The value of "element" is specified using such a format
 * <code>{namespace}name/dtd</code>. The namespace or dtd part
 * can be omitted and accepted are values like <code>name/dtd</code>,
 * <code>{ns}name</code> and <code>name</code>.
 * </p>
 * <p>
 * The describer will detect a document, if it matches at least one "element"
 * what means, that dtd, namespace (if specified) and name in "element"
 * match those in the document.
 * </p>
 * <p>
 * If the "element" name part is "*", e.g. <code>{namespace}*</code>,
 * it denotes a wildcard match. If the "element" namespace part is empty,
 * e.g. <code>{}name</code>, only these documents with the root element
 * that belong to the unnamed namespace <code><elem xmlns=""></code>
 * will be detected.
 * </p>
 * <p>
 * This class should be used instead of {@link XMLRootElementContentDescriber}
 * which doesn't detect namespaces and doesn't allow to specify
 * more than one set of dtds, root element names and namespaces which
 * should be detected.
 * </p>
 * <p>
 * This class is not intended to be subclassed or instantiated by clients,
 * only to be referenced by the "describer" configuration element in
 * extensions to the <code>org.eclipse.core.runtime.contentTypes</code>
 * extension point.
 * </p>
 *
 * @since org.eclipse.core.contenttype 3.3
 * @noinstantiate This class is not intended to be instantiated by clients.
 */
public final class XMLRootElementContentDescriber2 extends XMLContentDescriber implements IExecutableExtension {

	static final String DTD = "org.eclipse.core.runtime.content.XMLRootElementContentDescriber2.dtd"; //$NON-NLS-1$
	static final String NAMESPACE = "org.eclipse.core.runtime.content.XMLRootElementContentDescriber2.namespace"; //$NON-NLS-1$
	static final String ELEMENT = "org.eclipse.core.runtime.content.XMLRootElementContentDescriber2.element"; //$NON-NLS-1$
	static final String RESULT = "org.eclipse.core.runtime.content.XMLRootElementContentDescriber2.result"; //$NON-NLS-1$

	private static final String ELEMENT_TO_FIND = "element"; //$NON-NLS-1$

	/* (Intentionally not included in javadoc)
	 * The top-level elements we are looking for. This value will be initialized
	 * by the <code>setInitializationData</code> method. If no value is
	 * provided, then this means that we don't care what the top-level element
	 * will be. The list is a collection of <code>QualifiedElement</code>.
	 */
	private QualifiedElement[] elementsToFind = null;

	/* (Intentionally not included in javadoc)
	 * Simple value holder for root element name, its namespace and dtd.
	 */
	private class QualifiedElement {
		private String namespace;
		private String element;
		private String dtd;

		public QualifiedElement(String qualifiedElement) {
			// Extract namespace part
			int openBrace = qualifiedElement.indexOf('{');
			int closeBrace = qualifiedElement.indexOf('}');
			if (openBrace == 0 && closeBrace >= 1) {
				namespace = qualifiedElement.substring(1, closeBrace);
				qualifiedElement = qualifiedElement.substring(closeBrace + 1);
			}
			// Extract dtd part
			int dtdSlash = qualifiedElement.indexOf('/');
			if (dtdSlash > 0) {
				dtd = qualifiedElement.substring(dtdSlash + 1);
				qualifiedElement = qualifiedElement.substring(0, dtdSlash);
			}
			// Check if the name is a wildcard
			element = ("*".equals(qualifiedElement) ? null : qualifiedElement); //$NON-NLS-1$
		}

		public boolean matches(String someNamespace, String someElement, String someDtd) {
			boolean nsMatch = this.namespace != null ? this.namespace.equals(someNamespace) : true;
			boolean elementEquals = this.element != null ? this.element.equals(someElement) : true;
			boolean dtdEquals = this.dtd != null ? this.dtd.equals(someDtd) : true;
			return nsMatch && elementEquals && dtdEquals;
		}
	}

	/* (Intentionally not included in javadoc)
	 * Determines the validation status for the given contents.
	 *
	 * @param contents the contents to be evaluated
	 * @return one of the following:<ul>
	 * <li><code>VALID</code></li>,
	 * <li><code>INVALID</code></li>,
	 * <li><code>INDETERMINATE</code></li>
	 * </ul>
	 * @throws IOException
	 */
	private int checkCriteria(InputSource contents, Map<String, Object> properties) throws IOException {
		if (!isProcessed(properties))
			fillContentProperties(contents, properties);
		return checkCriteria(properties);
	}

	private int checkCriteria(Map<String, Object> properties) throws IOException {
		Boolean result = (Boolean) properties.get(RESULT);
		if (!result.booleanValue())
			return INDETERMINATE;
		// Check to see if we matched our criteria.
		if (elementsToFind != null) {
			boolean foundOne = false;
			for (int i = 0; i < elementsToFind.length && !foundOne; ++i) {
				String dtd = (String) properties.get(DTD);
				String namespace = (String) properties.get(NAMESPACE);
				String element = (String) properties.get(ELEMENT);
				foundOne |= elementsToFind[i].matches(namespace, element, dtd);
			}
			if (!foundOne)
				return INDETERMINATE;
		}
		// We must be okay then.
		return VALID;
	}

	@Override
	public int describe(InputStream contents, IContentDescription description) throws IOException {
		return describe(contents, description, new HashMap<String, Object>());
	}

	/**
	 * @noreference This method is not intended to be referenced by clients.
	 */
	public int describe(InputStream contents, IContentDescription description, Map<String, Object> properties) throws IOException {
		// call the basic XML describer to do basic recognition
		if (super.describe2(contents, description, properties) == INVALID)
			return INVALID;
		// super.describe will have consumed some chars, need to rewind
		contents.reset();
		// Check to see if we matched our criteria.
		return checkCriteria(new InputSource(contents), properties);
	}

	@Override
	public int describe(Reader contents, IContentDescription description) throws IOException {
		return describe(contents, description, new HashMap<String, Object>());
	}

	/**
	 * @noreference This method is not intended to be referenced by clients.
	 */
	public int describe(Reader contents, IContentDescription description, Map<String, Object> properties) throws IOException {
		// call the basic XML describer to do basic recognition
		if (super.describe2(contents, description, properties) == INVALID)
			return INVALID;
		// super.describe will have consumed some chars, need to rewind
		contents.reset();
		// Check to see if we matched our criteria.
		return checkCriteria(new InputSource(contents), properties);
	}

	static boolean isProcessed(Map<String, Object> properties) {
		Boolean result = (Boolean) properties.get(RESULT);
		// It can be set to false which means that content can't be parsed
		if (result != null)
			return true;
		return false;
	}

	static void fillContentProperties(InputSource input, Map<String, Object> properties) throws IOException {
		XMLRootHandler xmlHandler = new XMLRootHandler(true);
		try {
			if (!xmlHandler.parseContents(input)) {
				properties.put(RESULT, Boolean.FALSE);
				return;
			}
		} catch (SAXException e) {
			// we may be handed any kind of contents... it is normal we fail to parse
			properties.put(RESULT, Boolean.FALSE);
			return;
		} catch (ParserConfigurationException e) {
			// some bad thing happened - force this describer to be disabled
			String message = ContentMessages.content_parserConfiguration;
			RuntimeLog.log(new Status(IStatus.ERROR, ContentMessages.OWNER_NAME, 0, message, e));
			throw new RuntimeException(message);
		}
		String element = xmlHandler.getRootName();
		if (element != null)
			properties.put(ELEMENT, element);
		String dtd = xmlHandler.getDTD();
		if (dtd != null)
			properties.put(DTD, dtd);
		String namespace = xmlHandler.getRootNamespace();
		if (namespace != null)
			properties.put(NAMESPACE, namespace);
		properties.put(RESULT, Boolean.TRUE);
	}

	@Override
	public void setInitializationData(final IConfigurationElement config, final String propertyName, final Object data) throws CoreException {
		if (data instanceof String)
			elementsToFind = new QualifiedElement[] {new QualifiedElement((String) data)};
		else if (data instanceof Hashtable) {
			List<QualifiedElement> elements = null;

			// the describer parameters have to be read again, because "element" parameter can be specified multiple times
			// and the given hashtable carries only one of them
			IConfigurationElement describerElement = config.getChildren("describer")[0]; //$NON-NLS-1$
			IConfigurationElement[] params = describerElement.getChildren("parameter"); //$NON-NLS-1$
			String pname = null;
			for (IConfigurationElement param : params) {
				pname = param.getAttribute("name"); //$NON-NLS-1$
				if (ELEMENT_TO_FIND.equals(pname)) {
					if (elements == null)
						elements = new LinkedList<>();
					elements.add(new QualifiedElement(param.getAttribute("value"))); //$NON-NLS-1$
				}
			}

			List<QualifiedElement> qualifiedElements = new ArrayList<>();

			// create list of qualified elements
			if (elements != null) {
				for (QualifiedElement qualifiedElement : elements) {
					qualifiedElements.add(qualifiedElement);
				}
			}
			elementsToFind = qualifiedElements.toArray(new QualifiedElement[qualifiedElements.size()]);
		}

		if (elementsToFind.length == 0) {
			String message = NLS.bind(ContentMessages.content_badInitializationData, XMLRootElementContentDescriber2.class.getName());
			throw new CoreException(new Status(IStatus.ERROR, ContentMessages.OWNER_NAME, 0, message, null));
		}
	}
}
