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