| /******************************************************************************* |
| * Copyright (c) 2007, 2008 IBM Corporation 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: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.equinox.internal.p2.persistence; |
| |
| import java.util.List; |
| import java.util.StringTokenizer; |
| import javax.xml.parsers.*; |
| import org.eclipse.core.runtime.*; |
| import org.eclipse.equinox.internal.p2.core.Activator; |
| import org.eclipse.equinox.internal.p2.core.StringPool; |
| import org.eclipse.equinox.internal.p2.core.helpers.OrderedProperties; |
| import org.eclipse.equinox.internal.p2.core.helpers.Tracing; |
| import org.eclipse.osgi.service.resolver.VersionRange; |
| import org.eclipse.osgi.util.NLS; |
| import org.osgi.framework.BundleContext; |
| import org.osgi.framework.Version; |
| import org.osgi.util.tracker.ServiceTracker; |
| import org.xml.sax.*; |
| import org.xml.sax.helpers.DefaultHandler; |
| |
| public abstract class XMLParser extends DefaultHandler implements XMLConstants { |
| |
| // Get the root object that is being parsed. |
| protected abstract Object getRootObject(); |
| |
| // Get a generic parser error message for inclusion in an error status |
| protected abstract String getErrorMessage(); |
| |
| protected BundleContext context; // parser class bundle context |
| protected String bundleId; // parser class bundle id |
| |
| protected XMLReader xmlReader; // the XML reader for the parser |
| |
| protected MultiStatus status = null; // accumulation of non-fatal errors |
| protected Locator locator = null; // document locator, if supported by the parser |
| |
| protected StringPool stringPool = new StringPool();//used to eliminate string duplication |
| private IProgressMonitor monitor; |
| |
| private static ServiceTracker xmlTracker = null; |
| |
| public XMLParser(BundleContext context, String pluginId) { |
| super(); |
| this.context = context; |
| this.bundleId = pluginId; |
| } |
| |
| /** |
| * Non-fatal errors accumulated during parsing. |
| */ |
| public IStatus getStatus() { |
| return (status != null ? status : Status.OK_STATUS); |
| } |
| |
| /** |
| * Returns the canonical form of a string. Used to eliminate duplicate equal |
| * strings. |
| */ |
| protected String canonicalize(String string) { |
| return stringPool == null ? string : stringPool.add(string); |
| } |
| |
| public boolean isValidXML() { |
| return (status == null || !status.matches(IStatus.ERROR | IStatus.CANCEL)); |
| } |
| |
| private static SAXParserFactory acquireXMLParsing(BundleContext context) { |
| if (xmlTracker == null) { |
| xmlTracker = new ServiceTracker(context, SAXParserFactory.class.getName(), null); |
| xmlTracker.open(); |
| } |
| return (SAXParserFactory) xmlTracker.getService(); |
| } |
| |
| protected static void releaseXMLParsing() { |
| if (xmlTracker != null) { |
| xmlTracker.close(); |
| } |
| } |
| |
| protected SAXParser getParser() throws ParserConfigurationException, SAXException { |
| SAXParserFactory factory = acquireXMLParsing(this.context); |
| if (factory == null) { |
| throw new SAXException(Messages.XMLParser_No_SAX_Parser); |
| } |
| factory.setNamespaceAware(true); |
| factory.setValidating(false); |
| try { |
| factory.setFeature("http://xml.org/sax/features/string-interning", true); //$NON-NLS-1$ |
| } catch (SAXException se) { |
| // some parsers may not support string interning |
| } |
| SAXParser theParser = factory.newSAXParser(); |
| if (theParser == null) { |
| throw new SAXException(Messages.XMLParser_No_SAX_Parser); |
| } |
| xmlReader = theParser.getXMLReader(); |
| return theParser; |
| } |
| |
| public static String makeSimpleName(String localName, String qualifiedName) { |
| if (localName != null && localName.length() > 0) { |
| return localName; |
| } |
| int nameSpaceIndex = qualifiedName.indexOf(":"); //$NON-NLS-1$ |
| return (nameSpaceIndex == -1 ? qualifiedName : qualifiedName.substring(nameSpaceIndex + 1)); |
| } |
| |
| /** |
| * Set the document locator for the parser |
| * |
| * @see org.xml.sax.ContentHandler#setDocumentLocator |
| */ |
| public void setDocumentLocator(Locator docLocator) { |
| locator = docLocator; |
| } |
| |
| /** |
| * Sets the progress monitor for the parser |
| */ |
| protected void setProgressMonitor(IProgressMonitor monitor) { |
| this.monitor = monitor; |
| } |
| |
| /** |
| * Abstract base class for content handlers |
| */ |
| protected abstract class AbstractHandler extends DefaultHandler { |
| |
| protected ContentHandler parentHandler = null; |
| protected String elementHandled = null; |
| |
| protected StringBuffer characters = null; // character data inside an element |
| |
| public AbstractHandler() { |
| // Empty constructor for a root handler |
| } |
| |
| public AbstractHandler(ContentHandler parentHandler) { |
| this.parentHandler = parentHandler; |
| xmlReader.setContentHandler(this); |
| } |
| |
| public AbstractHandler(ContentHandler parentHandler, String elementHandled) { |
| this.parentHandler = parentHandler; |
| xmlReader.setContentHandler(this); |
| this.elementHandled = elementHandled; |
| } |
| |
| /** |
| * Set the document locator for the parser |
| * |
| * @see org.xml.sax.ContentHandler#setDocumentLocator |
| */ |
| public void setDocumentLocator(Locator docLocator) { |
| locator = docLocator; |
| } |
| |
| public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { |
| finishCharacters(); |
| String name = makeSimpleName(localName, qName); |
| trace(name, attributes); |
| startElement(name, attributes); |
| } |
| |
| public abstract void startElement(String name, Attributes attributes) throws SAXException; |
| |
| public void invalidElement(String name, Attributes attributes) { |
| unexpectedElement(this, name, attributes); |
| new IgnoringHandler(this); |
| } |
| |
| public void endElement(String namespaceURI, String localName, String qName) { |
| // TODO: throw a bad state error if makeSimpleName(localName, qName) != elementHandled |
| finishCharacters(); |
| finished(); |
| // Restore the parent content handler |
| xmlReader.setContentHandler(parentHandler); |
| } |
| |
| /** |
| * An implementation for startElement when there are no sub-elements |
| */ |
| protected void noSubElements(String name, Attributes attributes) { |
| unexpectedElement(this, name, attributes); |
| // Create a new handler to ignore subsequent nested elements |
| new IgnoringHandler(this); |
| } |
| |
| /* |
| * Save up character data until endElement or nested startElement |
| * |
| * @see org.xml.sax.ContentHandler#characters |
| */ |
| public void characters(char[] chars, int start, int length) { |
| if (this.characters == null) { |
| this.characters = new StringBuffer(); |
| } |
| this.characters.append(chars, start, length); |
| } |
| |
| // Consume the characters accumulated in this.characters. |
| // Called before startElement or endElement |
| private String finishCharacters() { |
| // common case -- no characters or only whitespace |
| if (this.characters == null || this.characters.length() == 0) { |
| return null; |
| } |
| if (allWhiteSpace(this.characters)) { |
| this.characters.setLength(0); |
| return null; |
| } |
| |
| // process the characters |
| try { |
| String trimmedChars = this.characters.toString().trim(); |
| if (trimmedChars.length() == 0) { |
| // this shouldn't happen due to the test for allWhiteSpace above |
| System.err.println("Unexpected non-whitespace characters: " //$NON-NLS-1$ |
| + trimmedChars); |
| return null; |
| } |
| processCharacters(trimmedChars); |
| return trimmedChars; |
| } finally { |
| this.characters.setLength(0); |
| } |
| } |
| |
| // Method to override in the handler of an element with CDATA. |
| protected void processCharacters(String data) { |
| if (data.length() > 0) { |
| unexpectedCharacterData(this, data); |
| } |
| } |
| |
| private boolean allWhiteSpace(StringBuffer sb) { |
| int length = sb.length(); |
| for (int i = 0; i < length; i += 1) { |
| if (!Character.isWhitespace(sb.charAt(i))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Called when this element and all elements nested into it have been |
| * handled. |
| */ |
| protected void finished() { |
| // Do nothing by default |
| } |
| |
| /* |
| * A name used to identify the handler. |
| */ |
| public String getName() { |
| return (elementHandled != null ? elementHandled : "NoName"); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Parse the attributes of an element with two required attributes. |
| */ |
| protected String[] parseRequiredAttributes(Attributes attributes, String name1, String name2) { |
| return parseRequiredAttributes(attributes, new String[] {name1, name2}); |
| } |
| |
| /** |
| * Parse the attributes of an element with only required attributes. |
| */ |
| protected String[] parseRequiredAttributes(Attributes attributes, String[] required) { |
| return parseAttributes(attributes, required, noAttributes); |
| } |
| |
| /** |
| * Parse the attributes of an element with a single optional attribute. |
| */ |
| protected String parseOptionalAttribute(Attributes attributes, String name) { |
| return parseAttributes(attributes, noAttributes, new String[] {name})[0]; |
| } |
| |
| /** |
| * Parse the attributes of an element, given the list of required and optional ones. |
| * Return values in same order, null for those not present. |
| * Log warnings for extra attributes or missing required attributes. |
| */ |
| protected String[] parseAttributes(Attributes attributes, String[] required, String[] optional) { |
| String[] result = new String[required.length + optional.length]; |
| for (int i = 0; i < attributes.getLength(); i += 1) { |
| String name = attributes.getLocalName(i); |
| String value = canonicalize(attributes.getValue(i).trim()); |
| int j; |
| if ((j = indexOf(required, name)) >= 0) { |
| result[j] = value; |
| } else if ((j = indexOf(optional, name)) >= 0) { |
| result[required.length + j] = value; |
| } else { |
| unexpectedAttribute(elementHandled, name, value); |
| } |
| } |
| for (int i = 0; i < required.length; i += 1) { |
| checkRequiredAttribute(elementHandled, required[i], result[i]); |
| } |
| return result; |
| } |
| |
| } |
| |
| /** |
| * Handler for an XML document. |
| * |
| * Using the inelegant name 'DocHandler' to clearly distinguish |
| * this class from the deprecated org.xml.sax.DocumentHandler. |
| */ |
| protected class DocHandler extends AbstractHandler { |
| |
| RootHandler rootHandler; |
| |
| public DocHandler(String rootName, RootHandler rootHandler) { |
| super(null, rootName); |
| this.rootHandler = rootHandler; |
| } |
| |
| public void startElement(String name, Attributes attributes) { |
| if (name.equals(elementHandled)) { |
| rootHandler.initialize(this, name, attributes); |
| xmlReader.setContentHandler(rootHandler); |
| } else { |
| this.noSubElements(name, attributes); |
| } |
| } |
| |
| } |
| |
| /** |
| * Abstract handler for the root element. |
| */ |
| protected abstract class RootHandler extends AbstractHandler { |
| |
| public RootHandler() { |
| super(); |
| } |
| |
| public void initialize(DocHandler document, String rootName, Attributes attributes) { |
| this.parentHandler = document; |
| this.elementHandled = rootName; |
| handleRootAttributes(attributes); |
| } |
| |
| protected abstract void handleRootAttributes(Attributes attributes); |
| |
| } |
| |
| /** |
| * Handler for an ordered properties collection. |
| */ |
| protected class PropertiesHandler extends AbstractHandler { |
| |
| private OrderedProperties properties; |
| |
| public PropertiesHandler(ContentHandler parentHandler, Attributes attributes) { |
| super(parentHandler, PROPERTIES_ELEMENT); |
| String size = parseOptionalAttribute(attributes, COLLECTION_SIZE_ATTRIBUTE); |
| properties = (size != null ? new OrderedProperties(new Integer(size).intValue()) : new OrderedProperties()); |
| } |
| |
| public OrderedProperties getProperties() { |
| return properties; |
| } |
| |
| public void startElement(String name, Attributes attributes) { |
| if (name.equals(PROPERTY_ELEMENT)) { |
| new PropertyHandler(this, attributes, properties); |
| } else { |
| invalidElement(name, attributes); |
| } |
| } |
| |
| } |
| |
| /** |
| * Handler for a property in an ordered properties collection. |
| */ |
| protected class PropertyHandler extends AbstractHandler { |
| |
| public PropertyHandler(ContentHandler parentHandler, Attributes attributes, OrderedProperties properties) { |
| super(parentHandler, PROPERTY_ELEMENT); |
| String[] property = parseProperty(attributes); |
| if (isValidProperty(property)) { |
| properties.setProperty(property[0], property[1]); |
| } |
| } |
| |
| public void startElement(String name, Attributes attributes) { |
| invalidElement(name, attributes); |
| } |
| |
| private String[] parseProperty(Attributes attributes) { |
| return parseRequiredAttributes(attributes, PROPERTY_NAME_ATTRIBUTE, PROPERTY_VALUE_ATTRIBUTE); |
| } |
| |
| private boolean isValidProperty(String[] property) { |
| return (property.length == 2 && property[0] != null && property[1] != null); |
| } |
| } |
| |
| /** |
| * Handler for an element with only cdata and no sub-elements. |
| */ |
| protected class TextHandler extends AbstractHandler { |
| |
| private String text = null; |
| |
| private List texts = null; |
| |
| // Constructor for a subclass that processes the attributes |
| public TextHandler(AbstractHandler parent, String elementName) { |
| super(parent, elementName); |
| } |
| |
| // Constructor for a subclass with no attributes |
| public TextHandler(AbstractHandler parent, String elementName, Attributes attributes) { |
| super(parent, elementName); |
| parseAttributes(attributes, noAttributes, noAttributes); |
| } |
| |
| public TextHandler(AbstractHandler parent, String elementName, Attributes attributes, List texts) { |
| super(parent, elementName); |
| parseAttributes(attributes, noAttributes, noAttributes); |
| this.texts = texts; |
| } |
| |
| public String getText() { |
| return (text != null ? text : ""); //$NON-NLS-1$ |
| } |
| |
| public void startElement(String name, Attributes attributes) { |
| invalidElement(name, attributes); |
| } |
| |
| protected void processCharacters(String data) { |
| this.text = data; |
| if (texts != null) { |
| texts.add(getText()); |
| } |
| } |
| |
| } |
| |
| /** |
| * Handler for ignoring content. |
| */ |
| protected class IgnoringHandler extends AbstractHandler { |
| |
| public IgnoringHandler(AbstractHandler parent) { |
| super(parent); |
| this.elementHandled = "IgnoringAll"; //$NON-NLS-1$ |
| } |
| |
| public void startElement(String name, Attributes attributes) { |
| noSubElements(name, attributes); |
| } |
| |
| } |
| |
| // Helpers for processing instructions that include a Class and/or a Version. |
| |
| public String extractPIClass(String data) { |
| return extractPIAttribute(data, PI_CLASS_ATTRIBUTE); |
| } |
| |
| public Version extractPIVersion(String target, String data) { |
| return checkVersion(target, PI_VERSION_ATTRIBUTE, extractPIAttribute(data, PI_VERSION_ATTRIBUTE)); |
| } |
| |
| private String extractPIAttribute(String data, String key) { |
| StringTokenizer piTokenizer = new StringTokenizer(data, " \'\""); //$NON-NLS-1$ |
| String[] tokens = new String[piTokenizer.countTokens()]; |
| int index = 0; |
| int valueIndex = -1; |
| while (piTokenizer.hasMoreTokens() && index < tokens.length) { |
| tokens[index] = piTokenizer.nextToken(); |
| if (tokens[index].equals(key + '=') && index < tokens.length) { |
| valueIndex = index + 1; |
| } |
| index++; |
| } |
| return (valueIndex >= 0 ? tokens[valueIndex] : ""); //$NON-NLS-1$ |
| } |
| |
| public void error(SAXParseException ex) { |
| addError(IStatus.WARNING, ex.getMessage(), ex); |
| } |
| |
| public void fatalError(SAXParseException ex) { |
| addError(IStatus.ERROR, ex.getMessage(), ex); |
| } |
| |
| protected String getErrorPrefix() { |
| return null; |
| } |
| |
| protected String getErrorSuffix() { |
| return null; |
| } |
| |
| /** |
| * Collects an error or warning that occurred during parsing. |
| */ |
| public final void addError(int severity, String msg, Throwable exception) { |
| int line = 0; |
| int column = 0; |
| String key = msg; |
| Object[] args = new Object[] {}; |
| String root = (getRootObject() == null ? "" //$NON-NLS-1$ |
| : " (" + getRootObject() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ |
| if (this.locator != null) { |
| String name = this.locator.getSystemId(); |
| line = this.locator.getLineNumber(); |
| column = this.locator.getColumnNumber(); |
| if (line > 0) { |
| args = new Object[] {msg, root, name, new Integer(line), new Integer(column)}; |
| if (column > 0) { |
| key = (name != null ? Messages.XMLParser_Error_At_Name_Line_Column // |
| : Messages.XMLParser_Error_At_Line_Column); |
| } else { |
| key = (name != null ? Messages.XMLParser_Error_At_Name_Line // |
| : Messages.XMLParser_Error_At_Line); |
| } |
| } |
| } |
| String errMsg = NLS.bind(key, args); |
| String prefix = getErrorPrefix(); |
| String suffix = getErrorSuffix(); |
| if (prefix != null) { |
| errMsg = prefix + errMsg; |
| } |
| if (suffix != null) { |
| errMsg = errMsg + suffix; |
| } |
| IStatus currStatus = new Status(severity, Activator.ID, errMsg, exception); |
| if (this.status == null) { |
| this.status = new MultiStatus(bundleId, IStatus.OK, new IStatus[] {currStatus}, getErrorMessage(), null); |
| } else { |
| this.status.add(currStatus); |
| } |
| } |
| |
| public void trace(String element, Attributes attributes) { |
| // TODO: support logging |
| // if (!getLogger().isDebugLoggable()) { |
| // return; |
| // } |
| // int indentSize = (this.stateStack != null ? this.stateStack.size() - 1 : 1); |
| // if (attributes == null) { |
| // indentSize -= 1; |
| // } |
| // char[] indent = new char[2 * indentSize]; |
| // Arrays.fill(indent, ' '); |
| // StringBuffer sb = new StringBuffer(); |
| // sb.append(indent); |
| // sb.append('<'); |
| // if (attributes != null) { |
| // sb.append(element); |
| // toString(sb, attributes); |
| // } else { |
| // sb.append('/').append(element); |
| // } |
| // sb.append('>'); |
| // getLogger().debug(sb.toString()); |
| } |
| |
| private static String toString(Attributes attributes) { |
| StringBuffer result = new StringBuffer(); |
| toString(result, attributes); |
| return result.toString(); |
| } |
| |
| private static void toString(StringBuffer sb, Attributes attributes) { |
| for (int i = 0; i < attributes.getLength(); i += 1) { |
| String name = attributes.getLocalName(i); |
| String value = attributes.getValue(i).trim(); |
| sb.append(' ').append(name); |
| sb.append('=').append('"'); |
| sb.append(value); |
| sb.append('"'); |
| } |
| } |
| |
| public void checkRequiredAttribute(String element, String name, Object value) { |
| if (value == null) { |
| addError(IStatus.WARNING, NLS.bind(Messages.XMLParser_Missing_Required_Attribute, element, name), null); |
| } |
| } |
| |
| // Check the format of a required boolean attribute |
| public Boolean checkBoolean(String element, String attribute, String value) { |
| try { |
| return Boolean.valueOf(value); |
| } catch (IllegalArgumentException iae) { |
| invalidAttributeValue(element, attribute, value); |
| } catch (NullPointerException npe) { |
| invalidAttributeValue(element, attribute, null); |
| } |
| return Boolean.FALSE; |
| } |
| |
| // Check the format of an optional boolean attribute |
| public Boolean checkBoolean(String element, String attribute, String value, boolean defaultValue) { |
| Boolean result = (defaultValue ? Boolean.TRUE : Boolean.FALSE); |
| if (value != null) { |
| try { |
| return Boolean.valueOf(value); |
| } catch (IllegalArgumentException iae) { |
| invalidAttributeValue(element, attribute, value); |
| } |
| } |
| return result; |
| } |
| |
| // Check the format of a required integer attribute |
| public int checkInteger(String element, String attribute, String value) { |
| try { |
| return Integer.parseInt(value); |
| } catch (IllegalArgumentException iae) { |
| invalidAttributeValue(element, attribute, value); |
| } |
| return 0; |
| } |
| |
| public void checkCancel() { |
| if (monitor != null && monitor.isCanceled()) |
| throw new OperationCanceledException(); |
| } |
| |
| /** |
| * Converts a version string to a Version object. Returns the version object, |
| * or {@link Version#emptyVersion} if the value was not a valid version. |
| */ |
| public Version checkVersion(String element, String attribute, String value) { |
| try { |
| if (value != null) |
| return new Version(value); |
| } catch (IllegalArgumentException iae) { |
| invalidAttributeValue(element, attribute, value); |
| } catch (NullPointerException npe) { |
| invalidAttributeValue(element, attribute, null); |
| } |
| return Version.emptyVersion; |
| } |
| |
| public VersionRange checkVersionRange(String element, String attribute, String value) { |
| try { |
| return new VersionRange(value); |
| } catch (IllegalArgumentException iae) { |
| invalidAttributeValue(element, attribute, value); |
| } catch (NullPointerException npe) { |
| invalidAttributeValue(element, attribute, null); |
| } |
| return VersionRange.emptyRange; |
| } |
| |
| public void unexpectedAttribute(String element, String attribute, String value) { |
| if (Tracing.DEBUG_PARSE_PROBLEMS) |
| Tracing.debug("Unexpected attribute for element " + element + ": " + attribute + '=' + value); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| public void invalidAttributeValue(String element, String attribute, String value) { |
| addError(IStatus.WARNING, NLS.bind(Messages.XMLParser_Illegal_Value_For_Attribute, new Object[] {attribute, element, value}), null); |
| } |
| |
| public void unexpectedElement(AbstractHandler handler, String element, Attributes attributes) { |
| if (Tracing.DEBUG_PARSE_PROBLEMS) |
| Tracing.debug("Unexpected element in element " + handler.getName() + ": <" + element + toString(attributes) + '>'); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| public void duplicateElement(AbstractHandler handler, String element, Attributes attributes) { |
| addError(IStatus.WARNING, NLS.bind(Messages.XMLParser_Duplicate_Element, new Object[] {handler.getName(), element, toString(attributes)}), null); |
| } |
| |
| public void unexpectedCharacterData(AbstractHandler handler, String cdata) { |
| if (Tracing.DEBUG_PARSE_PROBLEMS) |
| Tracing.debug("Unexpected character data in element " + handler.getName() + ": " + cdata.trim()); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| /** |
| * Find the index of the first occurrence of object in array, or -1. |
| * Use Arrays.binarySearch if array is big and sorted. |
| */ |
| protected static int indexOf(String[] array, String value) { |
| for (int i = 0; i < array.length; i += 1) { |
| if (value == null ? array[i] == null : value.equals(array[i])) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| // public class BadStateError extends AssertionError { |
| // private static final long serialVersionUID = 1L; // not serialized |
| // |
| // public BadStateError() { |
| // super("unexpected state" + //$NON-NLS-1$ |
| // (XMLParser.this.stateStack != null ? ": " + XMLParser.this.stateStack //$NON-NLS-1$ |
| // : "")); //$NON-NLS-1$ |
| // } |
| // |
| // public BadStateError(String element) { |
| // super("unexpected state for " + element + //$NON-NLS-1$ |
| // (XMLParser.this.stateStack != null ? ": " + XMLParser.this.stateStack //$NON-NLS-1$ |
| // : "")); //$NON-NLS-1$ |
| // } |
| // } |
| |
| } |