| /******************************************************************************* |
| * 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 <component> 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 <component> 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 <requirement> 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 <configuration> 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(); |
| } |
| } |