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