| /* |
| * 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 Namespace for Metatype 1.4 descriptors. |
| */ |
| static final String NAMESPACE_1_4 = "http://www.osgi.org/xmlns/metatype/v1.4.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<>(Arrays.asList(new String[] { "name", "description", "id", "type", "cardinality", "min", "max", "default", "required" })); |
| private static final Set<String> ATTRIBUTE_ATTRIBUTES = new HashSet<>(Arrays.asList(new String[] { "adref", "content" })); |
| private static final Set<String> DESIGNATE_ATTRIBUTES = new HashSet<>(Arrays.asList(new String[] { "pid", "factoryPid", "bundle", "optional", "merge" })); |
| private static final Set<String> DESIGNATEOBJECT_ATTRIBUTES = new HashSet<>(Arrays.asList(new String[] { "ocdref" })); |
| private static final Set<String> METADATA_ATTRIBUTES = new HashSet<>(Arrays.asList(new String[] { "localization" })); |
| private static final Set<String> OCD_ATTRIBUTES = new HashSet<>(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) |
| && !NAMESPACE_1_4.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<>(); |
| 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; |
| } |
| else if (NAMESPACE_1_4.equals(this.namespace)) |
| { |
| return 14; |
| } |
| // 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(); |
| } |
| } |