| /* |
| * 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.LinkedHashMap; |
| import java.util.Map; |
| |
| 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 XML parser used to read the XML documents */ |
| private KXmlParser parser = new KXmlParser(); |
| |
| |
| /** |
| * 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 occurrs accessing the stream. |
| * @throws XmlPullParserException If an error occurrs parsing the XML |
| * document. |
| */ |
| public MetaData parse( URL url ) throws IOException, XmlPullParserException |
| { |
| InputStream ins = null; |
| try |
| { |
| ins = url.openStream(); |
| this.parser.setProperty( "http://xmlpull.org/v1/doc/properties.html#location", url.toString() ); |
| this.parser.setFeature(KXmlParser.FEATURE_PROCESS_NAMESPACES, true); |
| return this.parse( ins ); |
| } |
| finally |
| { |
| if ( ins != null ) |
| { |
| try |
| { |
| ins.close(); |
| } |
| catch ( IOException ioe ) |
| { |
| // ignore |
| } |
| } |
| } |
| } |
| |
| |
| /** |
| * 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 occurrs accessing the stream. |
| * @throws XmlPullParserException If an error occurrs parsing the XML |
| * document. |
| */ |
| public MetaData parse( InputStream ins ) throws IOException, XmlPullParserException |
| { |
| // set the parser input, use null encoding to force detection with <?xml?> |
| this.parser.setInput( ins, null ); |
| |
| MetaData mti = null; |
| |
| int eventType = this.parser.getEventType(); |
| while ( eventType != XmlPullParser.END_DOCUMENT ) |
| { |
| if ( eventType == XmlPullParser.START_TAG ) |
| { |
| if ( "MetaData".equals( this.parser.getName() ) ) |
| { |
| mti = this.readMetaData(); |
| } |
| else |
| { |
| this.ignoreElement(); |
| } |
| } |
| eventType = this.parser.next(); |
| } |
| |
| return mti; |
| } |
| |
| |
| private MetaData readMetaData() throws IOException, XmlPullParserException |
| { |
| MetaData mti = this.createMetaData(); |
| mti.setLocalePrefix( this.getOptionalAttribute( "localization" ) ); |
| |
| int eventType = this.parser.next(); |
| while ( eventType != XmlPullParser.END_DOCUMENT ) |
| { |
| if ( eventType == XmlPullParser.START_TAG ) |
| { |
| if ( "OCD".equals( this.parser.getName() ) ) |
| { |
| mti.addObjectClassDefinition( this.readOCD() ); |
| } |
| else if ( "Designate".equals( this.parser.getName() ) ) |
| { |
| mti.addDesignate( this.readDesignate() ); |
| } |
| else |
| { |
| this.ignoreElement(); |
| } |
| } |
| else if ( eventType == XmlPullParser.END_TAG ) |
| { |
| if ( "MetaData".equals( this.parser.getName() ) ) |
| { |
| break; |
| } |
| |
| throw this.unexpectedElement(); |
| } |
| eventType = this.parser.next(); |
| } |
| |
| return mti; |
| } |
| |
| |
| private OCD readOCD() throws IOException, XmlPullParserException |
| { |
| OCD ocd = this.createOCD(); |
| ocd.setId( this.getRequiredAttribute( "id" ) ); |
| ocd.setName( this.getRequiredAttribute( "name" ) ); |
| ocd.setDescription( this.getOptionalAttribute( "description" ) ); |
| |
| int eventType = this.parser.next(); |
| while ( eventType != XmlPullParser.END_DOCUMENT ) |
| { |
| if ( eventType == XmlPullParser.START_TAG ) |
| { |
| if ( "AD".equals( this.parser.getName() ) ) |
| { |
| ocd.addAttributeDefinition( this.readAD() ); |
| } |
| else if ( "Icon".equals( this.parser.getName() ) ) |
| { |
| String res = this.getRequiredAttribute( "resource" ); |
| String sizeString = this.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 |
| { |
| this.ignoreElement(); |
| } |
| } |
| else if ( eventType == XmlPullParser.END_TAG ) |
| { |
| if ( "OCD".equals( this.parser.getName() ) ) |
| { |
| break; |
| } |
| else if ( !"Icon".equals( this.parser.getName() ) ) |
| { |
| throw this.unexpectedElement(); |
| } |
| } |
| eventType = this.parser.next(); |
| } |
| |
| return ocd; |
| } |
| |
| |
| private Designate readDesignate() throws IOException, XmlPullParserException |
| { |
| Designate designate = this.createDesignate(); |
| designate.setPid( this.getRequiredAttribute( "pid" ) ); |
| designate.setFactoryPid( this.getOptionalAttribute( "factoryPid" ) ); |
| designate.setBundleLocation( this.getOptionalAttribute( "bundle" ) ); |
| designate.setOptional( this.getOptionalAttribute( "optional", false ) ); |
| designate.setMerge( this.getOptionalAttribute( "merge", false ) ); |
| |
| int eventType = this.parser.next(); |
| while ( eventType != XmlPullParser.END_DOCUMENT ) |
| { |
| if ( eventType == XmlPullParser.START_TAG ) |
| { |
| if ( "Object".equals( this.parser.getName() ) ) |
| { |
| designate.setObject( this.readObject() ); |
| } |
| else |
| { |
| this.ignoreElement(); |
| } |
| } |
| else if ( eventType == XmlPullParser.END_TAG ) |
| { |
| if ( "Designate".equals( this.parser.getName() ) ) |
| { |
| break; |
| } |
| |
| throw this.unexpectedElement(); |
| } |
| eventType = this.parser.next(); |
| } |
| |
| return designate; |
| } |
| |
| |
| private AD readAD() throws IOException, XmlPullParserException |
| { |
| AD ad = this.createAD(); |
| ad.setID( this.getRequiredAttribute( "id" ) ); |
| ad.setName( this.getOptionalAttribute( "name" ) ); |
| ad.setDescription( this.getOptionalAttribute( "description" ) ); |
| ad.setType( this.getRequiredAttribute( "type" ) ); |
| ad.setCardinality( this.getOptionalAttribute( "cardinality", 0 ) ); |
| ad.setMin( this.getOptionalAttribute( "min" ) ); |
| ad.setMax( this.getOptionalAttribute( "max" ) ); |
| ad.setDefaultValue( this.getOptionalAttribute( "default" ) ); |
| ad.setRequired( this.getOptionalAttribute( "required", true ) ); |
| |
| Map options = new LinkedHashMap(); |
| int eventType = this.parser.next(); |
| while ( eventType != XmlPullParser.END_DOCUMENT ) |
| { |
| if ( eventType == XmlPullParser.START_TAG ) |
| { |
| if ( "Option".equals( this.parser.getName() ) ) |
| { |
| String value = this.getRequiredAttribute( "value" ); |
| String label = this.getRequiredAttribute( "label" ); |
| options.put( value, label ); |
| } |
| else |
| { |
| this.ignoreElement(); |
| } |
| } |
| else if ( eventType == XmlPullParser.END_TAG ) |
| { |
| if ( "AD".equals( this.parser.getName() ) ) |
| { |
| break; |
| } |
| else if ( !"Option".equals( this.parser.getName() ) ) |
| { |
| throw this.unexpectedElement(); |
| } |
| } |
| eventType = this.parser.next(); |
| } |
| |
| ad.setOptions( options ); |
| |
| return ad; |
| } |
| |
| |
| private DesignateObject readObject() throws IOException, XmlPullParserException |
| { |
| DesignateObject oh = this.createDesignateObject(); |
| oh.setOcdRef( this.getRequiredAttribute( "ocdref" ) ); |
| |
| int eventType = this.parser.next(); |
| while ( eventType != XmlPullParser.END_DOCUMENT ) |
| { |
| if ( eventType == XmlPullParser.START_TAG ) |
| { |
| if ( "Attribute".equals( this.parser.getName() ) ) |
| { |
| oh.addAttribute( this.readAttribute() ); |
| } |
| else |
| { |
| this.ignoreElement(); |
| } |
| } |
| else if ( eventType == XmlPullParser.END_TAG ) |
| { |
| if ( "Object".equals( this.parser.getName() ) ) |
| { |
| break; |
| } |
| throw this.unexpectedElement(); |
| } |
| eventType = this.parser.next(); |
| } |
| |
| return oh; |
| } |
| |
| |
| private Attribute readAttribute() throws IOException, XmlPullParserException |
| { |
| Attribute ah = this.createAttribute(); |
| ah.setAdRef( this.getRequiredAttribute( "adref" ) ); |
| ah.addContent( this.getOptionalAttribute( "content" ), true ); |
| |
| int eventType = this.parser.next(); |
| while ( eventType != XmlPullParser.END_DOCUMENT ) |
| { |
| if ( eventType == XmlPullParser.START_TAG ) |
| { |
| if ( "Value".equals( this.parser.getName() ) ) |
| { |
| ah.addContent( this.parser.nextText(), false ); |
| eventType = this.parser.getEventType(); |
| continue; |
| } |
| this.ignoreElement(); |
| } |
| else if ( eventType == XmlPullParser.END_TAG ) |
| { |
| if ( "Attribute".equals( this.parser.getName() ) ) |
| { |
| break; |
| } |
| else if ( !"Value".equals( this.parser.getName() ) ) |
| { |
| throw this.unexpectedElement(); |
| } |
| } |
| 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 this.missingAttribute( attrName ); |
| } |
| |
| |
| private String getOptionalAttribute( String attrName ) |
| { |
| return this.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 && attrVal.length() > 0 ) |
| { |
| 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; |
| } |
| |
| |
| //---------- 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 XmlPullParserException unexpectedElement() |
| { |
| String message = "Illegal Element " + this.parser.getName(); |
| 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(); |
| } |
| } |