blob: 4fd2f487ff69c728673c63b5ff92373a07a310bb [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.scrplugin;
import java.io.*;
import java.util.*;
import java.util.jar.*;
import org.apache.felix.scrplugin.om.Component;
import org.apache.felix.scrplugin.om.Components;
import org.apache.felix.scrplugin.tags.JavaClassDescription;
import org.apache.felix.scrplugin.tags.JavaTag;
import org.apache.felix.scrplugin.tags.annotation.AnnotationJavaClassDescription;
import org.apache.felix.scrplugin.tags.annotation.AnnotationTagProviderManager;
import org.apache.felix.scrplugin.tags.cl.ClassLoaderJavaClassDescription;
import org.apache.felix.scrplugin.tags.qdox.QDoxJavaClassDescription;
import org.apache.felix.scrplugin.xml.ComponentDescriptorIO;
import com.thoughtworks.qdox.JavaDocBuilder;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaSource;
/**
* The <code>JavaClassDescriptorManager</code> must be implemented to provide
* access to the java sources to be scanned for descriptor annotations and
* JavaDoc tags, the descriptors of components from the class path and the
* location of the generated class files to be able to add the bind and unbind
* methods.
*/
public abstract class JavaClassDescriptorManager
{
/** The maven log. */
protected final Log log;
/** The classloader used to compile the classes. */
private final ClassLoader classloader;
/** A cache containing the java class descriptions hashed by classname. */
private final Map<String, JavaClassDescription> javaClassDescriptions = new HashMap<String, JavaClassDescription>();
/**
* Supports mapping of built-in and custom java anntoations to
* {@link JavaTag} implementations.
*/
private final AnnotationTagProviderManager annotationTagProviderManager;
/** Parse Javadocs? */
private final boolean parseJavadocs;
/** Process Annotations? */
private final boolean processAnnotations;
/** The Java sources gathered by {@link #getSourceDescriptions()} */
private JavaSource[] sources;
/** The component definitions from other bundles hashed by classname. */
private Map<String, Component> componentDescriptions;
/**
* Construct a new manager.
*
* @throws SCRDescriptorFailureException
*/
public JavaClassDescriptorManager( final Log log, final ClassLoader classLoader,
final String[] annotationTagProviders, final boolean parseJavadocs, final boolean processAnnotations )
throws SCRDescriptorFailureException
{
this.processAnnotations = processAnnotations;
this.parseJavadocs = parseJavadocs;
this.log = log;
this.annotationTagProviderManager = new AnnotationTagProviderManager( annotationTagProviders, classLoader );
this.classloader = classLoader;
}
/**
* Returns the QDox JavaSource instances representing the source files for
* which the Declarative Services and Metatype descriptors have to be
* generated.
*
* @throws SCRDescriptorException May be thrown if an error occurrs
* gathering the java sources.
*/
protected JavaSource[] getSources() throws SCRDescriptorException
{
if ( this.sources == null )
{
this.log.debug( "Setting up QDox" );
JavaDocBuilder builder = new JavaDocBuilder();
builder.getClassLibrary().addClassLoader( this.getClassLoader() );
final Iterator<File> i = getSourceFiles();
while ( i.hasNext() )
{
File file = i.next();
this.log.debug( "Adding source file " + file );
try
{
builder.addSource( file );
}
catch ( IOException e )
{
// also FileNotFoundException
throw new SCRDescriptorException( "Unable to add source file", file.toString(), 0, e );
}
}
this.sources = builder.getSources();
}
return this.sources;
}
/**
* Returns an iterator of paths to directories providing Java source files
* to be parsed.
* <p>
* This method is called by the default {@link #getSources()} implementation
* to return the root directories for the Java files to be parsed. This
* default implementation returns an empty iterator. Implementations of this
* class not overwriting the {@link #getSources()} method should overwrite
* this method by providing the concrete source locations.
*
* @return An iterator of Java source locations.
*/
protected Iterator<File> getSourceFiles()
{
return Collections.<File> emptyList().iterator();
}
/**
* Returns a map of component descriptors which may be extended by the java
* sources returned by the {@link #getSources()} method.
* <p>
* This method calls the {@link #getDependencies()} method and checks for
* any Service-Component descriptors in the returned files.
* <p>
* This method may be overwritten by extensions of this class.
*
* @throws SCRDescriptorException May be thrown if an error occurrs
* gethering the component descriptors.
*/
protected Map<String, Component> getComponentDescriptors() throws SCRDescriptorException
{
if ( this.componentDescriptions == null )
{
final List<Component> components = new ArrayList<Component>();
final List<File> dependencies = getDependencies();
for ( File artifact : dependencies )
{
this.log.debug( "Trying to get manifest from artifact " + artifact );
try
{
final Manifest manifest = this.getManifest( artifact );
if ( manifest != null )
{
// read Service-Component entry
if ( manifest.getMainAttributes().getValue( Constants.SERVICE_COMPONENT ) != null )
{
final String serviceComponent = manifest.getMainAttributes().getValue(
Constants.SERVICE_COMPONENT );
this.log
.debug( "Found Service-Component: " + serviceComponent + " in artifact " + artifact );
final StringTokenizer st = new StringTokenizer( serviceComponent, "," );
while ( st.hasMoreTokens() )
{
final String entry = st.nextToken().trim();
if ( entry.length() > 0 )
{
final Components c = this.readServiceComponentDescriptor( artifact, entry );
if ( c != null )
{
components.addAll( c.getComponents() );
}
}
}
}
else
{
this.log.debug( "Artifact has no service component entry in manifest " + artifact );
}
}
else
{
this.log.debug( "Unable to get manifest from artifact " + artifact );
}
}
catch ( IOException ioe )
{
throw new SCRDescriptorException( "Unable to get manifest from artifact", artifact.toString(), 0,
ioe );
}
this.log.debug( "Trying to get scrinfo from artifact " + artifact );
// now read the scr private file - components stored there
// overwrite components already
// read from the service component section.
InputStream scrInfoFile = null;
try
{
scrInfoFile = this.getFile( artifact, Constants.ABSTRACT_DESCRIPTOR_ARCHIV_PATH );
if ( scrInfoFile != null )
{
components.addAll( this.parseServiceComponentDescriptor( scrInfoFile ).getComponents() );
}
else
{
this.log.debug( "Artifact has no scrinfo file (it's optional): " + artifact );
}
}
catch ( IOException ioe )
{
throw new SCRDescriptorException( "Unable to get scrinfo from artifact", artifact.toString(), 0,
ioe );
}
finally
{
if ( scrInfoFile != null )
{
try
{
scrInfoFile.close();
}
catch ( IOException ignore )
{
}
}
}
}
// now create map with component descriptions
this.componentDescriptions = new HashMap<String, Component>();
for ( final Component component : components )
{
this.componentDescriptions.put( component.getImplementation().getClassame(), component );
}
}
return this.componentDescriptions;
}
/**
* Returns a list of files denoting dependencies of the module for which
* descriptors are to be generated. The returned dependencies are expected
* to be bundles which may (or may not) contain Service Component
* descriptors (or internal descriptors in the case of abstract components
* not listed in the "official" descriptors).
* <p>
* This method is called by the {@link #getComponentDescriptors()} method in
* this class to get the list of bundles from where base component
* descriptors are to be extracted.
* <p>
* Extensions of this class not overwriting the
* {@link #getComponentDescriptors()} method should overwrite this method if
* they wish to provide such base component descriptors.
*
* @return
*/
protected List<File> getDependencies()
{
return Collections.<File> emptyList();
}
/**
* Returns the absolute filesystem path to the directory where the classes
* compiled from the java source files (see {@link #getSources()}) have been
* placed.
* <p>
* This method is called to find the class files to which bind and unbind
* methods are to be added.
*/
public abstract String getOutputDirectory();
/**
* Return the log.
*/
public Log getLog()
{
return this.log;
}
/**
* Return the class laoder.
*/
public ClassLoader getClassLoader()
{
return this.classloader;
}
/**
* @return Annotation tag provider manager
*/
public AnnotationTagProviderManager getAnnotationTagProviderManager()
{
return this.annotationTagProviderManager;
}
/**
* Returns <code>true</code> if this class descriptor manager is parsing
* JavaDoc tags.
*/
public boolean isParseJavadocs()
{
return parseJavadocs;
}
/**
* Returns <code>true</code> if this class descriptor manager is parsing
* Java 5 annotations.
*/
public boolean isProcessAnnotations()
{
return processAnnotations;
}
/**
* Parses the descriptors read from the given input stream. This method may
* be called by the {@link #getComponentDescriptors()} method to parse the
* descriptors gathered in an implementation dependent way.
*
* @throws SCRDescriptorException If an error occurrs reading the
* descriptors from the stream.
*/
protected Components parseServiceComponentDescriptor( InputStream file ) throws SCRDescriptorException
{
return ComponentDescriptorIO.read( file );
}
/**
* Return all source descriptions of this project.
*
* @return All contained java class descriptions.
*/
public JavaClassDescription[] getSourceDescriptions() throws SCRDescriptorException
{
final JavaClass[] javaClasses = getJavaClassesFromSources();
final JavaClassDescription[] descs = new JavaClassDescription[javaClasses.length];
for ( int i = 0; i < javaClasses.length; i++ )
{
descs[i] = this.getJavaClassDescription( javaClasses[i].getFullyQualifiedName() );
}
return descs;
}
private boolean doingHasScrPluginAnnotationCheck = false;
private boolean hasScrPluginAnnotation(final Class<?> clazz, final JavaClass javaClass)
{
boolean result;
doingHasScrPluginAnnotationCheck = true;
result = getAnnotationTagProviderManager().hasScrPluginAnnotation( javaClass,
new AnnotationJavaClassDescription( clazz, javaClass, this ));
doingHasScrPluginAnnotationCheck = false;
return result;
}
/**
* Get a java class description for the class.
*
* @param className
* @return The java class description.
* @throws SCRDescriptorException
*/
public JavaClassDescription getJavaClassDescription( String className ) throws SCRDescriptorException
{
JavaClassDescription result = this.javaClassDescriptions.get( className );
if ( result == null )
{
this.log.debug( "Searching description for: " + className );
int index = 0;
final JavaClass[] javaClasses = getJavaClassesFromSources();
while ( result == null && index < javaClasses.length )
{
final JavaClass javaClass = javaClasses[index];
if ( javaClass.getFullyQualifiedName().equals( className ) )
{
try
{
// check for java annotation descriptions - fallback to
// QDox if none found
Class<?> clazz = this.classloader.loadClass( className );
if ( this.processAnnotations && !doingHasScrPluginAnnotationCheck
&& hasScrPluginAnnotation(clazz, javaClass) )
{
this.log.debug( "Generating java annotation description for: " + className );
result = new AnnotationJavaClassDescription( clazz, javaClass, this );
}
else if ( this.parseJavadocs )
{
this.log.debug( "Generating qdox description for: " + className );
result = new QDoxJavaClassDescription( clazz, javaClass, this );
}
else
{
index++;
}
}
catch ( ClassNotFoundException e )
{
throw new SCRDescriptorException( "Unable to load class", className, 0 );
}
}
else
{
index++;
}
}
if ( result == null )
{
try
{
this.log.debug( "Generating classloader description for: " + className );
result = new ClassLoaderJavaClassDescription( this.classloader.loadClass( className ), this
.getComponentDescriptors().get( className ), this );
}
catch ( ClassNotFoundException e )
{
throw new SCRDescriptorException( "Unable to load class", className, 0 );
}
}
if ( !doingHasScrPluginAnnotationCheck ) {
this.javaClassDescriptions.put( className, result );
}
}
return result;
}
/**
* Get a list of all {@link JavaClass} definitions four all source files
* (including nested/inner classes)
*
* @return List of {@link JavaClass} definitions
*/
private JavaClass[] getJavaClassesFromSources() throws SCRDescriptorException
{
final JavaSource[] sources = this.getSources();
final List<JavaClass> classes = new ArrayList<JavaClass>();
for ( int i = 0; i < sources.length; i++ )
{
if ( sources[i].getClasses() == null || sources[i].getClasses().length == 0 )
{
continue;
}
for ( int j = 0; j < sources[i].getClasses().length; j++ )
{
final JavaClass clazz = sources[i].getClasses()[j];
classes.add( clazz );
for ( int k = 0; k < clazz.getNestedClasses().length; k++ )
{
final JavaClass nestedClass = clazz.getNestedClasses()[k];
classes.add( nestedClass );
}
}
}
return classes.toArray( new JavaClass[classes.size()] );
}
/**
* Read the service component description.
*
* @param artifact
* @param entry
* @throws IOException
* @throws SCRDescriptorException
*/
private Components readServiceComponentDescriptor( final File artifactFile, String entry )
{
this.log.debug( "Reading " + entry + " from " + artifactFile );
InputStream xml = null;
try
{
xml = this.getFile( artifactFile, entry );
if ( xml == null )
{
throw new SCRDescriptorException( "Entry " + entry + " not contained in JAR File ", artifactFile.toString(),
0 );
}
return this.parseServiceComponentDescriptor( xml );
}
catch ( IOException mee )
{
this.log.warn( "Unable to read SCR descriptor file from JAR File " + artifactFile + " at " + entry );
this.log.debug( "Exception occurred during reading: " + mee.getMessage(), mee );
}
catch ( SCRDescriptorException mee )
{
this.log.warn( "Unable to read SCR descriptor file from JAR File " + artifactFile + " at " + entry );
this.log.debug( "Exception occurred during reading: " + mee.getMessage(), mee );
}
finally
{
if ( xml != null )
{
try
{
xml.close();
}
catch ( IOException ignore )
{
}
}
}
return null;
}
private Manifest getManifest( File artifact ) throws IOException
{
if ( artifact.isDirectory() )
{
// this is maybe a classes directory, try to read manifest file directly
final File dir = new File(artifact, "META-INF");
if ( !dir.exists() || !dir.isDirectory() )
{
return null;
}
final File mf = new File(dir, "MANIFEST.MF");
if ( !mf.exists() || !mf.isFile() )
{
return null;
}
final InputStream is = new FileInputStream(mf);
try
{
return new Manifest(is);
}
finally
{
try
{
is.close();
}
catch (final IOException ignore) { }
}
}
JarFile file = null;
try
{
file = new JarFile( artifact );
return file.getManifest();
}
finally
{
if ( file != null )
{
try
{
file.close();
}
catch ( IOException ignore )
{
}
}
}
}
private InputStream getFile( final File artifactFile, final String path ) throws IOException
{
if ( artifactFile.isDirectory() )
{
final String filePath = path.replace('/', File.separatorChar).replace('\\', File.separatorChar);
final File file = new File(artifactFile, filePath);
if ( file.exists() && file.isFile() )
{
return new FileInputStream(file);
}
return null;
}
JarFile file = null;
try
{
file = new JarFile( artifactFile );
final JarEntry entry = file.getJarEntry( path );
if ( entry != null )
{
final InputStream stream = new ArtifactFileInputStream( file, entry );
file = null; // prevent file from being closed now
return stream;
}
return null;
}
finally
{
if ( file != null )
{
try
{
file.close();
}
catch ( IOException ignore )
{
}
}
}
}
private static class ArtifactFileInputStream extends FilterInputStream
{
final JarFile jarFile;
ArtifactFileInputStream( JarFile jarFile, JarEntry jarEntry ) throws IOException
{
super( jarFile.getInputStream( jarEntry ) );
this.jarFile = jarFile;
}
@Override
public void close() throws IOException
{
try
{
super.close();
}
catch ( IOException ioe )
{
}
jarFile.close();
}
@Override
protected void finalize() throws Throwable
{
try
{
close();
}
finally
{
super.finalize();
}
}
}
}