blob: 10be489c40f98dae19524387403e833c8766e1a4 [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.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 fmeschbe
*/
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();
}
}