blob: 8af23e204f040473cd5ebf129f164bc3b0037a1f [file] [log] [blame]
/*
* 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();
}
}