View Javadoc
1   package org.apache.maven.plugin.war.util;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.artifact.Artifact;
23  import org.apache.maven.model.Dependency;
24  import org.codehaus.plexus.util.StringUtils;
25  
26  import java.io.IOException;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.HashMap;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Set;
34  
35  /**
36   * Represents the structure of a web application composed of multiple overlays. Each overlay is registered within this
37   * structure with the set of files it holds.
38   * <p/>
39   * Note that this structure is persisted to disk at each invocation to store which owner holds which path (file).
40   *
41   * @author Stephane Nicoll
42   * @version $Id: WebappStructure.html 925069 2014-10-08 17:03:57Z khmarbaise $
43   */
44  public class WebappStructure
45  {
46  
47      private Map<String, PathSet> registeredFiles;
48  
49      private List<DependencyInfo> dependenciesInfo;
50  
51      private transient PathSet allFiles = new PathSet();
52  
53      private transient WebappStructure cache;
54  
55      /**
56       * Creates a new empty instance.
57       *
58       * @param dependencies the dependencies of the project
59       */
60      public WebappStructure( List<Dependency> dependencies )
61      {
62          this.dependenciesInfo = createDependenciesInfoList( dependencies );
63          this.registeredFiles = new HashMap<String, PathSet>();
64          this.cache = null;
65      }
66  
67      /**
68       * Creates a new instance with the specified cache.
69       *
70       * @param dependencies the dependencies of the project
71       * @param cache the cache
72       */
73      public WebappStructure( List<Dependency> dependencies, WebappStructure cache )
74      {
75          this.dependenciesInfo = createDependenciesInfoList( dependencies );
76          this.registeredFiles = new HashMap<String, PathSet>();
77          if ( cache == null )
78          {
79              this.cache = new WebappStructure( dependencies );
80          }
81          else
82          {
83              this.cache = cache;
84          }
85      }
86  
87      /**
88       * Returns the list of {@link DependencyInfo} for the project.
89       *
90       * @return the dependencies information of the project
91       */
92      public List<DependencyInfo> getDependenciesInfo()
93      {
94          return dependenciesInfo;
95      }
96  
97      /**
98       * Returns the dependencies of the project.
99       *
100      * @return the dependencies of the project
101      */
102     public List<Dependency> getDependencies()
103     {
104         final List<Dependency> result = new ArrayList<Dependency>();
105         if ( dependenciesInfo == null )
106         {
107             return result;
108         }
109         for ( DependencyInfo dependencyInfo : dependenciesInfo )
110         {
111             result.add( dependencyInfo.getDependency() );
112         }
113         return result;
114     }
115 
116     /**
117      * Specify if the specified <tt>path</tt> is registered or not.
118      *
119      * @param path the relative path from the webapp root directory
120      * @return true if the path is registered, false otherwise
121      */
122     public boolean isRegistered( String path )
123     {
124         return getFullStructure().contains( path );
125 
126     }
127 
128     /**
129      * Registers the specified path for the specified owner. Returns <tt>true</tt> if the path is not already
130      * registered, <tt>false</tt> otherwise.
131      *
132      * @param id the owner of the path
133      * @param path the relative path from the webapp root directory
134      * @return true if the file was registered successfully
135      */
136     public boolean registerFile( String id, String path )
137     {
138         if ( !isRegistered( path ) )
139         {
140             doRegister( id, path );
141             return true;
142         }
143         else
144         {
145             return false;
146         }
147     }
148 
149     /**
150      * Forces the registration of the specified path for the specified owner. If the file is not registered yet, a
151      * simple registration is performed. If the file already exists, the owner changes to the specified one.
152      * <p/>
153      * Beware that the semantic of the return boolean is different than the one from
154      * {@link #registerFile(String, String)}; returns <tt>true</tt> if an owner replacement was made and <tt>false</tt>
155      * if the file was simply registered for the first time.
156      *
157      * @param id the owner of the path
158      * @param path the relative path from the webapp root directory
159      * @return false if the file did not exist, true if the owner was replaced
160      */
161     public boolean registerFileForced( String id, String path )
162     {
163         if ( !isRegistered( path ) )
164         {
165             doRegister( id, path );
166             return false;
167         }
168         else
169         {
170             // Force the switch to the new owner
171             getStructure( getOwner( path ) ).remove( path );
172             getStructure( id ).add( path );
173             return true;
174         }
175 
176     }
177 
178     /**
179      * Registers the specified path for the specified owner. Invokes the <tt>callback</tt> with the result of the
180      * registration.
181      *
182      * @param id the owner of the path
183      * @param path the relative path from the webapp root directory
184      * @param callback the callback to invoke with the result of the registration
185      * @throws IOException if the callback invocation throws an IOException
186      */
187     public void registerFile( String id, String path, RegistrationCallback callback )
188         throws IOException
189     {
190 
191         // If the file is already in the current structure, rejects it with the current owner
192         if ( isRegistered( path ) )
193         {
194             callback.refused( id, path, getOwner( path ) );
195         }
196         else
197         {
198             doRegister( id, path );
199             // This is a new file
200             if ( cache.getOwner( path ) == null )
201             {
202                 callback.registered( id, path );
203 
204             } // The file already belonged to this owner
205             else if ( cache.getOwner( path ).equals( id ) )
206             {
207                 callback.alreadyRegistered( id, path );
208             } // The file belongs to another owner and it's known currently
209             else if ( getOwners().contains( cache.getOwner( path ) ) )
210             {
211                 callback.superseded( id, path, cache.getOwner( path ) );
212             } // The file belongs to another owner and it's unknown
213             else
214             {
215                 callback.supersededUnknownOwner( id, path, cache.getOwner( path ) );
216             }
217         }
218     }
219 
220     /**
221      * Returns the owner of the specified <tt>path</tt>. If the file is not registered, returns <tt>null</tt>
222      *
223      * @param path the relative path from the webapp root directory
224      * @return the owner or <tt>null</tt>.
225      */
226     public String getOwner( String path )
227     {
228         if ( !isRegistered( path ) )
229         {
230             return null;
231         }
232         else
233         {
234             for ( final String owner : registeredFiles.keySet() )
235             {
236                 final PathSet structure = getStructure( owner );
237                 if ( structure.contains( path ) )
238                 {
239                     return owner;
240                 }
241 
242             }
243             throw new IllegalStateException( "Should not happen, path [" + path
244                 + "] is flagged as being registered but was not found." );
245         }
246 
247     }
248 
249     /**
250      * Returns the owners. Note that this the returned {@link Set} may be inconsistent since it represents a persistent
251      * cache across multiple invocations.
252      * <p/>
253      * For instance, if an overlay was removed in this execution, it will be still be there till the cache is cleaned.
254      * This happens when the clean mojo is invoked.
255      *
256      * @return the list of owners
257      */
258     public Set<String> getOwners()
259     {
260         return registeredFiles.keySet();
261     }
262 
263     /**
264      * Returns all paths that have been registered so far.
265      *
266      * @return all registered path
267      */
268     public PathSet getFullStructure()
269     {
270         return allFiles;
271     }
272 
273     /**
274      * Returns the list of registered files for the specified owner.
275      *
276      * @param id the owner
277      * @return the list of files registered for that owner
278      */
279     public PathSet getStructure( String id )
280     {
281         PathSet pathSet = registeredFiles.get( id );
282         if ( pathSet == null )
283         {
284             pathSet = new PathSet();
285             registeredFiles.put( id, pathSet );
286         }
287         return pathSet;
288     }
289 
290     /**
291      * Analyze the dependencies of the project using the specified callback.
292      *
293      * @param callback the callback to use to report the result of the analysis
294      */
295     public void analyseDependencies( DependenciesAnalysisCallback callback )
296     {
297         if ( callback == null )
298         {
299             throw new NullPointerException( "Callback could not be null." );
300         }
301         if ( cache == null )
302         {
303             // Could not analyze dependencies without a cache
304             return;
305         }
306 
307         final List<Dependency> currentDependencies = new ArrayList<Dependency>( getDependencies() );
308         final List<Dependency> previousDependencies = new ArrayList<Dependency>( cache.getDependencies() );
309         final Iterator<Dependency> it = currentDependencies.listIterator();
310         while ( it.hasNext() )
311         {
312             Dependency dependency = it.next();
313             // Check if the dependency is there "as is"
314 
315             final Dependency matchingDependency = matchDependency( previousDependencies, dependency );
316             if ( matchingDependency != null )
317             {
318                 callback.unchangedDependency( dependency );
319                 // Handled so let's remove
320                 it.remove();
321                 previousDependencies.remove( matchingDependency );
322             }
323             else
324             {
325                 // Try to get the dependency
326                 final Dependency previousDep = findDependency( dependency, previousDependencies );
327                 if ( previousDep == null )
328                 {
329                     callback.newDependency( dependency );
330                     it.remove();
331                 }
332                 else if ( !dependency.getVersion().equals( previousDep.getVersion() ) )
333                 {
334                     callback.updatedVersion( dependency, previousDep.getVersion() );
335                     it.remove();
336                     previousDependencies.remove( previousDep );
337                 }
338                 else if ( !dependency.getScope().equals( previousDep.getScope() ) )
339                 {
340                     callback.updatedScope( dependency, previousDep.getScope() );
341                     it.remove();
342                     previousDependencies.remove( previousDep );
343                 }
344                 else if ( dependency.isOptional() != previousDep.isOptional() )
345                 {
346                     callback.updatedOptionalFlag( dependency, previousDep.isOptional() );
347                     it.remove();
348                     previousDependencies.remove( previousDep );
349                 }
350                 else
351                 {
352                     callback.updatedUnknown( dependency, previousDep );
353                     it.remove();
354                     previousDependencies.remove( previousDep );
355                 }
356             }
357         }
358         for ( Dependency dependency : previousDependencies )
359         {
360             callback.removedDependency( dependency );
361         }
362     }
363 
364     /**
365      * Registers the target file name for the specified artifact.
366      *
367      * @param artifact the artifact
368      * @param targetFileName the target file name
369      */
370     public void registerTargetFileName( Artifact artifact, String targetFileName )
371     {
372         if ( dependenciesInfo != null )
373         {
374             for ( DependencyInfo dependencyInfo : dependenciesInfo )
375             {
376                 if ( WarUtils.isRelated( artifact, dependencyInfo.getDependency() ) )
377                 {
378                     dependencyInfo.setTargetFileName( targetFileName );
379                 }
380             }
381         }
382     }
383 
384     /**
385      * Returns the cached target file name that matches the specified dependency, that is the target file name of the
386      * previous run.
387      * <p/>
388      * The dependency object may have changed so the comparison is based on basic attributes of the dependency.
389      *
390      * @param dependency a dependency
391      * @return the target file name of the last run for this dependency
392      */
393     public String getCachedTargetFileName( Dependency dependency )
394     {
395         if ( cache == null )
396         {
397             return null;
398         }
399         for ( DependencyInfo dependencyInfo : cache.getDependenciesInfo() )
400         {
401             final Dependency dependency2 = dependencyInfo.getDependency();
402             if ( StringUtils.equals( dependency.getGroupId(), dependency2.getGroupId() )
403                 && StringUtils.equals( dependency.getArtifactId(), dependency2.getArtifactId() )
404                 && StringUtils.equals( dependency.getType(), dependency2.getType() )
405                 && StringUtils.equals( dependency.getClassifier(), dependency2.getClassifier() ) )
406             {
407 
408                 return dependencyInfo.getTargetFileName();
409 
410             }
411         }
412         return null;
413     }
414 
415     // Private helpers
416 
417     private void doRegister( String id, String path )
418     {
419         getFullStructure().add( path );
420         getStructure( id ).add( path );
421     }
422 
423     /**
424      * Find a dependency that is similar from the specified dependency.
425      *
426      * @param dependency the dependency to find
427      * @param dependencies a list of dependencies
428      * @return a similar dependency or <tt>null</tt> if no similar dependency is found
429      */
430     private Dependency findDependency( Dependency dependency, List<Dependency> dependencies )
431     {
432         for ( Dependency dep : dependencies )
433         {
434             if ( dependency.getGroupId().equals( dep.getGroupId() )
435                 && dependency.getArtifactId().equals( dep.getArtifactId() )
436                 && dependency.getType().equals( dep.getType() )
437                 && ( 
438                         ( dependency.getClassifier() == null && dep.getClassifier() == null ) 
439                      || ( dependency.getClassifier() != null && dependency.getClassifier().equals( dep.getClassifier() ) ) ) )
440             {
441                 return dep;
442             }
443         }
444         return null;
445     }
446 
447     private Dependency matchDependency( List<Dependency> dependencies, Dependency dependency )
448     {
449         for ( Dependency dep : dependencies )
450         {
451             if ( WarUtils.dependencyEquals( dep, dependency ) )
452             {
453                 return dep;
454             }
455 
456         }
457         return null;
458     }
459 
460     private List<DependencyInfo> createDependenciesInfoList( List<Dependency> dependencies )
461     {
462         if ( dependencies == null )
463         {
464             return Collections.emptyList();
465         }
466         final List<DependencyInfo> result = new ArrayList<DependencyInfo>();
467         for ( Dependency dependency : dependencies )
468         {
469             result.add( new DependencyInfo( dependency ) );
470         }
471         return result;
472     }
473 
474     private Object readResolve()
475     {
476         // the full structure should be resolved so let's rebuild it
477         this.allFiles = new PathSet();
478         for ( PathSet pathSet : registeredFiles.values() )
479         {
480             this.allFiles.addAll( pathSet );
481         }
482         return this;
483     }
484 
485     /**
486      * Callback interface to handle events related to filepath registration in the webapp.
487      */
488     public interface RegistrationCallback
489     {
490 
491         /**
492          * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been registered successfully.
493          * <p/>
494          * This means that the <tt>targetFilename</tt> was unknown and has been registered successfully.
495          *
496          * @param ownerId the ownerId
497          * @param targetFilename the relative path according to the root of the webapp
498          * @throws IOException if an error occurred while handling this event
499          */
500         void registered( String ownerId, String targetFilename )
501             throws IOException;
502 
503         /**
504          * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has already been registered.
505          * <p/>
506          * This means that the <tt>targetFilename</tt> was known and belongs to the specified owner.
507          *
508          * @param ownerId the ownerId
509          * @param targetFilename the relative path according to the root of the webapp
510          * @throws IOException if an error occurred while handling this event
511          */
512         void alreadyRegistered( String ownerId, String targetFilename )
513             throws IOException;
514 
515         /**
516          * Called if the registration of the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been refused
517          * since the path already belongs to the <tt>actualOwnerId</tt>.
518          * <p/>
519          * This means that the <tt>targetFilename</tt> was known and does not belong to the specified owner.
520          *
521          * @param ownerId the ownerId
522          * @param targetFilename the relative path according to the root of the webapp
523          * @param actualOwnerId the actual owner
524          * @throws IOException if an error occurred while handling this event
525          */
526         void refused( String ownerId, String targetFilename, String actualOwnerId )
527             throws IOException;
528 
529         /**
530          * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been registered successfully by
531          * superseding a <tt>deprecatedOwnerId</tt>, that is the previous owner of the file.
532          * <p/>
533          * This means that the <tt>targetFilename</tt> was known but for another owner. This usually happens after a
534          * project's configuration change. As a result, the file has been registered successfully to the new owner.
535          *
536          * @param ownerId the ownerId
537          * @param targetFilename the relative path according to the root of the webapp
538          * @param deprecatedOwnerId the previous owner that does not exist anymore
539          * @throws IOException if an error occurred while handling this event
540          */
541         void superseded( String ownerId, String targetFilename, String deprecatedOwnerId )
542             throws IOException;
543 
544         /**
545          * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been registered successfully by
546          * superseding a <tt>unknownOwnerId</tt>, that is an owner that does not exist anymore in the current project.
547          * <p/>
548          * This means that the <tt>targetFilename</tt> was known but for an owner that does not exist anymore. Hence the
549          * file has been registered successfully to the new owner.
550          *
551          * @param ownerId the ownerId
552          * @param targetFilename the relative path according to the root of the webapp
553          * @param unknownOwnerId the previous owner that does not exist anymore
554          * @throws IOException if an error occurred while handling this event
555          */
556         void supersededUnknownOwner( String ownerId, String targetFilename, String unknownOwnerId )
557             throws IOException;
558     }
559 
560     /**
561      * Callback interface to handle events related to dependencies analysis.
562      */
563     public interface DependenciesAnalysisCallback
564     {
565 
566         /**
567          * Called if the dependency has not changed since the last build.
568          *
569          * @param dependency the dependency that hasn't changed
570          */
571         void unchangedDependency( Dependency dependency );
572 
573         /**
574          * Called if a new dependency has been added since the last build.
575          *
576          * @param dependency the new dependency
577          */
578         void newDependency( Dependency dependency );
579 
580         /**
581          * Called if the dependency has been removed since the last build.
582          *
583          * @param dependency the dependency that has been removed
584          */
585         void removedDependency( Dependency dependency );
586 
587         /**
588          * Called if the version of the dependency has changed since the last build.
589          *
590          * @param dependency the dependency
591          * @param previousVersion the previous version of the dependency
592          */
593         void updatedVersion( Dependency dependency, String previousVersion );
594 
595         /**
596          * Called if the scope of the dependency has changed since the last build.
597          *
598          * @param dependency the dependency
599          * @param previousScope the previous scope
600          */
601         void updatedScope( Dependency dependency, String previousScope );
602 
603         /**
604          * Called if the optional flag of the dependency has changed since the last build.
605          *
606          * @param dependency the dependency
607          * @param previousOptional the previous optional flag
608          */
609         void updatedOptionalFlag( Dependency dependency, boolean previousOptional );
610 
611         /**
612          * Called if the dependency has been updated for unknown reason.
613          *
614          * @param dependency the dependency
615          * @param previousDep the previous dependency
616          */
617         void updatedUnknown( Dependency dependency, Dependency previousDep );
618 
619     }
620 }