/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.felix.metatype; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import org.apache.felix.metatype.internal.Activator; import org.kxml2.io.KXmlParser; import org.osgi.service.log.LogService; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; /** * The <code>MetaDataReader</code> provides two methods to read meta type * documents according to the MetaType schema (105.8 XML Schema). The * {@link #parse(URL)} and {@link #parse(InputStream)} methods may be called * multiple times to parse such documents. * <p> * While reading the XML document java objects are created to hold the data. * These objects are created by factory methods. Users of this may extend this * class by overwriting the the factory methods to create specialized versions. * One notable use of this is the extension of the {@link AD} class to overwrite * the {@link AD#validate(String)} method. In this case, the {@link #createAD()} * method would be overwritten to return an instance of the extending class. * <p> * This class is not thread safe. Using instances of this class in multiple * threads concurrently is not supported and will fail. * * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a> */ public class MetaDataReader { /** * The initial XML Namespace for Metatype 1.1 descriptors. This has * been replaced by the v1.1.0 namespace in the Compendium * Specification 4.2. We still have to support this namespace for * backwards compatibility. */ static final String NAMESPACE_1_0 = "http://www.osgi.org/xmlns/metatype/v1.0.0"; /** * The XML Namespace for Metatype 1.1 descriptors. */ static final String NAMESPACE_1_1 = "http://www.osgi.org/xmlns/metatype/v1.1.0"; /** * The XML Namespace for Metatype 1.2 descriptors. */ static final String NAMESPACE_1_2 = "http://www.osgi.org/xmlns/metatype/v1.2.0"; /** * The XML Namespace for Metatype 1.3 descriptors. */ static final String NAMESPACE_1_3 = "http://www.osgi.org/xmlns/metatype/v1.3.0"; /** The XML parser used to read the XML documents */ private KXmlParser parser = new KXmlParser(); private String namespace = NAMESPACE_1_0; private URL documentURL; /** Sets of attributes belonging to XML elements. */ private static final Set<String> AD_ATTRIBUTES = new HashSet<String>(Arrays.asList(new String[] { "name", "description", "id", "type", "cardinality", "min", "max", "default", "required" })); private static final Set<String> ATTRIBUTE_ATTRIBUTES = new HashSet<String>(Arrays.asList(new String[] { "adref", "content" })); private static final Set<String> DESIGNATE_ATTRIBUTES = new HashSet<String>(Arrays.asList(new String[] { "pid", "factoryPid", "bundle", "optional", "merge" })); private static final Set<String> DESIGNATEOBJECT_ATTRIBUTES = new HashSet<String>(Arrays.asList(new String[] { "ocdref" })); private static final Set<String> METADATA_ATTRIBUTES = new HashSet<String>(Arrays.asList(new String[] { "localization" })); private static final Set<String> OCD_ATTRIBUTES = new HashSet<String>(Arrays.asList(new String[] { "name", "description", "id" })); /** * Parses the XML document provided by the <code>url</code>. The XML document * must be at the beginning of the stream contents. * <p> * This method is almost identical to * <code>return parse(url.openStream());</code> but also sets the string * representation of the URL as a location helper for error messages. * * @param url The <code>URL</code> providing access to the XML document. * * @return A {@link MetaData} providing access to the * raw contents of the XML document. * * @throws IOException If an I/O error occurs accessing the stream or * parsing the XML document. */ public MetaData parse(URL url) throws IOException { this.documentURL = url; InputStream ins = null; try { ins = url.openStream(); this.parser.setProperty("http://xmlpull.org/v1/doc/properties.html#location", url.toString()); MetaData md = parse(ins); if (md != null) { md.setSource(url); } return md; } catch (XmlPullParserException e) { throw new IOException("XML parsing exception while reading metadata: " + e.getMessage()); } finally { if (ins != null) { try { ins.close(); } catch (IOException ioe) { // ignore } } this.documentURL = null; } } /** * Parses the XML document in the given input stream. * <p> * This method starts reading at the current position of the input stream * and returns immediately after completely reading a single meta type * document. The stream is not closed by this method. * * @param ins The <code>InputStream</code> providing the XML document * * @return A {@link MetaData} providing access to the * raw contents of the XML document. * * @throws IOException If an I/O error occurs accessing the stream or * parsing the XML document. */ public MetaData parse(InputStream ins) throws IOException { MetaData mti = null; try { this.parser.setFeature(KXmlParser.FEATURE_PROCESS_NAMESPACES, true); // set the parser input, use null encoding to force detection with <?xml?> this.parser.setInput(ins, null); int eventType = this.parser.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { String tagName = this.parser.getName(); if (eventType == XmlPullParser.START_TAG) { if ("MetaData".equals(tagName)) { mti = readMetaData(); } else { ignoreElement(); } } eventType = this.parser.next(); } } catch (XmlPullParserException e) { throw new IOException("XML parsing exception while reading metadata: " + e.getMessage()); } return mti; } /** * Checks if this document has a meta type name space. * * @throws IOException when there the meta type name space is not valid */ private void checkMetatypeNamespace() throws IOException { final String namespace = this.parser.getNamespace(); if (namespace != null && !"".equals(namespace.trim())) { if (!NAMESPACE_1_0.equals(namespace) && !NAMESPACE_1_1.equals(namespace) && !NAMESPACE_1_2.equals(namespace) && !NAMESPACE_1_3.equals(namespace)) { throw new IOException("Unsupported Namespace: '" + namespace + "'"); } this.namespace = namespace; } } private void readOptionalAttributes(OptionalAttributes entity, Set attributes) { int count = this.parser.getAttributeCount(); for (int i = 0; i < count; i++) { String name = this.parser.getAttributeName(i); if (!attributes.contains(name)) { String value = this.parser.getAttributeValue(i); entity.addOptionalAttribute(name, value); } } } private MetaData readMetaData() throws IOException, XmlPullParserException { checkMetatypeNamespace(); MetaData mti = createMetaData(); mti.setNamespace(this.namespace); mti.setLocalePrefix(getOptionalAttribute("localization")); readOptionalAttributes(mti, METADATA_ATTRIBUTES); int eventType = this.parser.next(); while (eventType != XmlPullParser.END_DOCUMENT) { String tagName = this.parser.getName(); if (eventType == XmlPullParser.START_TAG) { if ("OCD".equals(tagName)) { mti.addObjectClassDefinition(readOCD()); } else if ("Designate".equals(tagName)) { mti.addDesignate(readDesignate()); } else { ignoreElement(); } } else if (eventType == XmlPullParser.END_TAG) { if ("MetaData".equals(tagName)) { break; } throw unexpectedElement(tagName); } eventType = this.parser.next(); } return mti; } private OCD readOCD() throws IOException, XmlPullParserException { OCD ocd = createOCD(); ocd.setId(getRequiredAttribute("id")); ocd.setName(getRequiredAttribute("name")); ocd.setDescription(getOptionalAttribute("description")); readOptionalAttributes(ocd, OCD_ATTRIBUTES); int eventType = this.parser.next(); while (eventType != XmlPullParser.END_DOCUMENT) { String tagName = this.parser.getName(); if (eventType == XmlPullParser.START_TAG) { if ("AD".equals(tagName)) { ocd.addAttributeDefinition(readAD()); } else if ("Icon".equals(tagName)) { String res = getRequiredAttribute("resource"); String sizeString = getRequiredAttribute("size"); try { Integer size = Integer.decode(sizeString); ocd.addIcon(size, res); } catch (NumberFormatException nfe) { Activator.log(LogService.LOG_DEBUG, "readOCD: Icon size '" + sizeString + "' is not a valid number"); } } else { ignoreElement(); } } else if (eventType == XmlPullParser.END_TAG) { if ("OCD".equals(tagName)) { if (getNamespaceVersion() < 12 && ocd.getIcons() != null && ocd.getIcons().size() > 1) { // Only one icon is allowed in versions 1.0 & 1.1... throw unexpectedElement("Icon"); } if (getNamespaceVersion() < 13 && ocd.getAttributeDefinitions() == null) { // Need at least one AD in versions 1.0, 1.1 & 1.2... logMissingElement("AD"); ocd = null; } break; } else if (!"Icon".equals(tagName)) { throw unexpectedElement(tagName); } } eventType = this.parser.next(); } return ocd; } private Designate readDesignate() throws IOException, XmlPullParserException { final String pid = getOptionalAttribute("pid"); final String factoryPid = getOptionalAttribute("factoryPid"); if (pid == null && factoryPid == null) { missingAttribute("pid or factoryPid"); } Designate designate = this.createDesignate(); designate.setPid(pid); designate.setFactoryPid(factoryPid); designate.setBundleLocation(getOptionalAttribute("bundle")); designate.setOptional(getOptionalAttribute("optional", false)); designate.setMerge(getOptionalAttribute("merge", false)); readOptionalAttributes(designate, DESIGNATE_ATTRIBUTES); int eventType = this.parser.next(); while (eventType != XmlPullParser.END_DOCUMENT) { String tagName = this.parser.getName(); if (eventType == XmlPullParser.START_TAG) { if ("Object".equals(tagName)) { if (designate.getObject() != null) { // Only 1 Object is allowed... throw unexpectedElement(tagName); } designate.setObject(readObject()); } else { this.ignoreElement(); } } else if (eventType == XmlPullParser.END_TAG) { if ("Designate".equals(tagName)) { if (designate.getObject() == null) { // Exactly 1 Object is allowed... logMissingElement("Object"); designate = null; } break; } throw unexpectedElement(tagName); } eventType = this.parser.next(); } return designate; } private AD readAD() throws IOException, XmlPullParserException { AD ad = createAD(); ad.setID(getRequiredAttribute("id")); ad.setName(getOptionalAttribute("name")); ad.setDescription(getOptionalAttribute("description")); ad.setType(getRequiredAttribute("type")); ad.setCardinality(getOptionalAttribute("cardinality", 0)); ad.setMin(getOptionalAttribute("min")); ad.setMax(getOptionalAttribute("max")); ad.setRequired(getOptionalAttribute("required", true)); String dfltValue = getOptionalAttribute("default"); readOptionalAttributes(ad, AD_ATTRIBUTES); Map<String, String> options = new LinkedHashMap<String, String>(); int eventType = this.parser.next(); while (eventType != XmlPullParser.END_DOCUMENT) { String tagName = this.parser.getName(); if (eventType == XmlPullParser.START_TAG) { if ("Option".equals(tagName)) { String value = getRequiredAttribute("value"); String label = getRequiredAttribute("label"); options.put(value, label); } else { ignoreElement(); } } else if (eventType == XmlPullParser.END_TAG) { if ("AD".equals(tagName)) { break; } else if (!"Option".equals(tagName)) { throw unexpectedElement(tagName); } } eventType = this.parser.next(); } ad.setOptions(options); // set value as late as possible to force an options check (FELIX-3884, FELIX-4665)... if (dfltValue != null) { ad.setDefaultValue(dfltValue); } return ad; } private DesignateObject readObject() throws IOException, XmlPullParserException { DesignateObject oh = createDesignateObject(); oh.setOcdRef(getRequiredAttribute("ocdref")); readOptionalAttributes(oh, DESIGNATEOBJECT_ATTRIBUTES); int eventType = this.parser.next(); while (eventType != XmlPullParser.END_DOCUMENT) { String tagName = this.parser.getName(); if (eventType == XmlPullParser.START_TAG) { if ("Attribute".equals(tagName)) { oh.addAttribute(readAttribute()); } else { ignoreElement(); } } else if (eventType == XmlPullParser.END_TAG) { if ("Object".equals(tagName)) { break; } throw unexpectedElement(tagName); } eventType = this.parser.next(); } return oh; } private Attribute readAttribute() throws IOException, XmlPullParserException { Attribute ah = createAttribute(); ah.setAdRef(getRequiredAttribute("adref")); ah.addContent(getOptionalAttribute("content"), true); readOptionalAttributes(ah, ATTRIBUTE_ATTRIBUTES); int eventType = this.parser.next(); while (eventType != XmlPullParser.END_DOCUMENT) { String tagName = this.parser.getName(); if (eventType == XmlPullParser.START_TAG) { if ("Value".equals(tagName)) { ah.addContent(this.parser.nextText(), false); eventType = this.parser.getEventType(); } else { ignoreElement(); } } else if (eventType == XmlPullParser.END_TAG) { if ("Attribute".equals(tagName)) { break; } else if (!"Value".equals(tagName)) { throw unexpectedElement(tagName); } } eventType = this.parser.next(); } return ah; } //---------- Attribute access helper -------------------------------------- private String getRequiredAttribute(String attrName) throws XmlPullParserException { String attrVal = this.parser.getAttributeValue(null, attrName); if (attrVal != null) { return attrVal; } // fail if value is missing throw missingAttribute(attrName); } private String getOptionalAttribute(String attrName) { return getOptionalAttribute(attrName, (String) null); } private String getOptionalAttribute(String attrName, String defaultValue) { String attrVal = this.parser.getAttributeValue(null, attrName); return (attrVal != null) ? attrVal : defaultValue; } private boolean getOptionalAttribute(String attrName, boolean defaultValue) { String attrVal = this.parser.getAttributeValue(null, attrName); return (attrVal != null) ? "true".equalsIgnoreCase(attrVal) : defaultValue; } private int getOptionalAttribute(String attrName, int defaultValue) { String attrVal = this.parser.getAttributeValue(null, attrName); if (attrVal != null && !"".equals(attrVal)) { try { return Integer.decode(attrVal).intValue(); } catch (NumberFormatException nfe) { Activator.log(LogService.LOG_DEBUG, "getOptionalAttribute: Value '" + attrVal + "' of attribute " + attrName + " is not a valid number. Using default value " + defaultValue); } } // fallback to default return defaultValue; } private int getNamespaceVersion() { if (NAMESPACE_1_0.equals(this.namespace)) { return 10; } else if (NAMESPACE_1_1.equals(this.namespace)) { return 11; } else if (NAMESPACE_1_2.equals(this.namespace)) { return 12; } else if (NAMESPACE_1_3.equals(this.namespace)) { return 13; } // Undetermined... return Integer.MAX_VALUE; } //---------- Error Handling support --------------------------------------- private void ignoreElement() throws IOException, XmlPullParserException { String ignoredElement = this.parser.getName(); int depth = 0; // enable nested ignored elements int eventType = this.parser.next(); while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { if (ignoredElement.equals(this.parser.getName())) { depth++; } } else if (eventType == XmlPullParser.END_TAG) { if (ignoredElement.equals(this.parser.getName())) { if (depth <= 0) { return; } depth--; } } eventType = this.parser.next(); } } private XmlPullParserException missingAttribute(String attrName) { String message = "Missing attribute " + attrName + " in element " + this.parser.getName(); return new XmlPullParserException(message, this.parser, null); } private void logMissingElement(final String elementName) { String message = "Missing element " + elementName + " in element " + this.parser.getName(); if ( documentURL != null ) { message = message + " : " + this.documentURL; } Activator.log(LogService.LOG_ERROR, message); } private XmlPullParserException unexpectedElement(String elementName) { String message = "Unexpected element " + elementName; return new XmlPullParserException(message, this.parser, null); } //---------- Factory methods ---------------------------------------------- /** * Creates a new {@link MetaData} object to hold the contents of the * <code>MetaData</code> element. * <p> * This method may be overwritten to return a customized extension. */ protected MetaData createMetaData() { return new MetaData(); } /** * Creates a new {@link OCD} object to hold the contents of the * <code>OCD</code> element. * <p> * This method may be overwritten to return a customized extension. */ protected OCD createOCD() { return new OCD(); } /** * Creates a new {@link AD} object to hold the contents of the * <code>AD</code> element. * <p> * This method may be overwritten to return a customized extension. */ protected AD createAD() { return new AD(); } /** * Creates a new {@link DesignateObject} object to hold the contents of the * <code>Object</code> element. * <p> * This method may be overwritten to return a customized extension. */ protected DesignateObject createDesignateObject() { return new DesignateObject(); } /** * Creates a new {@link Attribute} object to hold the contents of the * <code>Attribute</code> element. * <p> * This method may be overwritten to return a customized extension. */ protected Attribute createAttribute() { return new Attribute(); } /** * Creates a new {@link Designate} object to hold the contents of the * <code>Designate</code> element. * <p> * This method may be overwritten to return a customized extension. */ protected Designate createDesignate() { return new Designate(); } }