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 935522 2015-01-08 20:15:45Z 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 // CHECKSTYLE_OFF: LineLength
433 for ( Dependency dep : dependencies )
434 {
435 if ( dependency.getGroupId().equals( dep.getGroupId() )
436 && dependency.getArtifactId().equals( dep.getArtifactId() )
437 && dependency.getType().equals( dep.getType() )
438 && (
439 ( dependency.getClassifier() == null && dep.getClassifier() == null )
440 || ( dependency.getClassifier() != null && dependency.getClassifier().equals( dep.getClassifier() ) ) ) )
441 {
442 return dep;
443 }
444 }
445 return null;
446 // CHECKSTYLE_ON: LineLength
447 }
448
449 private Dependency matchDependency( List<Dependency> dependencies, Dependency dependency )
450 {
451 for ( Dependency dep : dependencies )
452 {
453 if ( WarUtils.dependencyEquals( dep, dependency ) )
454 {
455 return dep;
456 }
457
458 }
459 return null;
460 }
461
462 private List<DependencyInfo> createDependenciesInfoList( List<Dependency> dependencies )
463 {
464 if ( dependencies == null )
465 {
466 return Collections.emptyList();
467 }
468 final List<DependencyInfo> result = new ArrayList<DependencyInfo>();
469 for ( Dependency dependency : dependencies )
470 {
471 result.add( new DependencyInfo( dependency ) );
472 }
473 return result;
474 }
475
476 private Object readResolve()
477 {
478 // the full structure should be resolved so let's rebuild it
479 this.allFiles = new PathSet();
480 for ( PathSet pathSet : registeredFiles.values() )
481 {
482 this.allFiles.addAll( pathSet );
483 }
484 return this;
485 }
486
487 /**
488 * Callback interface to handle events related to filepath registration in the webapp.
489 */
490 public interface RegistrationCallback
491 {
492
493 /**
494 * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been registered successfully.
495 * <p/>
496 * This means that the <tt>targetFilename</tt> was unknown and has been registered successfully.
497 *
498 * @param ownerId the ownerId
499 * @param targetFilename the relative path according to the root of the webapp
500 * @throws IOException if an error occurred while handling this event
501 */
502 void registered( String ownerId, String targetFilename )
503 throws IOException;
504
505 /**
506 * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has already been registered.
507 * <p/>
508 * This means that the <tt>targetFilename</tt> was known and belongs to the specified owner.
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 alreadyRegistered( String ownerId, String targetFilename )
515 throws IOException;
516
517 /**
518 * Called if the registration of the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been refused
519 * since the path already belongs to the <tt>actualOwnerId</tt>.
520 * <p/>
521 * This means that the <tt>targetFilename</tt> was known and does not belong to the specified owner.
522 *
523 * @param ownerId the ownerId
524 * @param targetFilename the relative path according to the root of the webapp
525 * @param actualOwnerId the actual owner
526 * @throws IOException if an error occurred while handling this event
527 */
528 void refused( String ownerId, String targetFilename, String actualOwnerId )
529 throws IOException;
530
531 /**
532 * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been registered successfully by
533 * superseding a <tt>deprecatedOwnerId</tt>, that is the previous owner of the file.
534 * <p/>
535 * This means that the <tt>targetFilename</tt> was known but for another owner. This usually happens after a
536 * project's configuration change. As a result, the file has been registered successfully to the new owner.
537 *
538 * @param ownerId the ownerId
539 * @param targetFilename the relative path according to the root of the webapp
540 * @param deprecatedOwnerId the previous owner that does not exist anymore
541 * @throws IOException if an error occurred while handling this event
542 */
543 void superseded( String ownerId, String targetFilename, String deprecatedOwnerId )
544 throws IOException;
545
546 /**
547 * Called if the <tt>targetFilename</tt> for the specified <tt>ownerId</tt> has been registered successfully by
548 * superseding a <tt>unknownOwnerId</tt>, that is an owner that does not exist anymore in the current project.
549 * <p/>
550 * This means that the <tt>targetFilename</tt> was known but for an owner that does not exist anymore. Hence the
551 * file has been registered successfully to the new owner.
552 *
553 * @param ownerId the ownerId
554 * @param targetFilename the relative path according to the root of the webapp
555 * @param unknownOwnerId the previous owner that does not exist anymore
556 * @throws IOException if an error occurred while handling this event
557 */
558 void supersededUnknownOwner( String ownerId, String targetFilename, String unknownOwnerId )
559 throws IOException;
560 }
561
562 /**
563 * Callback interface to handle events related to dependencies analysis.
564 */
565 public interface DependenciesAnalysisCallback
566 {
567
568 /**
569 * Called if the dependency has not changed since the last build.
570 *
571 * @param dependency the dependency that hasn't changed
572 */
573 void unchangedDependency( Dependency dependency );
574
575 /**
576 * Called if a new dependency has been added since the last build.
577 *
578 * @param dependency the new dependency
579 */
580 void newDependency( Dependency dependency );
581
582 /**
583 * Called if the dependency has been removed since the last build.
584 *
585 * @param dependency the dependency that has been removed
586 */
587 void removedDependency( Dependency dependency );
588
589 /**
590 * Called if the version of the dependency has changed since the last build.
591 *
592 * @param dependency the dependency
593 * @param previousVersion the previous version of the dependency
594 */
595 void updatedVersion( Dependency dependency, String previousVersion );
596
597 /**
598 * Called if the scope of the dependency has changed since the last build.
599 *
600 * @param dependency the dependency
601 * @param previousScope the previous scope
602 */
603 void updatedScope( Dependency dependency, String previousScope );
604
605 /**
606 * Called if the optional flag of the dependency has changed since the last build.
607 *
608 * @param dependency the dependency
609 * @param previousOptional the previous optional flag
610 */
611 void updatedOptionalFlag( Dependency dependency, boolean previousOptional );
612
613 /**
614 * Called if the dependency has been updated for unknown reason.
615 *
616 * @param dependency the dependency
617 * @param previousDep the previous dependency
618 */
619 void updatedUnknown( Dependency dependency, Dependency previousDep );
620
621 }
622 }