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