| 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 ); |
| |
| } |
| } |