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 1390192 2012-09-25 22:16:06Z 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 if ( dependenciesInfo != null )
390 {
391 final Iterator it = dependenciesInfo.iterator();
392 while ( it.hasNext() )
393 {
394 DependencyInfo dependencyInfo = (DependencyInfo) it.next();
395 if ( WarUtils.isRelated( artifact, dependencyInfo.getDependency() ) )
396 {
397 dependencyInfo.setTargetFileName( targetFileName );
398 }
399 }
400 }
401 }
402
403 /**
404 * Returns the cached target file name that matches the specified
405 * dependency, that is the target file name of the previous run.
406 * <p/>
407 * The dependency object may have changed so the comparison is
408 * based on basic attributes of the dependency.
409 *
410 * @param dependency a dependency
411 * @return the target file name of the last run for this dependency
412 */
413 public String getCachedTargetFileName( Dependency dependency )
414 {
415 if ( cache == null )
416 {
417 return null;
418 }
419 final Iterator it = cache.getDependenciesInfo().iterator();
420 while ( it.hasNext() )
421 {
422 DependencyInfo dependencyInfo = (DependencyInfo) it.next();
423 final Dependency dependency2 = dependencyInfo.getDependency();
424 if ( StringUtils.equals( dependency.getGroupId(), dependency2.getGroupId() )
425 && StringUtils.equals( dependency.getArtifactId(), dependency2.getArtifactId() )
426 && StringUtils.equals( dependency.getType(), dependency2.getType() )
427 && StringUtils.equals( dependency.getClassifier(), dependency2.getClassifier() ) )
428 {
429
430 return dependencyInfo.getTargetFileName();
431
432 }
433 }
434 return null;
435 }
436
437 // Private helpers
438
439 private void doRegister( String id, String path )
440 {
441 getFullStructure().add( path );
442 getStructure( id ).add( path );
443 }
444
445 /**
446 * Find a dependency that is similar from the specified dependency.
447 *
448 * @param dependency the dependency to find
449 * @param dependencies a list of dependencies
450 * @return a similar dependency or <tt>null</tt> if no similar dependency is found
451 */
452 private Dependency findDependency( Dependency dependency, List dependencies )
453 {
454 final Iterator it = dependencies.iterator();
455 while ( it.hasNext() )
456 {
457 Dependency dep = (Dependency) it.next();
458 if ( dependency.getGroupId().equals( dep.getGroupId() )
459 && dependency.getArtifactId().equals( dep.getArtifactId() )
460 && dependency.getType().equals( dep.getType() )
461 && ( ( dependency.getClassifier() == null && dep.getClassifier() == null )
462 || ( dependency.getClassifier() != null
463 && dependency.getClassifier().equals( dep.getClassifier() ) ) ) )
464 {
465 return dep;
466 }
467 }
468 return null;
469 }
470
471 private Dependency matchDependency( List dependencies, Dependency dependency )
472 {
473 final Iterator it = dependencies.iterator();
474 while ( it.hasNext() )
475 {
476 Dependency dep = (Dependency) it.next();
477 if ( WarUtils.dependencyEquals( dep, dependency ) )
478 {
479 return dep;
480 }
481
482 }
483 return null;
484 }
485
486
487 private List createDependenciesInfoList( List dependencies )
488 {
489 if ( dependencies == null )
490 {
491 return Collections.EMPTY_LIST;
492 }
493 final List result = new ArrayList();
494 final Iterator it = dependencies.iterator();
495 while ( it.hasNext() )
496 {
497 Dependency dependency = (Dependency) it.next();
498 result.add( new DependencyInfo( dependency ) );
499 }
500 return result;
501 }
502
503
504 private Object readResolve()
505 {
506 // the full structure should be resolved so let's rebuild it
507 this.allFiles = new PathSet();
508 final Iterator it = registeredFiles.values().iterator();
509 while ( it.hasNext() )
510 {
511 PathSet pathSet = (PathSet) it.next();
512 this.allFiles.addAll( pathSet );
513 }
514 return this;
515 }
516
517 /**
518 * Callback interface to handle events related to filepath registration in
519 * the webapp.
520 */
521 public interface RegistrationCallback
522 {
523
524
525 /**
526 * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt>
527 * has been registered successfully.
528 * <p/>
529 * This means that the <tt>targetFilename</tt> was unknown and has been
530 * registered successfully.
531 *
532 * @param ownerId the ownerId
533 * @param targetFilename the relative path according to the root of the webapp
534 * @throws IOException if an error occurred while handling this event
535 */
536 void registered( String ownerId, String targetFilename )
537 throws IOException;
538
539 /**
540 * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt>
541 * has already been registered.
542 * <p/>
543 * This means that the <tt>targetFilename</tt> was known and belongs to the
544 * specified owner.
545 *
546 * @param ownerId the ownerId
547 * @param targetFilename the relative path according to the root of the webapp
548 * @throws IOException if an error occurred while handling this event
549 */
550 void alreadyRegistered( String ownerId, String targetFilename )
551 throws IOException;
552
553 /**
554 * Called if the registration of the <tt>targetFilename</tt> for the
555 * specified <tt>ownerId</tt> has been refused since the path already
556 * belongs to the <tt>actualOwnerId</tt>.
557 * <p/>
558 * This means that the <tt>targetFilename</tt> was known and does not
559 * belong to the specified owner.
560 *
561 * @param ownerId the ownerId
562 * @param targetFilename the relative path according to the root of the webapp
563 * @param actualOwnerId the actual owner
564 * @throws IOException if an error occurred while handling this event
565 */
566 void refused( String ownerId, String targetFilename, String actualOwnerId )
567 throws IOException;
568
569 /**
570 * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt>
571 * has been registered successfully by superseding a <tt>deprecatedOwnerId</tt>,
572 * that is the previous owner of the file.
573 * <p/>
574 * This means that the <tt>targetFilename</tt> was known but for another
575 * owner. This usually happens after a project's configuration change. As a
576 * result, the file has been registered successfully to the new owner.
577 *
578 * @param ownerId the ownerId
579 * @param targetFilename the relative path according to the root of the webapp
580 * @param deprecatedOwnerId the previous owner that does not exist anymore
581 * @throws IOException if an error occurred while handling this event
582 */
583 void superseded( String ownerId, String targetFilename, String deprecatedOwnerId )
584 throws IOException;
585
586 /**
587 * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt>
588 * has been registered successfully by superseding a <tt>unknownOwnerId</tt>,
589 * that is an owner that does not exist anymore in the current project.
590 * <p/>
591 * This means that the <tt>targetFilename</tt> was known but for an owner that
592 * does not exist anymore. Hence the file has been registered successfully to
593 * the new owner.
594 *
595 * @param ownerId the ownerId
596 * @param targetFilename the relative path according to the root of the webapp
597 * @param unknownOwnerId the previous owner that does not exist anymore
598 * @throws IOException if an error occurred while handling this event
599 */
600 void supersededUnknownOwner( String ownerId, String targetFilename, String unknownOwnerId )
601 throws IOException;
602 }
603
604 /**
605 * Callback interface to handle events related to dependencies analysis.
606 */
607 public interface DependenciesAnalysisCallback
608 {
609
610 /**
611 * Called if the dependency has not changed since the last build.
612 *
613 * @param dependency the dependency that hasn't changed
614 */
615 void unchangedDependency( Dependency dependency );
616
617 /**
618 * Called if a new dependency has been added since the last build.
619 *
620 * @param dependency the new dependency
621 */
622 void newDependency( Dependency dependency );
623
624 /**
625 * Called if the dependency has been removed since the last build.
626 *
627 * @param dependency the dependency that has been removed
628 */
629 void removedDependency( Dependency dependency );
630
631 /**
632 * Called if the version of the dependency has changed since the last build.
633 *
634 * @param dependency the dependency
635 * @param previousVersion the previous version of the dependency
636 */
637 void updatedVersion( Dependency dependency, String previousVersion );
638
639 /**
640 * Called if the scope of the dependency has changed since the last build.
641 *
642 * @param dependency the dependency
643 * @param previousScope the previous scope
644 */
645 void updatedScope( Dependency dependency, String previousScope );
646
647 /**
648 * Called if the optional flag of the dependency has changed since the
649 * last build.
650 *
651 * @param dependency the dependency
652 * @param previousOptional the previous optional flag
653 */
654 void updatedOptionalFlag( Dependency dependency, boolean previousOptional );
655
656 /**
657 * Called if the dependency has been updated for unknown reason.
658 *
659 * @param dependency the dependency
660 * @param previousDep the previous dependency
661 */
662 void updatedUnknown( Dependency dependency, Dependency previousDep );
663
664 }
665 }