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 }