blob: bb0d8c0df2571c4a9b2e18f179e2df0834fea788 [file] [log] [blame]
/*******************************************************************************
* 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;
}
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$
// }
// }
}