ProjectClassLoader.java
/*
* Copyright (C) Christian Schulte <cs@schulte.it>, 2005-206
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* o Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* $JOMC: ProjectClassLoader.java 5043 2015-05-27 07:03:39Z schulte $
*
*/
package org.jomc.ant;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.Path;
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
import org.jomc.modlet.ModelContext;
import org.jomc.modlet.ModelContextFactory;
import org.jomc.modlet.ModelException;
import org.jomc.modlet.Modlet;
import org.jomc.modlet.ModletObject;
import org.jomc.modlet.Modlets;
import org.jomc.modlet.ObjectFactory;
import org.jomc.modlet.Schema;
import org.jomc.modlet.Schemas;
import org.jomc.modlet.Service;
import org.jomc.modlet.Services;
import org.jomc.util.ParseException;
import org.jomc.util.TokenMgrError;
import org.jomc.util.VersionParser;
/**
* Class loader supporting JOMC resources backed by a project.
*
* @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
* @version $JOMC: ProjectClassLoader.java 5043 2015-05-27 07:03:39Z schulte $
*/
public class ProjectClassLoader extends URLClassLoader
{
/**
* Constant to prefix relative resource names with.
*/
private static final String ABSOLUTE_RESOURCE_NAME_PREFIX = "/org/jomc/ant/";
/**
* Empty URL array.
*/
private static final URL[] NO_URLS =
{
};
/**
* Set of modlet names to exclude.
*/
private Set<String> modletExcludes;
/**
* Excluded modlets.
*/
private Modlets excludedModlets;
/**
* Set of service class names to exclude.
*/
private Set<String> serviceExcludes;
/**
* Excluded services.
*/
private Services excludedServices;
/**
* Set of schema public ids to exclude.
*/
private Set<String> schemaExcludes;
/**
* Excluded schemas.
*/
private Schemas excludedSchemas;
/**
* Set of providers to exclude.
*/
private Set<String> providerExcludes;
/**
* Set of excluded providers.
*/
private Set<String> excludedProviders;
/**
* The project the class loader is associated with.
*/
private final Project project;
/**
* Set of modlet resource locations to filter.
*/
private Set<String> modletResourceLocations;
/**
* Set of provider resource locations to filter.
*/
private Set<String> providerResourceLocations;
/**
* Set of temporary resources.
*/
private final Set<File> temporaryResources = new HashSet<File>();
/**
* Creates a new {@code ProjectClassLoader} instance taking a project and a class path.
*
* @param project The project to which this class loader is to belong.
* @param classpath The class path to use for loading.
*
* @throws MalformedURLException if {@code classpath} contains unsupported elements.
*/
public ProjectClassLoader( final Project project, final Path classpath ) throws MalformedURLException
{
super( NO_URLS, ProjectClassLoader.class.getClassLoader() );
for ( final String name : classpath.list() )
{
final File resolved = project.resolveFile( name );
this.addURL( resolved.toURI().toURL() );
}
this.project = project;
}
/**
* Gets the project of the instance.
*
* @return The project of the instance.
*/
public final Project getProject()
{
return this.project;
}
/**
* Finds a resource with a given name.
*
* @param name The name of the resource to search.
*
* @return An {@code URL} object for reading the resource or {@code null}, if no resource matching {@code name} is
* found.
*/
@Override
public URL findResource( final String name )
{
try
{
URL resource = super.findResource( name );
if ( resource != null )
{
if ( this.getProviderResourceLocations().contains( name ) )
{
resource = this.filterProviders( resource );
}
else if ( this.getModletResourceLocations().contains( name ) )
{
resource = this.filterModlets( resource );
}
}
return resource;
}
catch ( final IOException e )
{
this.getProject().log( Messages.getMessage( e ), Project.MSG_ERR );
return null;
}
catch ( final JAXBException e )
{
String message = Messages.getMessage( e );
if ( message == null && e.getLinkedException() != null )
{
message = Messages.getMessage( e.getLinkedException() );
}
this.getProject().log( message, Project.MSG_ERR );
return null;
}
catch ( final ModelException e )
{
this.getProject().log( Messages.getMessage( e ), Project.MSG_ERR );
return null;
}
}
/**
* Gets all resources matching a given name.
*
* @param name The name of the resources to get.
*
* @return An enumeration of {@code URL} objects of found resources.
*
* @throws IOException if getting resources fails.
*/
@Override
public Enumeration<URL> findResources( final String name ) throws IOException
{
final Enumeration<URL> allResources = super.findResources( name );
Enumeration<URL> enumeration = allResources;
if ( this.getProviderResourceLocations().contains( name ) )
{
enumeration = new Enumeration<URL>()
{
public boolean hasMoreElements()
{
return allResources.hasMoreElements();
}
public URL nextElement()
{
try
{
return filterProviders( allResources.nextElement() );
}
catch ( final IOException e )
{
getProject().log( Messages.getMessage( e ), Project.MSG_ERR );
return null;
}
}
};
}
else if ( this.getModletResourceLocations().contains( name ) )
{
enumeration = new Enumeration<URL>()
{
public boolean hasMoreElements()
{
return allResources.hasMoreElements();
}
public URL nextElement()
{
try
{
return filterModlets( allResources.nextElement() );
}
catch ( final IOException e )
{
getProject().log( Messages.getMessage( e ), Project.MSG_ERR );
return null;
}
catch ( final JAXBException e )
{
String message = Messages.getMessage( e );
if ( message == null && e.getLinkedException() != null )
{
message = Messages.getMessage( e.getLinkedException() );
}
getProject().log( message, Project.MSG_ERR );
return null;
}
catch ( final ModelException e )
{
getProject().log( Messages.getMessage( e ), Project.MSG_ERR );
return null;
}
}
};
}
return enumeration;
}
/**
* Gets a set of modlet resource locations to filter.
* <p>
* This accessor method returns a reference to the live set, not a snapshot. Therefore any modification you make
* to the returned set will be present inside the object. This is why there is no {@code set} method for the
* modlet resource locations property.
* </p>
*
* @return A set of modlet resource locations to filter.
*/
public final Set<String> getModletResourceLocations()
{
if ( this.modletResourceLocations == null )
{
this.modletResourceLocations = new HashSet<String>();
}
return this.modletResourceLocations;
}
/**
* Gets a set of provider resource locations to filter.
* <p>
* This accessor method returns a reference to the live set, not a snapshot. Therefore any modification you make
* to the returned set will be present inside the object. This is why there is no {@code set} method for the
* provider resource locations property.
* </p>
*
* @return A set of provider resource locations to filter.
*/
public final Set<String> getProviderResourceLocations()
{
if ( this.providerResourceLocations == null )
{
this.providerResourceLocations = new HashSet<String>();
}
return this.providerResourceLocations;
}
/**
* Gets a set of modlet names to exclude.
* <p>
* This accessor method returns a reference to the live set, not a snapshot. Therefore any modification you make
* to the returned set will be present inside the object. This is why there is no {@code set} method for the
* modlet excludes property.
* </p>
*
* @return A set of modlet names to exclude.
*/
public final Set<String> getModletExcludes()
{
if ( this.modletExcludes == null )
{
this.modletExcludes = new HashSet<String>();
}
return this.modletExcludes;
}
/**
* Gets a set of modlet names excluded by default.
*
* @return An unmodifiable set of modlet names excluded by default.
*
* @throws IOException if reading configuration resources fails.
*/
public static Set<String> getDefaultModletExcludes() throws IOException
{
return readDefaultExcludes( ABSOLUTE_RESOURCE_NAME_PREFIX + "DefaultModletExcludes" );
}
/**
* Gets a set of modlets excluded during resource loading.
* <p>
* This accessor method returns a reference to the live set, not a snapshot. Therefore any modification you make
* to the returned set will be present inside the object. This is why there is no {@code set} method for the
* excluded modlets property.
* </p>
*
* @return A set of modlets excluded during resource loading.
*/
public final Modlets getExcludedModlets()
{
if ( this.excludedModlets == null )
{
this.excludedModlets = new Modlets();
}
return this.excludedModlets;
}
/**
* Gets a set of provider names to exclude.
* <p>
* This accessor method returns a reference to the live set, not a snapshot. Therefore any modification you make
* to the returned set will be present inside the object. This is why there is no {@code set} method for the
* provider excludes property.
* </p>
*
* @return A set of providers to exclude.
*/
public final Set<String> getProviderExcludes()
{
if ( this.providerExcludes == null )
{
this.providerExcludes = new HashSet<String>();
}
return this.providerExcludes;
}
/**
* Gets a set of provider names excluded by default.
*
* @return An unmodifiable set of provider names excluded by default.
*
* @throws IOException if reading configuration resources fails.
*/
public static Set<String> getDefaultProviderExcludes() throws IOException
{
return readDefaultExcludes( ABSOLUTE_RESOURCE_NAME_PREFIX + "DefaultProviderExcludes" );
}
/**
* Gets a set of providers excluded during resource loading.
* <p>
* This accessor method returns a reference to the live set, not a snapshot. Therefore any modification you make
* to the returned set will be present inside the object. This is why there is no {@code set} method for the
* excluded providers property.
* </p>
*
* @return A set of providers excluded during resource loading.
*/
public final Set<String> getExcludedProviders()
{
if ( this.excludedProviders == null )
{
this.excludedProviders = new HashSet<String>();
}
return this.excludedProviders;
}
/**
* Gets a set of service class names to exclude.
* <p>
* This accessor method returns a reference to the live set, not a snapshot. Therefore any modification you make
* to the returned set will be present inside the object. This is why there is no {@code set} method for the
* service excludes property.
* </p>
*
* @return A set of service class names to exclude.
*/
public final Set<String> getServiceExcludes()
{
if ( this.serviceExcludes == null )
{
this.serviceExcludes = new HashSet<String>();
}
return this.serviceExcludes;
}
/**
* Gets a set of service class names excluded by default.
*
* @return An unmodifiable set of service class names excluded by default.
*
* @throws IOException if reading configuration resources fails.
*/
public static Set<String> getDefaultServiceExcludes() throws IOException
{
return readDefaultExcludes( ABSOLUTE_RESOURCE_NAME_PREFIX + "DefaultServiceExcludes" );
}
/**
* Gets a set of services excluded during resource loading.
* <p>
* This accessor method returns a reference to the live set, not a snapshot. Therefore any modification you make
* to the returned set will be present inside the object. This is why there is no {@code set} method for the
* excluded services property.
* </p>
*
* @return Services excluded during resource loading.
*/
public final Services getExcludedServices()
{
if ( this.excludedServices == null )
{
this.excludedServices = new Services();
}
return this.excludedServices;
}
/**
* Gets a set of schema public identifiers to exclude.
* <p>
* This accessor method returns a reference to the live set, not a snapshot. Therefore any modification you make
* to the returned set will be present inside the object. This is why there is no {@code set} method for the
* schema excludes property.
* </p>
*
* @return A set of schema public identifiers to exclude.
*/
public final Set<String> getSchemaExcludes()
{
if ( this.schemaExcludes == null )
{
this.schemaExcludes = new HashSet<String>();
}
return this.schemaExcludes;
}
/**
* Gets a set of schema public identifiers excluded by default.
*
* @return An unmodifiable set of schema public identifiers excluded by default.
*
* @throws IOException if reading configuration resources fails.
*/
public static Set<String> getDefaultSchemaExcludes() throws IOException
{
return readDefaultExcludes( ABSOLUTE_RESOURCE_NAME_PREFIX + "DefaultSchemaExcludes" );
}
/**
* Gets a set of schemas excluded during resource loading.
* <p>
* This accessor method returns a reference to the live set, not a snapshot. Therefore any modification you make
* to the returned set will be present inside the object. This is why there is no {@code set} method for the
* excluded schemas property.
* </p>
*
* @return Schemas excluded during resource loading.
*/
public final Schemas getExcludedSchemas()
{
if ( this.excludedSchemas == null )
{
this.excludedSchemas = new Schemas();
}
return this.excludedSchemas;
}
/**
* Closes the class loader.
*
* @throws IOException if closing the class loader fails.
*/
@Override
@IgnoreJRERequirement
public void close() throws IOException
{
for ( final Iterator<File> it = this.temporaryResources.iterator(); it.hasNext(); )
{
final File temporaryResource = it.next();
if ( temporaryResource.exists() && temporaryResource.delete() )
{
it.remove();
}
}
if ( Closeable.class.isAssignableFrom( ProjectClassLoader.class ) )
{
super.close();
}
}
/**
* Removes temporary resources.
*
* @throws Throwable if finalization fails.
*/
@Override
protected void finalize() throws Throwable
{
for ( final Iterator<File> it = this.temporaryResources.iterator(); it.hasNext(); )
{
final File temporaryResource = it.next();
if ( temporaryResource.exists() && !temporaryResource.delete() )
{
temporaryResource.deleteOnExit();
}
it.remove();
}
super.finalize();
}
private URL filterProviders( final URL resource ) throws IOException
{
InputStream in = null;
boolean suppressExceptionOnClose = true;
try
{
URL filteredResource = resource;
in = resource.openStream();
final List<String> lines = IOUtils.readLines( in, "UTF-8" );
final List<String> filteredLines = new ArrayList<String>( lines.size() );
for ( final String line : lines )
{
if ( !this.getProviderExcludes().contains( line.trim() ) )
{
filteredLines.add( line.trim() );
}
else
{
this.getExcludedProviders().add( line.trim() );
this.getProject().log( Messages.getMessage( "providerExclusion", resource.toExternalForm(),
line.trim() ), Project.MSG_DEBUG );
}
}
if ( lines.size() != filteredLines.size() )
{
OutputStream out = null;
final File tmpResource = File.createTempFile( this.getClass().getName(), ".rsrc" );
this.temporaryResources.add( tmpResource );
try
{
out = new FileOutputStream( tmpResource );
IOUtils.writeLines( filteredLines, System.getProperty( "line.separator", "\n" ), out, "UTF-8" );
suppressExceptionOnClose = false;
}
finally
{
try
{
if ( out != null )
{
out.close();
}
suppressExceptionOnClose = true;
}
catch ( final IOException e )
{
if ( suppressExceptionOnClose )
{
this.project.log( Messages.getMessage( e ), e, Project.MSG_ERR );
}
else
{
throw e;
}
}
}
filteredResource = tmpResource.toURI().toURL();
}
suppressExceptionOnClose = false;
return filteredResource;
}
finally
{
try
{
if ( in != null )
{
in.close();
}
}
catch ( final IOException e )
{
if ( suppressExceptionOnClose )
{
this.project.log( Messages.getMessage( e ), e, Project.MSG_ERR );
}
else
{
throw e;
}
}
}
}
private URL filterModlets( final URL resource ) throws ModelException, IOException, JAXBException
{
InputStream in = null;
boolean suppressExceptionOnClose = true;
try
{
URL filteredResource = resource;
final ModelContext modelContext = ModelContextFactory.newInstance().newModelContext();
in = resource.openStream();
final JAXBElement<?> e =
(JAXBElement<?>) modelContext.createUnmarshaller( ModletObject.MODEL_PUBLIC_ID ).unmarshal( in );
final Object o = e.getValue();
Modlets modlets = null;
boolean filtered = false;
if ( o instanceof Modlets )
{
modlets = (Modlets) o;
}
else if ( o instanceof Modlet )
{
modlets = new Modlets();
modlets.getModlet().add( (Modlet) o );
}
if ( modlets != null )
{
for ( final Iterator<Modlet> it = modlets.getModlet().iterator(); it.hasNext(); )
{
final Modlet m = it.next();
if ( this.getModletExcludes().contains( m.getName() ) )
{
it.remove();
filtered = true;
this.addExcludedModlet( m );
this.getProject().log( Messages.getMessage( "modletExclusion", resource.toExternalForm(),
m.getName() ), Project.MSG_DEBUG );
continue;
}
if ( this.filterModlet( m, resource.toExternalForm() ) )
{
filtered = true;
}
}
if ( filtered )
{
final File tmpResource = File.createTempFile( this.getClass().getName(), ".rsrc" );
this.temporaryResources.add( tmpResource );
modelContext.createMarshaller( ModletObject.MODEL_PUBLIC_ID ).marshal(
new ObjectFactory().createModlets( modlets ), tmpResource );
filteredResource = tmpResource.toURI().toURL();
}
}
suppressExceptionOnClose = false;
return filteredResource;
}
finally
{
try
{
if ( in != null )
{
in.close();
}
}
catch ( final IOException e )
{
if ( suppressExceptionOnClose )
{
this.project.log( Messages.getMessage( e ), e, Project.MSG_ERR );
}
else
{
throw e;
}
}
}
}
private boolean filterModlet( final Modlet modlet, final String resourceInfo )
{
boolean filteredSchemas = false;
boolean filteredServices = false;
if ( modlet.getSchemas() != null )
{
final Schemas schemas = new Schemas();
for ( final Schema s : modlet.getSchemas().getSchema() )
{
if ( !this.getSchemaExcludes().contains( s.getPublicId() ) )
{
schemas.getSchema().add( s );
}
else
{
this.getProject().log( Messages.getMessage( "schemaExclusion", resourceInfo, s.getPublicId() ),
Project.MSG_DEBUG );
this.addExcludedSchema( s );
filteredSchemas = true;
}
}
if ( filteredSchemas )
{
modlet.setSchemas( schemas );
}
}
if ( modlet.getServices() != null )
{
final Services services = new Services();
for ( final Service s : modlet.getServices().getService() )
{
if ( !this.getServiceExcludes().contains( s.getClazz() ) )
{
services.getService().add( s );
}
else
{
this.getProject().log( Messages.getMessage( "serviceExclusion", resourceInfo, s.getClazz() ),
Project.MSG_DEBUG );
this.addExcludedService( s );
filteredServices = true;
}
}
if ( filteredServices )
{
modlet.setServices( services );
}
}
return filteredSchemas || filteredServices;
}
private void addExcludedModlet( final Modlet modlet )
{
try
{
final Modlet m = this.getExcludedModlets().getModlet( modlet.getName() );
if ( m != null )
{
if ( m.getVersion() != null && modlet.getVersion() != null
&& VersionParser.compare( m.getVersion(), modlet.getVersion() ) < 0 )
{
this.getExcludedModlets().getModlet().remove( m );
this.getExcludedModlets().getModlet().add( modlet );
}
}
else
{
this.getExcludedModlets().getModlet().add( modlet );
}
}
catch ( final ParseException e )
{
this.getProject().log( Messages.getMessage( e ), e, Project.MSG_WARN );
}
catch ( final TokenMgrError e )
{
this.getProject().log( Messages.getMessage( e ), e, Project.MSG_WARN );
}
}
private void addExcludedSchema( final Schema schema )
{
if ( this.getExcludedSchemas().getSchemaBySystemId( schema.getSystemId() ) == null )
{
this.getExcludedSchemas().getSchema().add( schema );
}
}
private void addExcludedService( final Service service )
{
for ( int i = 0, s0 = this.getExcludedServices().getService().size(); i < s0; i++ )
{
final Service s = this.getExcludedServices().getService().get( i );
if ( s.getIdentifier().equals( service.getIdentifier() ) && s.getClazz().equals( service.getClazz() ) )
{
return;
}
}
this.getExcludedServices().getService().add( service );
}
private static Set<String> readDefaultExcludes( final String location ) throws IOException
{
InputStream resource = null;
boolean suppressExceptionOnClose = true;
Set<String> defaultExcludes = null;
try
{
resource = ProjectClassLoader.class.getResourceAsStream( location );
if ( resource != null )
{
final List<String> lines = IOUtils.readLines( resource, "UTF-8" );
defaultExcludes = new HashSet<String>( lines.size() );
for ( final String line : lines )
{
final String trimmed = line.trim();
if ( trimmed.contains( "#" ) || StringUtils.isEmpty( trimmed ) )
{
continue;
}
defaultExcludes.add( trimmed );
}
}
suppressExceptionOnClose = false;
return defaultExcludes != null
? Collections.unmodifiableSet( defaultExcludes ) : Collections.<String>emptySet();
}
finally
{
try
{
if ( resource != null )
{
resource.close();
}
}
catch ( final IOException e )
{
if ( !suppressExceptionOnClose )
{
throw e;
}
}
}
}
}