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 }