blob: 26605a943b5347c7189cd190a0ea71596daa1f07 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010-present Sonatype, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Stuart McCulloch (Sonatype, Inc.) - initial API and implementation
*******************************************************************************/
package org.eclipse.sisu.plexus;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Configuration;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.InterpolationFilterReader;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.xml.pull.MXParser;
import org.codehaus.plexus.util.xml.pull.XmlPullParser;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.eclipse.sisu.inject.DeferredClass;
import org.eclipse.sisu.inject.Logs;
import org.eclipse.sisu.space.ClassSpace;
import org.eclipse.sisu.space.Streams;
/**
* Helper class that can scan XML resources for Plexus metadata.
*/
final class PlexusXmlScanner
{
// ----------------------------------------------------------------------
// Implementation fields
// ----------------------------------------------------------------------
private final Map<?, ?> variables;
private final URL plexusXml;
private final Map<String, PlexusBeanMetadata> metadata;
// ----------------------------------------------------------------------
// Constructors
// ----------------------------------------------------------------------
/**
* Creates an XML scanner that also accumulates Plexus bean metadata in the given map.
*
* @param variables The filter variables
* @param plexusXml The plexus.xml URL
* @param metadata The metadata map
*/
PlexusXmlScanner( final Map<?, ?> variables, final URL plexusXml, final Map<String, PlexusBeanMetadata> metadata )
{
this.variables = variables;
this.plexusXml = plexusXml;
this.metadata = metadata;
}
// ----------------------------------------------------------------------
// Locally-shared methods
// ----------------------------------------------------------------------
Map<Component, DeferredClass<?>> scan( final ClassSpace space, final boolean root )
{
final PlexusTypeRegistry registry = new PlexusTypeRegistry( space );
if ( null != plexusXml )
{
parsePlexusXml( plexusXml, registry );
}
final Enumeration<URL> e;
if ( root )
{
e = space.getResources( "META-INF/plexus/components.xml" );
}
else
{
e = space.findEntries( "META-INF/plexus", "components.xml", false );
}
while ( e.hasMoreElements() )
{
parseComponentsXml( e.nextElement(), registry );
}
return registry.getComponents();
}
// ----------------------------------------------------------------------
// Implementation methods
// ----------------------------------------------------------------------
/**
* Wraps the given {@link InputStream} as a {@link Reader} with XML encoding detection and optional interpolation.
*
* @param in The input stream
* @param variables The filter variables
* @return Reader that can automatically detect XML encodings and optionally interpolate variables
*/
private static Reader filteredXmlReader( final InputStream in, @SuppressWarnings( "rawtypes" ) final Map variables )
throws IOException
{
final Reader reader = ReaderFactory.newXmlReader( in );
if ( null != variables )
{
return new InterpolationFilterReader( reader, variables );
}
return reader;
}
/**
* Parses a {@code plexus.xml} resource into load-on-start settings and Plexus bean metadata.
*
* @param url The plexus.xml URL
* @param registry The parsed components
*/
private void parsePlexusXml( final URL url, final PlexusTypeRegistry registry )
{
try
{
final InputStream in = Streams.open( url );
try
{
final MXParser parser = new MXParser();
parser.setInput( filteredXmlReader( in, variables ) );
parser.nextTag();
parser.require( XmlPullParser.START_TAG, null, null ); // this may be <component-set> or <plexus>
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
final String name = parser.getName();
if ( Strategies.LOAD_ON_START.equals( name ) )
{
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
parseLoadOnStart( parser, registry );
}
}
else if ( "components".equals( name ) )
{
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
parseComponent( parser, registry );
}
}
else
{
parser.skipSubTree();
}
}
}
finally
{
IOUtil.close( in );
}
}
catch ( final Exception e )
{
Logs.trace( "Problem parsing: {}", url, e );
}
}
/**
* Parses a {@code components.xml} resource into a series of Plexus bean metadata.
*
* @param url The components.xml URL
* @param registry The parsed components
*/
private void parseComponentsXml( final URL url, final PlexusTypeRegistry registry )
{
try
{
final InputStream in = Streams.open( url );
try
{
final MXParser parser = new MXParser();
parser.setInput( filteredXmlReader( in, variables ) );
parser.nextTag();
parser.require( XmlPullParser.START_TAG, null, null ); // this may be <component-set> or <plexus>
parser.nextTag();
parser.require( XmlPullParser.START_TAG, null, "components" );
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
parseComponent( parser, registry );
}
}
finally
{
IOUtil.close( in );
}
}
catch ( final Exception e )
{
Logs.trace( "Problem parsing: {}", url, e );
}
}
/**
* Parses a load-on-start &lt;component&gt; XML stanza into a Plexus role-hint.
*
* @param parser The XML parser
* @param registry The parsed components
*/
private static void parseLoadOnStart( final MXParser parser, final PlexusTypeRegistry registry )
throws XmlPullParserException, IOException
{
if ( "component".equals( parser.getName() ) )
{
String role = null;
String hint = "";
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
if ( "role".equals( parser.getName() ) )
{
role = TEXT( parser );
}
else if ( "role-hint".equals( parser.getName() ) )
{
hint = TEXT( parser );
}
else
{
parser.skipSubTree();
}
}
if ( null == role )
{
throw new XmlPullParserException( "Missing <role> element.", parser, null );
}
registry.loadOnStart( role, hint );
}
else
{
parser.skipSubTree();
}
}
/**
* Parses a &lt;component&gt; XML stanza into a deferred implementation, configuration, and requirements.
*
* @param parser The XML parser
* @param registry The parsed components
*/
private void parseComponent( final MXParser parser, final PlexusTypeRegistry registry )
throws XmlPullParserException, IOException
{
String role = null;
String hint = "";
String instantiationStrategy = Strategies.SINGLETON;
String description = "";
String implementation = null;
final Map<String, Requirement> requirementMap = new HashMap<String, Requirement>();
final Map<String, Configuration> configurationMap = new HashMap<String, Configuration>();
final ClassSpace space = registry.getSpace();
parser.require( XmlPullParser.START_TAG, null, "component" );
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
final String name = parser.getName();
if ( "requirements".equals( name ) )
{
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
parseRequirement( parser, space, requirementMap );
}
}
else if ( "configuration".equals( name ) )
{
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
parseConfiguration( parser, configurationMap );
}
}
else if ( "role".equals( name ) )
{
role = TEXT( parser ).intern();
}
else if ( "role-hint".equals( name ) )
{
hint = TEXT( parser );
}
else if ( "instantiation-strategy".equals( name ) )
{
instantiationStrategy = TEXT( parser ).intern();
}
else if ( "description".equals( name ) )
{
description = TEXT( parser );
}
else if ( "implementation".equals( name ) )
{
implementation = TEXT( parser ).intern();
}
else
{
parser.skipSubTree();
}
}
if ( null == implementation )
{
throw new XmlPullParserException( "Missing <implementation> element.", parser, null );
}
if ( null == role )
{
role = implementation;
}
implementation = registry.addComponent( role, hint, instantiationStrategy, description, implementation );
if ( null != implementation )
{
updatePlexusBeanMetadata( implementation, configurationMap, requirementMap );
}
}
/**
* Updates the shared Plexus bean metadata with the given local information.
*
* @param implementation The component implementation
* @param configurationMap The field -> @{@link Configuration} map
* @param requirementMap The field -> @{@link Requirement} map
*/
private void updatePlexusBeanMetadata( final String implementation,
final Map<String, Configuration> configurationMap,
final Map<String, Requirement> requirementMap )
{
if ( null != metadata && ( !configurationMap.isEmpty() || !requirementMap.isEmpty() ) )
{
final PlexusXmlMetadata beanMetadata = (PlexusXmlMetadata) metadata.get( implementation );
if ( beanMetadata != null )
{
beanMetadata.merge( configurationMap, requirementMap );
}
else
{
metadata.put( implementation, new PlexusXmlMetadata( configurationMap, requirementMap ) );
}
}
}
/**
* Parses a &lt;requirement&gt; XML stanza into a mapping from a field name to a @{@link Requirement}.
*
* @param parser The XML parser
* @param space The class space
* @param requirementMap The field -> @{@link Requirement} map
*/
private static void parseRequirement( final MXParser parser, final ClassSpace space,
final Map<String, Requirement> requirementMap )
throws XmlPullParserException, IOException
{
String role = null;
final List<String> hintList = new ArrayList<String>();
String fieldName = null;
boolean optional = false;
parser.require( XmlPullParser.START_TAG, null, "requirement" );
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
final String name = parser.getName();
if ( "role".equals( name ) )
{
role = TEXT( parser ).intern();
}
else if ( "role-hint".equals( name ) )
{
hintList.add( TEXT( parser ) );
}
else if ( "role-hints".equals( name ) )
{
while ( parser.nextTag() == XmlPullParser.START_TAG )
{
hintList.add( TEXT( parser ) );
}
}
else if ( "field-name".equals( name ) )
{
fieldName = TEXT( parser );
}
else if ( "optional".equals( name ) )
{
optional = Boolean.parseBoolean( TEXT( parser ) );
}
else
{
parser.skipSubTree();
}
}
if ( null == role )
{
throw new XmlPullParserException( "Missing <role> element.", parser, null );
}
if ( null == fieldName )
{
fieldName = role; // use fully-qualified role as the field name (see PlexusXmlMetadata)
}
requirementMap.put( fieldName, new RequirementImpl( space.deferLoadClass( role ), optional,
Hints.canonicalHints( hintList ) ) );
}
/**
* Parses a &lt;configuration&gt; XML stanza into a mapping from a field name to a @{@link Configuration}.
*
* @param parser The XML parser
* @param configurationMap The field -> @{@link Configuration} map
*/
private static void parseConfiguration( final MXParser parser, final Map<String, Configuration> configurationMap )
throws XmlPullParserException, IOException
{
final String name = parser.getName();
// make sure we have a valid Java identifier
final String fieldName = Roles.camelizeName( name );
final StringBuilder buf = new StringBuilder();
final String header = parser.getText().trim();
final int depth = parser.getDepth();
while ( parser.next() != XmlPullParser.END_TAG || parser.getDepth() > depth )
{
// combine children into single string
buf.append( parser.getText().trim() );
}
// add header+footer when there's nested XML or attributes
if ( buf.indexOf( "<" ) == 0 || header.indexOf( '=' ) > 0 )
{
buf.insert( 0, header );
if ( !header.endsWith( "/>" ) )
{
// follow up with basic footer
buf.append( "</" + name + '>' );
}
}
configurationMap.put( fieldName, new ConfigurationImpl( fieldName, buf.toString() ) );
}
/**
* Returns the text contained inside the current XML element, without any surrounding whitespace.
*
* @param parser The XML parser
* @return Trimmed TEXT element
*/
private static String TEXT( final XmlPullParser parser )
throws XmlPullParserException, IOException
{
return parser.nextText().trim();
}
}