View Javadoc
1   package org.apache.maven.plugins.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   * 
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$
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.</p>
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      * </p>
256      *
257      * @return the list of owners
258      */
259     public Set<String> getOwners()
260     {
261         return registeredFiles.keySet();
262     }
263 
264     /**
265      * Returns all paths that have been registered so far.
266      *
267      * @return all registered path
268      */
269     public PathSet getFullStructure()
270     {
271         return allFiles;
272     }
273 
274     /**
275      * Returns the list of registered files for the specified owner.
276      *
277      * @param id the owner
278      * @return the list of files registered for that owner
279      */
280     public PathSet getStructure( String id )
281     {
282         PathSet pathSet = registeredFiles.get( id );
283         if ( pathSet == null )
284         {
285             pathSet = new PathSet();
286             registeredFiles.put( id, pathSet );
287         }
288         return pathSet;
289     }
290 
291     /**
292      * Analyze the dependencies of the project using the specified callback.
293      *
294      * @param callback the callback to use to report the result of the analysis
295      */
296     public void analyseDependencies( DependenciesAnalysisCallback callback )
297     {
298         if ( callback == null )
299         {
300             throw new NullPointerException( "Callback could not be null." );
301         }
302         if ( cache == null )
303         {
304             // Could not analyze dependencies without a cache
305             return;
306         }
307 
308         final List<Dependency> currentDependencies = new ArrayList<Dependency>( getDependencies() );
309         final List<Dependency> previousDependencies = new ArrayList<Dependency>( cache.getDependencies() );
310         final Iterator<Dependency> it = currentDependencies.listIterator();
311         while ( it.hasNext() )
312         {
313             Dependency dependency = it.next();
314             // Check if the dependency is there "as is"
315 
316             final Dependency matchingDependency = matchDependency( previousDependencies, dependency );
317             if ( matchingDependency != null )
318             {
319                 callback.unchangedDependency( dependency );
320                 // Handled so let's remove
321                 it.remove();
322                 previousDependencies.remove( matchingDependency );
323             }
324             else
325             {
326                 // Try to get the dependency
327                 final Dependency previousDep = findDependency( dependency, previousDependencies );
328                 if ( previousDep == null )
329                 {
330                     callback.newDependency( dependency );
331                     it.remove();
332                 }
333                 else if ( !dependency.getVersion().equals( previousDep.getVersion() ) )
334                 {
335                     callback.updatedVersion( dependency, previousDep.getVersion() );
336                     it.remove();
337                     previousDependencies.remove( previousDep );
338                 }
339                 else if ( !dependency.getScope().equals( previousDep.getScope() ) )
340                 {
341                     callback.updatedScope( dependency, previousDep.getScope() );
342                     it.remove();
343                     previousDependencies.remove( previousDep );
344                 }
345                 else if ( dependency.isOptional() != previousDep.isOptional() )
346                 {
347                     callback.updatedOptionalFlag( dependency, previousDep.isOptional() );
348                     it.remove();
349                     previousDependencies.remove( previousDep );
350                 }
351                 else
352                 {
353                     callback.updatedUnknown( dependency, previousDep );
354                     it.remove();
355                     previousDependencies.remove( previousDep );
356                 }
357             }
358         }
359         for ( Dependency dependency : previousDependencies )
360         {
361             callback.removedDependency( dependency );
362         }
363     }
364 
365     /**
366      * Registers the target file name for the specified artifact.
367      *
368      * @param artifact the artifact
369      * @param targetFileName the target file name
370      */
371     public void registerTargetFileName( Artifact artifact, String targetFileName )
372     {
373         if ( dependenciesInfo != null )
374         {
375             for ( DependencyInfo dependencyInfo : dependenciesInfo )
376             {
377                 if ( WarUtils.isRelated( artifact, dependencyInfo.getDependency() ) )
378                 {
379                     dependencyInfo.setTargetFileName( targetFileName );
380                 }
381             }
382         }
383     }
384 
385     /**
386      * Returns the cached target file name that matches the specified dependency, that is the target file name of the
387      * previous run.
388      * <p>
389      * The dependency object may have changed so the comparison is based on basic attributes of the dependency.
390      * </p>
391      *
392      * @param dependency a dependency
393      * @return the target file name of the last run for this dependency
394      */
395     public String getCachedTargetFileName( Dependency dependency )
396     {
397         if ( cache == null )
398         {
399             return null;
400         }
401         for ( DependencyInfo dependencyInfo : cache.getDependenciesInfo() )
402         {
403             final Dependency dependency2 = dependencyInfo.getDependency();
404             if ( StringUtils.equals( dependency.getGroupId(), dependency2.getGroupId() )
405                 && StringUtils.equals( dependency.getArtifactId(), dependency2.getArtifactId() )
406                 && StringUtils.equals( dependency.getType(), dependency2.getType() )
407                 && StringUtils.equals( dependency.getClassifier(), dependency2.getClassifier() ) )
408             {
409 
410                 return dependencyInfo.getTargetFileName();
411 
412             }
413         }
414         return null;
415     }
416 
417     // Private helpers
418 
419     private void doRegister( String id, String path )
420     {
421         getFullStructure().add( path );
422         getStructure( id ).add( path );
423     }
424 
425     /**
426      * Find a dependency that is similar from the specified dependency.
427      *
428      * @param dependency the dependency to find
429      * @param dependencies a list of dependencies
430      * @return a similar dependency or <tt>null</tt> if no similar dependency is found
431      */
432     private Dependency findDependency( Dependency dependency, List<Dependency> dependencies )
433     {
434         // CHECKSTYLE_OFF: LineLength
435         for ( Dependency dep : dependencies )
436         {
437             if ( dependency.getGroupId().equals( dep.getGroupId() )
438                 && dependency.getArtifactId().equals( dep.getArtifactId() )
439                 && dependency.getType().equals( dep.getType() )
440                 && ( 
441                         ( dependency.getClassifier() == null && dep.getClassifier() == null ) 
442                      || ( dependency.getClassifier() != null && dependency.getClassifier().equals( dep.getClassifier() ) ) ) )
443             {
444                 return dep;
445             }
446         }
447         return null;
448         // CHECKSTYLE_ON: LineLength
449     }
450 
451     private Dependency matchDependency( List<Dependency> dependencies, Dependency dependency )
452     {
453         for ( Dependency dep : dependencies )
454         {
455             if ( WarUtils.dependencyEquals( dep, dependency ) )
456             {
457                 return dep;
458             }
459 
460         }
461         return null;
462     }
463 
464     private List<DependencyInfo> createDependenciesInfoList( List<Dependency> dependencies )
465     {
466         if ( dependencies == null )
467         {
468             return Collections.emptyList();
469         }
470         final List<DependencyInfo> result = new ArrayList<DependencyInfo>();
471         for ( Dependency dependency : dependencies )
472         {
473             result.add( new DependencyInfo( dependency ) );
474         }
475         return result;
476     }
477 
478     private Object readResolve()
479     {
480         // the full structure should be resolved so let's rebuild it
481         this.allFiles = new PathSet();
482         for ( PathSet pathSet : registeredFiles.values() )
483         {
484             this.allFiles.addAll( pathSet );
485         }
486         return this;
487     }
488 
489     /**
490      * Callback interface to handle events related to filepath registration in the webapp.
491      */
492     public interface RegistrationCallback
493     {
494 
495         /**
496          * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been registered successfully.
497          * 
498          * This means that the <tt>targetFilename</tt> was unknown and has been registered successfully.
499          *
500          * @param ownerId the ownerId
501          * @param targetFilename the relative path according to the root of the webapp
502          * @throws IOException if an error occurred while handling this event
503          */
504         void registered( String ownerId, String targetFilename )
505             throws IOException;
506 
507         /**
508          * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has already been registered.
509          * 
510          * This means that the <tt>targetFilename</tt> was known and belongs to the specified owner.
511          *
512          * @param ownerId the ownerId
513          * @param targetFilename the relative path according to the root of the webapp
514          * @throws IOException if an error occurred while handling this event
515          */
516         void alreadyRegistered( String ownerId, String targetFilename )
517             throws IOException;
518 
519         /**
520          * <p>
521          * Called if the registration of the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been refused
522          * since the path already belongs to the <tt>actualOwnerId</tt>.
523          * </p> 
524          * This means that the <tt>targetFilename</tt> was known and does not belong to the specified owner.
525          *
526          * @param ownerId the ownerId
527          * @param targetFilename the relative path according to the root of the webapp
528          * @param actualOwnerId the actual owner
529          * @throws IOException if an error occurred while handling this event
530          */
531         void refused( String ownerId, String targetFilename, String actualOwnerId )
532             throws IOException;
533 
534         /**
535          * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been registered successfully by
536          * superseding a <tt>deprecatedOwnerId</tt>, that is the previous owner of the file.
537          * 
538          * This means that the <tt>targetFilename</tt> was known but for another owner. This usually happens after a
539          * project's configuration change. As a result, the file has been registered successfully to the new owner.
540          *
541          * @param ownerId the ownerId
542          * @param targetFilename the relative path according to the root of the webapp
543          * @param deprecatedOwnerId the previous owner that does not exist anymore
544          * @throws IOException if an error occurred while handling this event
545          */
546         void superseded( String ownerId, String targetFilename, String deprecatedOwnerId )
547             throws IOException;
548 
549         /**
550          * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been registered successfully by
551          * superseding a <tt>unknownOwnerId</tt>, that is an owner that does not exist anymore in the current project.
552          * 
553          * This means that the <tt>targetFilename</tt> was known but for an owner that does not exist anymore. Hence the
554          * 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 unknownOwnerId the previous owner that does not exist anymore
559          * @throws IOException if an error occurred while handling this event
560          */
561         void supersededUnknownOwner( String ownerId, String targetFilename, String unknownOwnerId )
562             throws IOException;
563     }
564 
565     /**
566      * Callback interface to handle events related to dependencies analysis.
567      */
568     public interface DependenciesAnalysisCallback
569     {
570 
571         /**
572          * Called if the dependency has not changed since the last build.
573          *
574          * @param dependency the dependency that hasn't changed
575          */
576         void unchangedDependency( Dependency dependency );
577 
578         /**
579          * Called if a new dependency has been added since the last build.
580          *
581          * @param dependency the new dependency
582          */
583         void newDependency( Dependency dependency );
584 
585         /**
586          * Called if the dependency has been removed since the last build.
587          *
588          * @param dependency the dependency that has been removed
589          */
590         void removedDependency( Dependency dependency );
591 
592         /**
593          * Called if the version of the dependency has changed since the last build.
594          *
595          * @param dependency the dependency
596          * @param previousVersion the previous version of the dependency
597          */
598         void updatedVersion( Dependency dependency, String previousVersion );
599 
600         /**
601          * Called if the scope of the dependency has changed since the last build.
602          *
603          * @param dependency the dependency
604          * @param previousScope the previous scope
605          */
606         void updatedScope( Dependency dependency, String previousScope );
607 
608         /**
609          * Called if the optional flag of the dependency has changed since the last build.
610          *
611          * @param dependency the dependency
612          * @param previousOptional the previous optional flag
613          */
614         void updatedOptionalFlag( Dependency dependency, boolean previousOptional );
615 
616         /**
617          * Called if the dependency has been updated for unknown reason.
618          *
619          * @param dependency the dependency
620          * @param previousDep the previous dependency
621          */
622         void updatedUnknown( Dependency dependency, Dependency previousDep );
623 
624     }
625 }