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