blob: f842519e735b243b379c175e5e71720a96ed063c [file] [log] [blame]
package org.apache.maven.plugin.war.util;
/*
* 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.
*/
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency;
import org.codehaus.plexus.util.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Represents the structure of a web application composed of multiple overlays. Each overlay is registered within this
* structure with the set of files it holds.
* <p/>
* Note that this structure is persisted to disk at each invocation to store which owner holds which path (file).
*
* @author Stephane Nicoll
* @version $Id$
*/
public class WebappStructure
{
private Map<String, PathSet> registeredFiles;
private List<DependencyInfo> dependenciesInfo;
private transient PathSet allFiles = new PathSet();
private transient WebappStructure cache;
/**
* Creates a new empty instance.
*
* @param dependencies the dependencies of the project
*/
public WebappStructure( List<Dependency> dependencies )
{
this.dependenciesInfo = createDependenciesInfoList( dependencies );
this.registeredFiles = new HashMap<String, PathSet>();
this.cache = null;
}
/**
* Creates a new instance with the specified cache.
*
* @param dependencies the dependencies of the project
* @param cache the cache
*/
public WebappStructure( List<Dependency> dependencies, WebappStructure cache )
{
this.dependenciesInfo = createDependenciesInfoList( dependencies );
this.registeredFiles = new HashMap<String, PathSet>();
if ( cache == null )
{
this.cache = new WebappStructure( dependencies );
}
else
{
this.cache = cache;
}
}
/**
* Returns the list of {@link DependencyInfo} for the project.
*
* @return the dependencies information of the project
*/
public List<DependencyInfo> getDependenciesInfo()
{
return dependenciesInfo;
}
/**
* Returns the dependencies of the project.
*
* @return the dependencies of the project
*/
public List<Dependency> getDependencies()
{
final List<Dependency> result = new ArrayList<Dependency>();
if ( dependenciesInfo == null )
{
return result;
}
for ( DependencyInfo dependencyInfo : dependenciesInfo )
{
result.add( dependencyInfo.getDependency() );
}
return result;
}
/**
* Specify if the specified <tt>path</tt> is registered or not.
*
* @param path the relative path from the webapp root directory
* @return true if the path is registered, false otherwise
*/
public boolean isRegistered( String path )
{
return getFullStructure().contains( path );
}
/**
* Registers the specified path for the specified owner. Returns <tt>true</tt> if the path is not already
* registered, <tt>false</tt> otherwise.
*
* @param id the owner of the path
* @param path the relative path from the webapp root directory
* @return true if the file was registered successfully
*/
public boolean registerFile( String id, String path )
{
if ( !isRegistered( path ) )
{
doRegister( id, path );
return true;
}
else
{
return false;
}
}
/**
* Forces the registration of the specified path for the specified owner. If the file is not registered yet, a
* simple registration is performed. If the file already exists, the owner changes to the specified one.
* <p/>
* Beware that the semantic of the return boolean is different than the one from
* {@link #registerFile(String, String)}; returns <tt>true</tt> if an owner replacement was made and <tt>false</tt>
* if the file was simply registered for the first time.
*
* @param id the owner of the path
* @param path the relative path from the webapp root directory
* @return false if the file did not exist, true if the owner was replaced
*/
public boolean registerFileForced( String id, String path )
{
if ( !isRegistered( path ) )
{
doRegister( id, path );
return false;
}
else
{
// Force the switch to the new owner
getStructure( getOwner( path ) ).remove( path );
getStructure( id ).add( path );
return true;
}
}
/**
* Registers the specified path for the specified owner. Invokes the <tt>callback</tt> with the result of the
* registration.
*
* @param id the owner of the path
* @param path the relative path from the webapp root directory
* @param callback the callback to invoke with the result of the registration
* @throws IOException if the callback invocation throws an IOException
*/
public void registerFile( String id, String path, RegistrationCallback callback )
throws IOException
{
// If the file is already in the current structure, rejects it with the current owner
if ( isRegistered( path ) )
{
callback.refused( id, path, getOwner( path ) );
}
else
{
doRegister( id, path );
// This is a new file
if ( cache.getOwner( path ) == null )
{
callback.registered( id, path );
} // The file already belonged to this owner
else if ( cache.getOwner( path ).equals( id ) )
{
callback.alreadyRegistered( id, path );
} // The file belongs to another owner and it's known currently
else if ( getOwners().contains( cache.getOwner( path ) ) )
{
callback.superseded( id, path, cache.getOwner( path ) );
} // The file belongs to another owner and it's unknown
else
{
callback.supersededUnknownOwner( id, path, cache.getOwner( path ) );
}
}
}
/**
* Returns the owner of the specified <tt>path</tt>. If the file is not registered, returns <tt>null</tt>
*
* @param path the relative path from the webapp root directory
* @return the owner or <tt>null</tt>.
*/
public String getOwner( String path )
{
if ( !isRegistered( path ) )
{
return null;
}
else
{
for ( final String owner : registeredFiles.keySet() )
{
final PathSet structure = getStructure( owner );
if ( structure.contains( path ) )
{
return owner;
}
}
throw new IllegalStateException( "Should not happen, path [" + path
+ "] is flagged as being registered but was not found." );
}
}
/**
* Returns the owners. Note that this the returned {@link Set} may be inconsistent since it represents a persistent
* cache across multiple invocations.
* <p/>
* For instance, if an overlay was removed in this execution, it will be still be there till the cache is cleaned.
* This happens when the clean mojo is invoked.
*
* @return the list of owners
*/
public Set<String> getOwners()
{
return registeredFiles.keySet();
}
/**
* Returns all paths that have been registered so far.
*
* @return all registered path
*/
public PathSet getFullStructure()
{
return allFiles;
}
/**
* Returns the list of registered files for the specified owner.
*
* @param id the owner
* @return the list of files registered for that owner
*/
public PathSet getStructure( String id )
{
PathSet pathSet = registeredFiles.get( id );
if ( pathSet == null )
{
pathSet = new PathSet();
registeredFiles.put( id, pathSet );
}
return pathSet;
}
/**
* Analyze the dependencies of the project using the specified callback.
*
* @param callback the callback to use to report the result of the analysis
*/
public void analyseDependencies( DependenciesAnalysisCallback callback )
{
if ( callback == null )
{
throw new NullPointerException( "Callback could not be null." );
}
if ( cache == null )
{
// Could not analyze dependencies without a cache
return;
}
final List<Dependency> currentDependencies = new ArrayList<Dependency>( getDependencies() );
final List<Dependency> previousDependencies = new ArrayList<Dependency>( cache.getDependencies() );
final Iterator<Dependency> it = currentDependencies.listIterator();
while ( it.hasNext() )
{
Dependency dependency = it.next();
// Check if the dependency is there "as is"
final Dependency matchingDependency = matchDependency( previousDependencies, dependency );
if ( matchingDependency != null )
{
callback.unchangedDependency( dependency );
// Handled so let's remove
it.remove();
previousDependencies.remove( matchingDependency );
}
else
{
// Try to get the dependency
final Dependency previousDep = findDependency( dependency, previousDependencies );
if ( previousDep == null )
{
callback.newDependency( dependency );
it.remove();
}
else if ( !dependency.getVersion().equals( previousDep.getVersion() ) )
{
callback.updatedVersion( dependency, previousDep.getVersion() );
it.remove();
previousDependencies.remove( previousDep );
}
else if ( !dependency.getScope().equals( previousDep.getScope() ) )
{
callback.updatedScope( dependency, previousDep.getScope() );
it.remove();
previousDependencies.remove( previousDep );
}
else if ( dependency.isOptional() != previousDep.isOptional() )
{
callback.updatedOptionalFlag( dependency, previousDep.isOptional() );
it.remove();
previousDependencies.remove( previousDep );
}
else
{
callback.updatedUnknown( dependency, previousDep );
it.remove();
previousDependencies.remove( previousDep );
}
}
}
for ( Dependency dependency : previousDependencies )
{
callback.removedDependency( dependency );
}
}
/**
* Registers the target file name for the specified artifact.
*
* @param artifact the artifact
* @param targetFileName the target file name
*/
public void registerTargetFileName( Artifact artifact, String targetFileName )
{
if ( dependenciesInfo != null )
{
for ( DependencyInfo dependencyInfo : dependenciesInfo )
{
if ( WarUtils.isRelated( artifact, dependencyInfo.getDependency() ) )
{
dependencyInfo.setTargetFileName( targetFileName );
}
}
}
}
/**
* Returns the cached target file name that matches the specified dependency, that is the target file name of the
* previous run.
* <p/>
* The dependency object may have changed so the comparison is based on basic attributes of the dependency.
*
* @param dependency a dependency
* @return the target file name of the last run for this dependency
*/
public String getCachedTargetFileName( Dependency dependency )
{
if ( cache == null )
{
return null;
}
for ( DependencyInfo dependencyInfo : cache.getDependenciesInfo() )
{
final Dependency dependency2 = dependencyInfo.getDependency();
if ( StringUtils.equals( dependency.getGroupId(), dependency2.getGroupId() )
&& StringUtils.equals( dependency.getArtifactId(), dependency2.getArtifactId() )
&& StringUtils.equals( dependency.getType(), dependency2.getType() )
&& StringUtils.equals( dependency.getClassifier(), dependency2.getClassifier() ) )
{
return dependencyInfo.getTargetFileName();
}
}
return null;
}
// Private helpers
private void doRegister( String id, String path )
{
getFullStructure().add( path );
getStructure( id ).add( path );
}
/**
* Find a dependency that is similar from the specified dependency.
*
* @param dependency the dependency to find
* @param dependencies a list of dependencies
* @return a similar dependency or <tt>null</tt> if no similar dependency is found
*/
private Dependency findDependency( Dependency dependency, List<Dependency> dependencies )
{
for ( Dependency dep : dependencies )
{
if ( dependency.getGroupId().equals( dep.getGroupId() )
&& dependency.getArtifactId().equals( dep.getArtifactId() )
&& dependency.getType().equals( dep.getType() )
&& (
( dependency.getClassifier() == null && dep.getClassifier() == null )
|| ( dependency.getClassifier() != null && dependency.getClassifier().equals( dep.getClassifier() ) ) ) )
{
return dep;
}
}
return null;
}
private Dependency matchDependency( List<Dependency> dependencies, Dependency dependency )
{
for ( Dependency dep : dependencies )
{
if ( WarUtils.dependencyEquals( dep, dependency ) )
{
return dep;
}
}
return null;
}
private List<DependencyInfo> createDependenciesInfoList( List<Dependency> dependencies )
{
if ( dependencies == null )
{
return Collections.emptyList();
}
final List<DependencyInfo> result = new ArrayList<DependencyInfo>();
for ( Dependency dependency : dependencies )
{
result.add( new DependencyInfo( dependency ) );
}
return result;
}
private Object readResolve()
{
// the full structure should be resolved so let's rebuild it
this.allFiles = new PathSet();
for ( PathSet pathSet : registeredFiles.values() )
{
this.allFiles.addAll( pathSet );
}
return this;
}
/**
* Callback interface to handle events related to filepath registration in the webapp.
*/
public interface RegistrationCallback
{
/**
* Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been registered successfully.
* <p/>
* This means that the <tt>targetFilename</tt> was unknown and has been registered successfully.
*
* @param ownerId the ownerId
* @param targetFilename the relative path according to the root of the webapp
* @throws IOException if an error occurred while handling this event
*/
void registered( String ownerId, String targetFilename )
throws IOException;
/**
* Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has already been registered.
* <p/>
* This means that the <tt>targetFilename</tt> was known and belongs to the specified owner.
*
* @param ownerId the ownerId
* @param targetFilename the relative path according to the root of the webapp
* @throws IOException if an error occurred while handling this event
*/
void alreadyRegistered( String ownerId, String targetFilename )
throws IOException;
/**
* Called if the registration of the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been refused
* since the path already belongs to the <tt>actualOwnerId</tt>.
* <p/>
* This means that the <tt>targetFilename</tt> was known and does not belong to the specified owner.
*
* @param ownerId the ownerId
* @param targetFilename the relative path according to the root of the webapp
* @param actualOwnerId the actual owner
* @throws IOException if an error occurred while handling this event
*/
void refused( String ownerId, String targetFilename, String actualOwnerId )
throws IOException;
/**
* Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been registered successfully by
* superseding a <tt>deprecatedOwnerId</tt>, that is the previous owner of the file.
* <p/>
* This means that the <tt>targetFilename</tt> was known but for another owner. This usually happens after a
* project's configuration change. As a result, the file has been registered successfully to the new owner.
*
* @param ownerId the ownerId
* @param targetFilename the relative path according to the root of the webapp
* @param deprecatedOwnerId the previous owner that does not exist anymore
* @throws IOException if an error occurred while handling this event
*/
void superseded( String ownerId, String targetFilename, String deprecatedOwnerId )
throws IOException;
/**
* Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been registered successfully by
* superseding a <tt>unknownOwnerId</tt>, that is an owner that does not exist anymore in the current project.
* <p/>
* This means that the <tt>targetFilename</tt> was known but for an owner that does not exist anymore. Hence the
* file has been registered successfully to the new owner.
*
* @param ownerId the ownerId
* @param targetFilename the relative path according to the root of the webapp
* @param unknownOwnerId the previous owner that does not exist anymore
* @throws IOException if an error occurred while handling this event
*/
void supersededUnknownOwner( String ownerId, String targetFilename, String unknownOwnerId )
throws IOException;
}
/**
* Callback interface to handle events related to dependencies analysis.
*/
public interface DependenciesAnalysisCallback
{
/**
* Called if the dependency has not changed since the last build.
*
* @param dependency the dependency that hasn't changed
*/
void unchangedDependency( Dependency dependency );
/**
* Called if a new dependency has been added since the last build.
*
* @param dependency the new dependency
*/
void newDependency( Dependency dependency );
/**
* Called if the dependency has been removed since the last build.
*
* @param dependency the dependency that has been removed
*/
void removedDependency( Dependency dependency );
/**
* Called if the version of the dependency has changed since the last build.
*
* @param dependency the dependency
* @param previousVersion the previous version of the dependency
*/
void updatedVersion( Dependency dependency, String previousVersion );
/**
* Called if the scope of the dependency has changed since the last build.
*
* @param dependency the dependency
* @param previousScope the previous scope
*/
void updatedScope( Dependency dependency, String previousScope );
/**
* Called if the optional flag of the dependency has changed since the last build.
*
* @param dependency the dependency
* @param previousOptional the previous optional flag
*/
void updatedOptionalFlag( Dependency dependency, boolean previousOptional );
/**
* Called if the dependency has been updated for unknown reason.
*
* @param dependency the dependency
* @param previousDep the previous dependency
*/
void updatedUnknown( Dependency dependency, Dependency previousDep );
}
}