View Javadoc
1   package org.apache.maven.plugin.resources.remote;
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 java.io.ByteArrayInputStream;
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.FileNotFoundException;
26  import java.io.FileOutputStream;
27  import java.io.FileReader;
28  import java.io.FileWriter;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.io.InputStreamReader;
32  import java.io.OutputStream;
33  import java.io.OutputStreamWriter;
34  import java.io.PrintWriter;
35  import java.io.Reader;
36  import java.io.StringReader;
37  import java.io.UnsupportedEncodingException;
38  import java.io.Writer;
39  import java.net.MalformedURLException;
40  import java.net.URL;
41  import java.text.SimpleDateFormat;
42  import java.util.AbstractMap;
43  import java.util.ArrayList;
44  import java.util.Arrays;
45  import java.util.Collections;
46  import java.util.Comparator;
47  import java.util.Date;
48  import java.util.Enumeration;
49  import java.util.HashMap;
50  import java.util.LinkedHashSet;
51  import java.util.List;
52  import java.util.Map;
53  import java.util.Properties;
54  import java.util.Set;
55  import java.util.TreeMap;
56  
57  import org.apache.commons.io.output.DeferredFileOutputStream;
58  import org.apache.maven.ProjectDependenciesResolver;
59  import org.apache.maven.archiver.MavenArchiver;
60  import org.apache.maven.artifact.Artifact;
61  import org.apache.maven.artifact.factory.ArtifactFactory;
62  import org.apache.maven.artifact.repository.ArtifactRepository;
63  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
64  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
65  import org.apache.maven.artifact.resolver.ArtifactResolver;
66  import org.apache.maven.artifact.versioning.VersionRange;
67  import org.apache.maven.execution.MavenSession;
68  import org.apache.maven.model.Model;
69  import org.apache.maven.model.Organization;
70  import org.apache.maven.model.Resource;
71  import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
72  import org.apache.maven.plugin.AbstractMojo;
73  import org.apache.maven.plugin.MojoExecutionException;
74  import org.apache.maven.plugin.resources.remote.io.xpp3.RemoteResourcesBundleXpp3Reader;
75  import org.apache.maven.plugin.resources.remote.io.xpp3.SupplementalDataModelXpp3Reader;
76  import org.apache.maven.plugins.annotations.Component;
77  import org.apache.maven.plugins.annotations.LifecyclePhase;
78  import org.apache.maven.plugins.annotations.Mojo;
79  import org.apache.maven.plugins.annotations.Parameter;
80  import org.apache.maven.project.InvalidProjectModelException;
81  import org.apache.maven.project.MavenProject;
82  import org.apache.maven.project.MavenProjectBuilder;
83  import org.apache.maven.project.ProjectBuildingException;
84  import org.apache.maven.project.artifact.InvalidDependencyVersionException;
85  import org.apache.maven.project.inheritance.ModelInheritanceAssembler;
86  import org.apache.maven.shared.artifact.filter.collection.ArtifactFilterException;
87  import org.apache.maven.shared.artifact.filter.collection.ArtifactIdFilter;
88  import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts;
89  import org.apache.maven.shared.artifact.filter.collection.GroupIdFilter;
90  import org.apache.maven.shared.artifact.filter.collection.ProjectTransitivityFilter;
91  import org.apache.maven.shared.artifact.filter.collection.ScopeFilter;
92  import org.apache.maven.shared.filtering.MavenFileFilter;
93  import org.apache.maven.shared.filtering.MavenFileFilterRequest;
94  import org.apache.maven.shared.filtering.MavenFilteringException;
95  import org.apache.velocity.VelocityContext;
96  import org.apache.velocity.app.Velocity;
97  import org.apache.velocity.app.VelocityEngine;
98  import org.apache.velocity.exception.MethodInvocationException;
99  import org.apache.velocity.exception.ParseErrorException;
100 import org.apache.velocity.exception.ResourceNotFoundException;
101 import org.apache.velocity.exception.VelocityException;
102 import org.apache.velocity.runtime.RuntimeConstants;
103 import org.apache.velocity.runtime.RuntimeServices;
104 import org.apache.velocity.runtime.log.LogChute;
105 import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
106 import org.codehaus.plexus.resource.ResourceManager;
107 import org.codehaus.plexus.resource.loader.FileResourceLoader;
108 import org.codehaus.plexus.util.FileUtils;
109 import org.codehaus.plexus.util.IOUtil;
110 import org.codehaus.plexus.util.ReaderFactory;
111 import org.codehaus.plexus.util.StringUtils;
112 import org.codehaus.plexus.util.WriterFactory;
113 import org.codehaus.plexus.util.xml.Xpp3Dom;
114 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
115 
116 /**
117  * <p>
118  * Pull down resourceBundles containing remote resources and process the resources contained inside. When that is done,
119  * the resources are injected into the current (in-memory) Maven project, making them available to the process-resources
120  * phase.
121  * </p>
122  * <p>
123  * Resources that end in ".vm" are treated as Velocity templates. For those, the ".vm" is stripped off for the final
124  * artifact name and it's fed through Velocity to have properties expanded, conditions processed, etc...
125  * </p>
126  * <p/>
127  * Resources that don't end in ".vm" are copied "as is".
128  */
129 // NOTE: Removed the following in favor of maven-artifact-resolver library, for MRRESOURCES-41
130 // If I leave this intact, interdependent projects within the reactor that haven't been built
131 // (remember, this runs in the generate-resources phase) will cause the build to fail.
132 //
133 // @requiresDependencyResolution test
134 @Mojo( name = "process", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true )
135 public class ProcessRemoteResourcesMojo
136     extends AbstractMojo
137     implements LogChute
138 {
139 
140     private static final String TEMPLATE_SUFFIX = ".vm";
141 
142     /**
143      * <p>
144      * In cases where a local resource overrides one from a remote resource bundle, that resource should be filtered if
145      * the resource set specifies it. In those cases, this parameter defines the list of delimiters for filterable
146      * expressions. These delimiters are specified in the form 'beginToken*endToken'. If no '*' is given, the delimiter
147      * is assumed to be the same for start and end.
148      * </p>
149      * <p>
150      * So, the default filtering delimiters might be specified as:
151      * </p>
152      * 
153      * <pre>
154      * &lt;delimiters&gt;
155      *   &lt;delimiter&gt;${*}&lt/delimiter&gt;
156      *   &lt;delimiter&gt;@&lt/delimiter&gt;
157      * &lt;/delimiters&gt;
158      * </pre>
159      * <p/>
160      * Since the '@' delimiter is the same on both ends, we don't need to specify '@*@' (though we can).
161      *
162      * @since 1.1
163      */
164     @Parameter
165     protected List<String> filterDelimiters;
166 
167     /**
168      * @since 1.1
169      */
170     @Parameter( defaultValue = "true" )
171     protected boolean useDefaultFilterDelimiters;
172 
173     /**
174      * If true, only generate resources in the directory of the root project in a multimodule build.
175      * Dependencies from all modules will be aggregated before resource-generation takes place.
176      *
177      * @since 1.1
178      */
179     @Parameter( defaultValue = "false" )
180     protected boolean runOnlyAtExecutionRoot;
181 
182     /**
183      * Used for calculation of execution-root for {@link ProcessRemoteResourcesMojo#runOnlyAtExecutionRoot}.
184      */
185     @Parameter( defaultValue = "${basedir}", readonly = true, required = true )
186     protected File basedir;
187 
188     /**
189      * The character encoding scheme to be applied when filtering resources.
190      */
191     @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
192     protected String encoding;
193 
194     /**
195      * The local repository taken from Maven's runtime. Typically <code>$HOME/.m2/repository</code>.
196      */
197     @Parameter( defaultValue = "${localRepository}", readonly = true, required = true )
198     private ArtifactRepository localRepository;
199 
200     /**
201      * List of Remote Repositories used by the resolver.
202      */
203     @Parameter( defaultValue = "${project.remoteArtifactRepositories}", readonly = true, required = true )
204     private List<ArtifactRepository> remoteArtifactRepositories;
205 
206     /**
207      * The current Maven project.
208      */
209     @Parameter( defaultValue = "${project}", readonly = true, required = true )
210     private MavenProject project;
211 
212     /**
213      * The directory where processed resources will be placed for packaging.
214      */
215     @Parameter( defaultValue = "${project.build.directory}/maven-shared-archive-resources" )
216     private File outputDirectory;
217 
218     /**
219      * The directory containing extra information appended to the generated resources.
220      */
221     @Parameter( defaultValue = "${basedir}/src/main/appended-resources" )
222     private File appendedResourcesDirectory;
223 
224     /**
225      * Supplemental model data. Useful when processing
226      * artifacts with incomplete POM metadata.
227      * <p/>
228      * By default, this Mojo looks for supplemental model data in the file
229      * "<code>${appendedResourcesDirectory}/supplemental-models.xml</code>".
230      *
231      * @since 1.0-alpha-5
232      */
233     @Parameter
234     private String[] supplementalModels;
235 
236     /**
237      * List of artifacts that are added to the search path when looking
238      * for supplementalModels, expressed with <code>groupId:artifactId:version[:type[:classifier]]</code> format.
239      *
240      * @since 1.1
241      */
242     @Parameter
243     private List<String> supplementalModelArtifacts;
244 
245     /**
246      * Map of artifacts to supplemental project object models.
247      */
248     private Map<String, Model> supplementModels;
249 
250     /**
251      * Merges supplemental data model with artifact metadata. Useful when processing artifacts with
252      * incomplete POM metadata.
253      */
254     @Component
255     private ModelInheritanceAssembler inheritanceAssembler;
256 
257     /**
258      * The resource bundles that will be retrieved and processed,
259      * expressed with <code>groupId:artifactId:version[:type[:classifier]]</code> format.
260      */
261     @Parameter( required = true )
262     private List<String> resourceBundles;
263 
264     /**
265      * Skip remote-resource processing
266      *
267      * @since 1.0-alpha-5
268      */
269     @Parameter( property = "remoteresources.skip", defaultValue = "false" )
270     private boolean skip;
271 
272     /**
273      * Attaches the resources to the main build of the project as a resource directory.
274      *
275      * @since 1.5
276      */
277     @Parameter( defaultValue = "true", property = "attachToMain" )
278     private boolean attachToMain;
279 
280     /**
281      * Attaches the resources to the test build of the project as a resource directory.
282      *
283      * @since 1.5
284      */
285     @Parameter( defaultValue = "true", property = "attachToTest" )
286     private boolean attachToTest;
287 
288     /**
289      * Additional properties to be passed to Velocity.
290      * <p/>
291      * Several properties are automatically added:<ul>
292      * <li><code>project</code> - the current MavenProject </li>
293      * <li><code>projects</code> - the list of dependency projects</li>
294      * <li><code>projectsSortedByOrganization</code> - the list of dependency projects sorted by organization</li>
295      * <li><code>projectTimespan</code> - the timespan of the current project (requires inceptionYear in pom)</li>
296      * <li><code>locator</code> - the ResourceManager that can be used to retrieve additional resources</li>
297      * </ul>
298      * See <a
299      * href="https://maven.apache.org/ref/current/maven-project/apidocs/org/apache/maven/project/MavenProject.html"> the
300      * javadoc for MavenProject</a> for information about the properties on the MavenProject.
301      */
302     @Parameter
303     private Map<String, Object> properties = new HashMap<>();
304 
305     /**
306      * Whether to include properties defined in the project when filtering resources.
307      *
308      * @since 1.2
309      */
310     @Parameter( defaultValue = "false" )
311     protected boolean includeProjectProperties = false;
312 
313     /**
314      * When the result of velocity transformation fits in memory, it is compared with the actual contents on disk
315      * to eliminate unnecessary destination file overwrite. This improves build times since further build steps
316      * typically rely on the modification date.
317      *
318      * @since 1.6
319      */
320     @Parameter( defaultValue = "5242880" )
321     protected int velocityFilterInMemoryThreshold = 5 * 1024 * 1024;
322 
323     /**
324      * The list of resources defined for the project.
325      */
326     @Parameter( defaultValue = "${project.resources}", readonly = true, required = true )
327     private List<Resource> resources;
328 
329     /**
330      * Artifact Resolver, needed to resolve and download the {@code resourceBundles}.
331      */
332     @Component
333     private ArtifactResolver artifactResolver;
334 
335     /**
336      * Filtering support, for local resources that override those in the remote bundle.
337      */
338     @Component
339     private MavenFileFilter fileFilter;
340 
341     /**
342      * Artifact factory, needed to create artifacts.
343      */
344     @Component
345     private ArtifactFactory artifactFactory;
346 
347     /**
348      * The Maven session.
349      */
350     @Parameter( defaultValue = "${session}", readonly = true, required = true )
351     private MavenSession mavenSession;
352 
353     /**
354      * ProjectBuilder, needed to create projects from the artifacts.
355      */
356     @Component( role = MavenProjectBuilder.class )
357     private MavenProjectBuilder mavenProjectBuilder;
358 
359     /**
360      */
361     @Component
362     private ResourceManager locator;
363 
364     /**
365      * Scope to include. An Empty string indicates all scopes (default is "runtime").
366      *
367      * @since 1.0
368      */
369     @Parameter( property = "includeScope", defaultValue = "runtime" )
370     protected String includeScope;
371 
372     /**
373      * Scope to exclude. An Empty string indicates no scopes (default).
374      *
375      * @since 1.0
376      */
377     @Parameter( property = "excludeScope", defaultValue = "" )
378     protected String excludeScope;
379 
380     /**
381      * When resolving project dependencies, specify the scopes to include.
382      * The default is the same as "includeScope" if there are no exclude scopes set.
383      * Otherwise, it defaults to "test" to grab all the dependencies so the
384      * exclude filters can filter out what is not needed.
385      * 
386      * @since 1.5
387      */
388     @Parameter
389     private String[] resolveScopes;
390 
391     /**
392      * Comma separated list of Artifact names too exclude.
393      *
394      * @since 1.0
395      */
396     @Parameter( property = "excludeArtifactIds", defaultValue = "" )
397     protected String excludeArtifactIds;
398 
399     /**
400      * Comma separated list of Artifact names to include.
401      *
402      * @since 1.0
403      */
404     @Parameter( property = "includeArtifactIds", defaultValue = "" )
405     protected String includeArtifactIds;
406 
407     /**
408      * Comma separated list of GroupId Names to exclude.
409      *
410      * @since 1.0
411      */
412     @Parameter( property = "excludeGroupIds", defaultValue = "" )
413     protected String excludeGroupIds;
414 
415     /**
416      * Comma separated list of GroupIds to include.
417      *
418      * @since 1.0
419      */
420     @Parameter( property = "includeGroupIds", defaultValue = "" )
421     protected String includeGroupIds;
422 
423     /**
424      * If we should exclude transitive dependencies
425      *
426      * @since 1.0
427      */
428     @Parameter( property = "excludeTransitive", defaultValue = "false" )
429     protected boolean excludeTransitive;
430 
431     /**
432      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
433      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
434      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
435      */
436     @Parameter( defaultValue = "${project.build.outputTimestamp}" )
437     private String outputTimestamp;
438 
439     /**
440      */
441     @Component( hint = "default" )
442     protected ProjectDependenciesResolver dependencyResolver;
443 
444     private VelocityEngine velocity;
445 
446     @Override
447     @SuppressWarnings( "unchecked" )
448     public void execute()
449         throws MojoExecutionException
450     {
451         if ( skip )
452         {
453             getLog().info( "Skipping remote resources execution." );
454             return;
455         }
456 
457         if ( StringUtils.isEmpty( encoding ) )
458         {
459             getLog().warn( "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
460                                + ", i.e. build is platform dependent!" );
461         }
462 
463         if ( runOnlyAtExecutionRoot && !project.isExecutionRoot() )
464         {
465             getLog().info( "Skipping remote-resource generation in this project because it's not the Execution Root" );
466             return;
467         }
468 
469         if ( resolveScopes == null )
470         {
471             resolveScopes =
472                 new String[] { StringUtils.isEmpty( excludeScope ) ? this.includeScope : Artifact.SCOPE_TEST };
473         }
474 
475         if ( supplementalModels == null )
476         {
477             File sups = new File( appendedResourcesDirectory, "supplemental-models.xml" );
478             if ( sups.exists() )
479             {
480                 try
481                 {
482                     supplementalModels = new String[] { sups.toURI().toURL().toString() };
483                 }
484                 catch ( MalformedURLException e )
485                 {
486                     // ignore
487                     getLog().debug( "URL issue with supplemental-models.xml: " + e.toString() );
488                 }
489             }
490         }
491 
492         configureLocator();
493 
494         if ( includeProjectProperties )
495         {
496             final Properties projectProperties = project.getProperties();
497             for ( Object key : projectProperties.keySet() )
498             {
499                 properties.put( key.toString(), projectProperties.get( key ).toString() );
500             }
501         }
502 
503         ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
504         try
505         {
506             validate();
507 
508             List<File> resourceBundleArtifacts = downloadBundles( resourceBundles );
509             supplementModels = loadSupplements( supplementalModels );
510 
511             ClassLoader classLoader = initalizeClassloader( resourceBundleArtifacts );
512 
513             Thread.currentThread().setContextClassLoader( classLoader );
514 
515             velocity = new VelocityEngine();
516             velocity.setProperty( RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, this );
517             velocity.setProperty( "resource.loader", "classpath" );
518             velocity.setProperty( "classpath.resource.loader.class", ClasspathResourceLoader.class.getName() );
519             velocity.init();
520 
521             VelocityContext context = buildVelocityContext( properties );
522 
523             processResourceBundles( classLoader, context );
524 
525             if ( outputDirectory.exists() )
526             {
527                 // ----------------------------------------------------------------------------
528                 // Push our newly generated resources directory into the MavenProject so that
529                 // these resources can be picked up by the process-resources phase.
530                 // ----------------------------------------------------------------------------
531                 Resource resource = new Resource();
532                 resource.setDirectory( outputDirectory.getAbsolutePath() );
533                 // MRRESOURCES-61 handle main and test resources separately
534                 if ( attachToMain )
535                 {
536                     project.getResources().add( resource );
537                 }
538                 if ( attachToTest )
539                 {
540                     project.getTestResources().add( resource );
541                 }
542 
543                 // ----------------------------------------------------------------------------
544                 // Write out archiver dot file
545                 // ----------------------------------------------------------------------------
546                 try
547                 {
548                     File dotFile = new File( project.getBuild().getDirectory(), ".plxarc" );
549                     FileUtils.mkdir( dotFile.getParentFile().getAbsolutePath() );
550                     FileUtils.fileWrite( dotFile.getAbsolutePath(), outputDirectory.getName() );
551                 }
552                 catch ( IOException e )
553                 {
554                     throw new MojoExecutionException( "Error creating dot file for archiving instructions.", e );
555                 }
556             }
557         }
558         finally
559         {
560             Thread.currentThread().setContextClassLoader( origLoader );
561         }
562     }
563 
564     private void configureLocator()
565         throws MojoExecutionException
566     {
567         if ( supplementalModelArtifacts != null && !supplementalModelArtifacts.isEmpty() )
568         {
569             List<File> artifacts = downloadBundles( supplementalModelArtifacts );
570 
571             for ( File artifact : artifacts )
572             {
573                 if ( artifact.isDirectory() )
574                 {
575                     locator.addSearchPath( FileResourceLoader.ID, artifact.getAbsolutePath() );
576                 }
577                 else
578                 {
579                     try
580                     {
581                         locator.addSearchPath( "jar", "jar:" + artifact.toURI().toURL().toExternalForm() );
582                     }
583                     catch ( MalformedURLException e )
584                     {
585                         throw new MojoExecutionException( "Could not use jar " + artifact.getAbsolutePath(), e );
586                     }
587                 }
588             }
589 
590         }
591 
592         locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
593         if ( appendedResourcesDirectory != null )
594         {
595             locator.addSearchPath( FileResourceLoader.ID, appendedResourcesDirectory.getAbsolutePath() );
596         }
597         locator.addSearchPath( "url", "" );
598         locator.setOutputDirectory( new File( project.getBuild().getDirectory() ) );
599     }
600 
601     @SuppressWarnings( "unchecked" )
602     protected List<MavenProject> getProjects()
603     {
604         List<MavenProject> projects = new ArrayList<>();
605 
606         // add filters in well known order, least specific to most specific
607         FilterArtifacts filter = new FilterArtifacts();
608 
609         Set<Artifact> artifacts = resolveProjectArtifacts();
610         if ( this.excludeTransitive )
611         {
612             Set<Artifact> depArtifacts;
613             if ( runOnlyAtExecutionRoot )
614             {
615                 depArtifacts = aggregateProjectDependencyArtifacts();
616             }
617             else
618             {
619                 depArtifacts = project.getDependencyArtifacts();
620             }
621             filter.addFilter( new ProjectTransitivityFilter( depArtifacts, true ) );
622         }
623 
624         filter.addFilter( new ScopeFilter( this.includeScope, this.excludeScope ) );
625         filter.addFilter( new GroupIdFilter( this.includeGroupIds, this.excludeGroupIds ) );
626         filter.addFilter( new ArtifactIdFilter( this.includeArtifactIds, this.excludeArtifactIds ) );
627 
628         // perform filtering
629         try
630         {
631             artifacts = filter.filter( artifacts );
632         }
633         catch ( ArtifactFilterException e )
634         {
635             throw new IllegalStateException( e.getMessage(), e );
636         }
637 
638         for ( Artifact artifact : artifacts )
639         {
640             try
641             {
642                 List<ArtifactRepository> remoteRepo = remoteArtifactRepositories;
643                 if ( artifact.isSnapshot() )
644                 {
645                     VersionRange rng = VersionRange.createFromVersion( artifact.getBaseVersion() );
646                     artifact =
647                         artifactFactory.createDependencyArtifact( artifact.getGroupId(), artifact.getArtifactId(), rng,
648                                                                   artifact.getType(), artifact.getClassifier(),
649                                                                   artifact.getScope(), null, artifact.isOptional() );
650                 }
651 
652                 getLog().debug( "Building project for " + artifact );
653                 MavenProject p;
654                 try
655                 {
656                     p = mavenProjectBuilder.buildFromRepository( artifact, remoteRepo, localRepository );
657                 }
658                 catch ( InvalidProjectModelException e )
659                 {
660                     getLog().warn( "Invalid project model for artifact [" + artifact.getArtifactId() + ":"
661                                        + artifact.getGroupId() + ":" + artifact.getVersion() + "]. "
662                                        + "It will be ignored by the remote resources Mojo." );
663                     continue;
664                 }
665 
666                 String supplementKey =
667                     generateSupplementMapKey( p.getModel().getGroupId(), p.getModel().getArtifactId() );
668 
669                 if ( supplementModels.containsKey( supplementKey ) )
670                 {
671                     Model mergedModel = mergeModels( p.getModel(), supplementModels.get( supplementKey ) );
672                     MavenProject mergedProject = new MavenProject( mergedModel );
673                     projects.add( mergedProject );
674                     mergedProject.setArtifact( artifact );
675                     mergedProject.setVersion( artifact.getVersion() );
676                     getLog().debug( "Adding project with groupId [" + mergedProject.getGroupId() + "] (supplemented)" );
677                 }
678                 else
679                 {
680                     projects.add( p );
681                     getLog().debug( "Adding project with groupId [" + p.getGroupId() + "]" );
682                 }
683             }
684             catch ( ProjectBuildingException e )
685             {
686                 throw new IllegalStateException( e.getMessage(), e );
687             }
688         }
689         Collections.sort( projects, new ProjectComparator() );
690         return projects;
691     }
692 
693     private Set<Artifact> resolveProjectArtifacts()
694     {
695         try
696         {
697             if ( runOnlyAtExecutionRoot )
698             {
699                 List<MavenProject> projects = mavenSession.getSortedProjects();
700                 return dependencyResolver.resolve( projects, Arrays.asList( resolveScopes ), mavenSession );
701             }
702             else
703             {
704                 return dependencyResolver.resolve( project, Arrays.asList( resolveScopes ), mavenSession );
705             }
706         }
707         catch ( ArtifactResolutionException e )
708         {
709             throw new IllegalStateException( "Failed to resolve dependencies for one or more projects in the reactor. "
710                 + "Reason: " + e.getMessage(), e );
711         }
712         catch ( ArtifactNotFoundException e )
713         {
714             throw new IllegalStateException( "Failed to resolve dependencies for one or more projects in the reactor. "
715                 + "Reason: " + e.getMessage(), e );
716         }
717     }
718 
719     @SuppressWarnings( "unchecked" )
720     private Set<Artifact> aggregateProjectDependencyArtifacts()
721     {
722         Set<Artifact> artifacts = new LinkedHashSet<>();
723 
724         List<MavenProject> projects = mavenSession.getSortedProjects();
725         for ( MavenProject p : projects )
726         {
727             if ( p.getDependencyArtifacts() == null )
728             {
729                 try
730                 {
731                     Set<Artifact> depArtifacts = p.createArtifacts( artifactFactory, null, null );
732 
733                     if ( depArtifacts != null && !depArtifacts.isEmpty() )
734                     {
735                         for ( Artifact artifact : depArtifacts )
736                         {
737                             if ( artifact.getVersion() == null && artifact.getVersionRange() != null )
738                             {
739                                 // Version is required for equality comparison between artifacts,
740                                 // but it is not needed for our purposes of filtering out
741                                 // transitive dependencies (which requires only groupId/artifactId).
742                                 // Therefore set an arbitrary version if missing.
743                                 artifact.setResolvedVersion( Artifact.LATEST_VERSION );
744                             }
745                             artifacts.add( artifact );
746                         }
747                     }
748                 }
749                 catch ( InvalidDependencyVersionException e )
750                 {
751                     throw new IllegalStateException( "Failed to create dependency artifacts for: " + p.getId()
752                         + ". Reason: " + e.getMessage(), e );
753                 }
754             }
755         }
756 
757         return artifacts;
758     }
759 
760     protected Map<Organization, List<MavenProject>> getProjectsSortedByOrganization( List<MavenProject> projects )
761     {
762         Map<Organization, List<MavenProject>> organizations =
763             new TreeMap<>( new OrganizationComparator() );
764         List<MavenProject> unknownOrganization = new ArrayList<>();
765 
766         for ( MavenProject p : projects )
767         {
768             if ( p.getOrganization() != null && StringUtils.isNotEmpty( p.getOrganization().getName() ) )
769             {
770                 List<MavenProject> sortedProjects = organizations.get( p.getOrganization() );
771                 if ( sortedProjects == null )
772                 {
773                     sortedProjects = new ArrayList<>();
774                 }
775                 sortedProjects.add( p );
776 
777                 organizations.put( p.getOrganization(), sortedProjects );
778             }
779             else
780             {
781                 unknownOrganization.add( p );
782             }
783         }
784         if ( !unknownOrganization.isEmpty() )
785         {
786             Organization unknownOrg = new Organization();
787             unknownOrg.setName( "an unknown organization" );
788             organizations.put( unknownOrg, unknownOrganization );
789         }
790 
791         return organizations;
792     }
793 
794     protected boolean copyResourceIfExists( File file, String relFileName, VelocityContext context )
795         throws IOException, MojoExecutionException
796     {
797         for ( Resource resource : resources )
798         {
799             File resourceDirectory = new File( resource.getDirectory() );
800 
801             if ( !resourceDirectory.exists() )
802             {
803                 continue;
804             }
805 
806             // TODO - really should use the resource includes/excludes and name mapping
807             File source = new File( resourceDirectory, relFileName );
808             File templateSource = new File( resourceDirectory, relFileName + TEMPLATE_SUFFIX );
809 
810             if ( !source.exists() && templateSource.exists() )
811             {
812                 source = templateSource;
813             }
814 
815             if ( source.exists() && !source.equals( file ) )
816             {
817                 if ( source == templateSource )
818                 {
819                     try ( DeferredFileOutputStream os = 
820                                     new DeferredFileOutputStream( velocityFilterInMemoryThreshold, file ) )
821                     {
822                         try ( Reader reader = getReader( source ); Writer writer = getWriter( os ) )
823                         {
824                             velocity.evaluate( context, writer, "", reader );
825                         }
826                         catch ( ParseErrorException | MethodInvocationException | ResourceNotFoundException e )
827                         {
828                             throw new MojoExecutionException( "Error rendering velocity resource: " + source, e );
829                         }
830                         fileWriteIfDiffers( os );
831                     }
832                 }
833                 else if ( resource.isFiltering() )
834                 {
835 
836                     MavenFileFilterRequest req = setupRequest( resource, source, file );
837 
838                     try
839                     {
840                         fileFilter.copyFile( req );
841                     }
842                     catch ( MavenFilteringException e )
843                     {
844                         throw new MojoExecutionException( "Error filtering resource: " + source, e );
845                     }
846                 }
847                 else
848                 {
849                     FileUtils.copyFile( source, file );
850                 }
851 
852                 // exclude the original (so eclipse doesn't complain about duplicate resources)
853                 resource.addExclude( relFileName );
854 
855                 return true;
856             }
857 
858         }
859         return false;
860     }
861 
862     private Reader getReader( File source ) throws IOException
863     {
864         if ( encoding != null )
865         {
866             return new InputStreamReader( new FileInputStream( source ), encoding );
867         }
868         else
869         {
870             return ReaderFactory.newPlatformReader( source );
871         }
872     }
873     
874     private Writer getWriter( OutputStream os ) throws IOException
875     {
876         if ( encoding != null )
877         {
878             return new OutputStreamWriter( os, encoding );
879         }
880         else
881         {
882             return WriterFactory.newPlatformWriter( os );
883         }
884     }
885 
886     /**
887      * If the transformation result fits in memory and the destination file already exists
888      * then both are compared.
889      * <p>If destination file is byte-by-byte equal, then it is not overwritten.
890      * This improves subsequent compilation times since upstream plugins property see that
891      * the resource was not modified.
892      * <p>Note: the method should be called after {@link org.apache.commons.io.output.DeferredFileOutputStream#close}
893      *
894      * @param outStream Deferred stream
895      * @throws IOException
896      */
897     private void fileWriteIfDiffers( DeferredFileOutputStream outStream )
898         throws IOException
899     {
900         File file = outStream.getFile();
901         if ( outStream.isThresholdExceeded() )
902         {
903             getLog().info( "File " + file + " was overwritten due to content limit threshold "
904                     + outStream.getThreshold() + " reached" );
905             return;
906         }
907         boolean needOverwrite = true;
908 
909         if ( file.exists() )
910         {
911             try ( InputStream is = new FileInputStream( file ) )
912             {
913                 final InputStream newContents = new ByteArrayInputStream( outStream.getData() );
914                 needOverwrite = !IOUtil.contentEquals( is, newContents );
915                 if ( getLog().isDebugEnabled() )
916                 {
917                     getLog().debug( "File " + file + " contents "
918                                         + ( needOverwrite ? "differs" : "does not differ" ) );
919                 }
920             }
921         }
922 
923         if ( !needOverwrite )
924         {
925             getLog().debug( "File " + file + " is up to date" );
926             return;
927         }
928         getLog().debug( "Writing " + file );
929         
930         try ( OutputStream os = new FileOutputStream( file ) )
931         {
932             outStream.writeTo( os );
933         }
934     }
935 
936     private MavenFileFilterRequest setupRequest( Resource resource, File source, File file )
937     {
938         MavenFileFilterRequest req = new MavenFileFilterRequest();
939         req.setFrom( source );
940         req.setTo( file );
941         req.setFiltering( resource.isFiltering() );
942 
943         req.setMavenProject( project );
944         req.setMavenSession( mavenSession );
945         req.setInjectProjectBuildFilters( true );
946 
947         if ( encoding != null )
948         {
949             req.setEncoding( encoding );
950         }
951 
952         if ( filterDelimiters != null && !filterDelimiters.isEmpty() )
953         {
954             LinkedHashSet<String> delims = new LinkedHashSet<>();
955             if ( useDefaultFilterDelimiters )
956             {
957                 delims.addAll( req.getDelimiters() );
958             }
959 
960             for ( String delim : filterDelimiters )
961             {
962                 if ( delim == null )
963                 {
964                     delims.add( "${*}" );
965                 }
966                 else
967                 {
968                     delims.add( delim );
969                 }
970             }
971 
972             req.setDelimiters( delims );
973         }
974 
975         return req;
976     }
977 
978     protected void validate()
979         throws MojoExecutionException
980     {
981         int bundleCount = 1;
982 
983         for ( String artifactDescriptor : resourceBundles )
984         {
985             // groupId:artifactId:version, groupId:artifactId:version:type
986             // or groupId:artifactId:version:type:classifier
987             String[] s = StringUtils.split( artifactDescriptor, ":" );
988 
989             if ( s.length < 3 || s.length > 5 )
990             {
991                 String position;
992 
993                 if ( bundleCount == 1 )
994                 {
995                     position = "1st";
996                 }
997                 else if ( bundleCount == 2 )
998                 {
999                     position = "2nd";
1000                 }
1001                 else if ( bundleCount == 3 )
1002                 {
1003                     position = "3rd";
1004                 }
1005                 else
1006                 {
1007                     position = bundleCount + "th";
1008                 }
1009 
1010                 throw new MojoExecutionException( "The " + position
1011                     + " resource bundle configured must specify a groupId, artifactId, "
1012                     + " version and, optionally, type and classifier for a remote resource bundle. "
1013                     + "Must be of the form <resourceBundle>groupId:artifactId:version</resourceBundle>, "
1014                     + "<resourceBundle>groupId:artifactId:version:type</resourceBundle> or "
1015                     + "<resourceBundle>groupId:artifactId:version:type:classifier</resourceBundle>" );
1016             }
1017 
1018             bundleCount++;
1019         }
1020 
1021     }
1022 
1023     private static final String KEY_PROJECTS = "projects";
1024     private static final String KEY_PROJECTS_ORGS = "projectsSortedByOrganization";
1025 
1026     protected VelocityContext buildVelocityContext( Map<String, Object> properties )
1027         throws MojoExecutionException
1028     {
1029         // the following properties are expensive to calculate, so we provide them lazily
1030         VelocityContext context = new VelocityContext( properties )
1031         {
1032             @Override
1033             public Object internalGet( String key )
1034             {
1035                 Object result = super.internalGet( key );
1036                 if ( result == null && key != null && key.startsWith( KEY_PROJECTS ) && containsKey( key ) )
1037                 {
1038                     // calculate and put projects* properties
1039                     List<MavenProject> projects = getProjects();
1040                     put( KEY_PROJECTS, projects );
1041                     put( KEY_PROJECTS_ORGS, getProjectsSortedByOrganization( projects ) );
1042                     return super.internalGet( key );
1043                 }
1044                 return result;
1045             }
1046         };
1047         // to have a consistent getKeys()/containsKey() behaviour, keys must be present from the start
1048         context.put( KEY_PROJECTS, null );
1049         context.put( KEY_PROJECTS_ORGS, null );
1050         // the following properties are cheap to calculate, so we provide them eagerly
1051 
1052         // Reproducible Builds: try to use reproducible output timestamp 
1053         MavenArchiver archiver = new MavenArchiver();
1054         Date outputDate = archiver.parseOutputTimestamp( outputTimestamp );
1055 
1056         String inceptionYear = project.getInceptionYear();
1057         String year = new SimpleDateFormat( "yyyy" ).format( ( outputDate == null ) ? new Date() : outputDate );
1058 
1059         if ( StringUtils.isEmpty( inceptionYear ) )
1060         {
1061             if ( getLog().isDebugEnabled() )
1062             {
1063                 getLog().debug( "inceptionYear not specified, defaulting to " + year );
1064             }
1065 
1066             inceptionYear = year;
1067         }
1068         context.put( "project", project );
1069         context.put( "presentYear", year );
1070         context.put( "locator", locator );
1071 
1072         if ( inceptionYear.equals( year ) )
1073         {
1074             context.put( "projectTimespan", year );
1075         }
1076         else
1077         {
1078             context.put( "projectTimespan", inceptionYear + "-" + year );
1079         }
1080         return context;
1081     }
1082 
1083     private List<File> downloadBundles( List<String> bundles )
1084         throws MojoExecutionException
1085     {
1086         List<File> bundleArtifacts = new ArrayList<>();
1087 
1088         try
1089         {
1090             for ( String artifactDescriptor : bundles )
1091             {
1092                 getLog().info( "Preparing remote bundle " + artifactDescriptor );
1093                 // groupId:artifactId:version[:type[:classifier]]
1094                 String[] s = artifactDescriptor.split( ":" );
1095 
1096                 File artifactFile = null;
1097                 // check if the artifact is part of the reactor
1098                 if ( mavenSession != null )
1099                 {
1100                     List<MavenProject> list = mavenSession.getSortedProjects();
1101                     for ( MavenProject p : list )
1102                     {
1103                         if ( s[0].equals( p.getGroupId() ) && s[1].equals( p.getArtifactId() )
1104                             && s[2].equals( p.getVersion() ) )
1105                         {
1106                             if ( s.length >= 4 && "test-jar".equals( s[3] ) )
1107                             {
1108                                 artifactFile = new File( p.getBuild().getTestOutputDirectory() );
1109                             }
1110                             else
1111                             {
1112                                 artifactFile = new File( p.getBuild().getOutputDirectory() );
1113                             }
1114                         }
1115                     }
1116                 }
1117                 if ( artifactFile == null || !artifactFile.exists() )
1118                 {
1119                     String type = ( s.length >= 4 ? s[3] : "jar" );
1120                     String classifier = ( s.length == 5 ? s[4] : null );
1121                     Artifact artifact =
1122                         artifactFactory.createDependencyArtifact( s[0], s[1], VersionRange.createFromVersion( s[2] ),
1123                                                                   type, classifier, Artifact.SCOPE_RUNTIME );
1124 
1125                     artifactResolver.resolve( artifact, remoteArtifactRepositories, localRepository );
1126 
1127                     artifactFile = artifact.getFile();
1128                 }
1129                 bundleArtifacts.add( artifactFile );
1130             }
1131         }
1132         catch ( ArtifactResolutionException e )
1133         {
1134             throw new MojoExecutionException( "Error downloading resources archive.", e );
1135         }
1136         catch ( ArtifactNotFoundException e )
1137         {
1138             throw new MojoExecutionException( "Resources archive cannot be found.", e );
1139         }
1140 
1141         return bundleArtifacts;
1142     }
1143 
1144     private ClassLoader initalizeClassloader( List<File> artifacts )
1145         throws MojoExecutionException
1146     {
1147         RemoteResourcesClassLoader cl = new RemoteResourcesClassLoader( null );
1148         try
1149         {
1150             for ( File artifact : artifacts )
1151             {
1152                 cl.addURL( artifact.toURI().toURL() );
1153             }
1154             return cl;
1155         }
1156         catch ( MalformedURLException e )
1157         {
1158             throw new MojoExecutionException( "Unable to configure resources classloader: " + e.getMessage(), e );
1159         }
1160     }
1161 
1162     protected void processResourceBundles( ClassLoader classLoader, VelocityContext context )
1163         throws MojoExecutionException
1164     {
1165         List<Map.Entry<String, RemoteResourcesBundle>> remoteResources =
1166             new ArrayList<>();
1167         int bundleCount = 0;
1168         int resourceCount = 0;
1169 
1170         // list remote resources form bundles
1171         try
1172         {
1173             RemoteResourcesBundleXpp3Reader bundleReader = new RemoteResourcesBundleXpp3Reader();
1174 
1175             for ( Enumeration<URL> e =
1176                 classLoader.getResources( BundleRemoteResourcesMojo.RESOURCES_MANIFEST ); e.hasMoreElements(); )
1177             {
1178                 URL url = e.nextElement();
1179                 bundleCount++;
1180                 getLog().debug( "processResourceBundle on bundle#" + bundleCount + " " + url );
1181 
1182                 RemoteResourcesBundle bundle;
1183                 
1184                 try ( InputStream in = url.openStream() )
1185                 {
1186                     bundle = bundleReader.read( in );
1187                 }
1188 
1189                 int n = 0;
1190                 for ( String bundleResource : bundle.getRemoteResources() )
1191                 {
1192                     n++;
1193                     resourceCount++;
1194                     getLog().debug( "bundle#" + bundleCount + " resource#" + n + " " + bundleResource );
1195                     remoteResources.add( new AbstractMap.SimpleEntry<>( bundleResource,
1196                                                                                                      bundle ) );
1197                 }
1198             }
1199         }
1200         catch ( IOException ioe )
1201         {
1202             throw new MojoExecutionException( "Error finding remote resources manifests", ioe );
1203         }
1204         catch ( XmlPullParserException xppe )
1205         {
1206             throw new MojoExecutionException( "Error parsing remote resource bundle descriptor.", xppe );
1207         }
1208 
1209         getLog().info( "Copying " + resourceCount + " resource" + ( ( resourceCount > 1 ) ? "s" : "" ) + " from "
1210             + bundleCount + " bundle" + ( ( bundleCount > 1 ) ? "s" : "" ) + "." );
1211 
1212         String velocityResource = null;
1213         try
1214         {
1215 
1216             for ( Map.Entry<String, RemoteResourcesBundle> entry : remoteResources )
1217             {
1218                 String bundleResource = entry.getKey();
1219                 RemoteResourcesBundle bundle = entry.getValue();
1220 
1221                 String projectResource = bundleResource;
1222 
1223                 boolean doVelocity = false;
1224                 if ( projectResource.endsWith( TEMPLATE_SUFFIX ) )
1225                 {
1226                     projectResource = projectResource.substring( 0, projectResource.length() - 3 );
1227                     velocityResource = bundleResource;
1228                     doVelocity = true;
1229                 }
1230 
1231                 // Don't overwrite resource that are already being provided.
1232 
1233                 File f = new File( outputDirectory, projectResource );
1234 
1235                 FileUtils.mkdir( f.getParentFile().getAbsolutePath() );
1236 
1237                 if ( !copyResourceIfExists( f, projectResource, context ) )
1238                 {
1239                     if ( doVelocity )
1240                     {
1241                         try ( DeferredFileOutputStream os =
1242                                         new DeferredFileOutputStream( velocityFilterInMemoryThreshold, f ) )
1243                         {
1244                             try ( Writer writer = bundle.getSourceEncoding() == null ? new OutputStreamWriter( os )
1245                                             : new OutputStreamWriter( os, bundle.getSourceEncoding() ) )
1246                             {
1247                                 if ( bundle.getSourceEncoding() == null )
1248                                 {
1249                                     // TODO: Is this correct? Shouldn't we behave like the rest of maven and fail
1250                                     // down to JVM default instead ISO-8859-1 ?
1251                                     velocity.mergeTemplate( bundleResource, "ISO-8859-1", context, writer );
1252                                 }
1253                                 else
1254                                 {
1255                                     velocity.mergeTemplate( bundleResource, bundle.getSourceEncoding(), context,
1256                                                             writer );
1257                                 }
1258                             }
1259                             fileWriteIfDiffers( os );
1260                         }
1261                     }
1262                     else
1263                     {
1264                         URL resUrl = classLoader.getResource( bundleResource );
1265                         if ( resUrl != null )
1266                         {
1267                             FileUtils.copyURLToFile( resUrl, f );
1268                         }
1269                     }
1270 
1271                     File appendedResourceFile = new File( appendedResourcesDirectory, projectResource );
1272                     File appendedVmResourceFile = new File( appendedResourcesDirectory, projectResource + ".vm" );
1273 
1274                     if ( appendedResourceFile.exists() )
1275                     {
1276                         getLog().info( "Copying appended resource: " + projectResource );
1277                         try ( InputStream in = new FileInputStream( appendedResourceFile );
1278                               OutputStream out = new FileOutputStream( f, true ) )
1279                         {
1280                             IOUtil.copy( in, out );
1281                         }
1282                         
1283                     }
1284                     else if ( appendedVmResourceFile.exists() )
1285                     {
1286                         getLog().info( "Filtering appended resource: " + projectResource + ".vm" );
1287                         
1288 
1289                         try ( Reader reader = new FileReader( appendedVmResourceFile ); 
1290                               Writer writer = getWriter( bundle, f ) )
1291                         {
1292                             Velocity.init();
1293                             Velocity.evaluate( context, writer, "remote-resources", reader );
1294                         }
1295                     }
1296                 }
1297             }
1298         }
1299         catch ( IOException ioe )
1300         {
1301             throw new MojoExecutionException( "Error reading remote resource", ioe );
1302         }
1303         catch ( VelocityException e )
1304         {
1305             throw new MojoExecutionException( "Error rendering Velocity resource '" + velocityResource + "'", e );
1306         }
1307     }
1308 
1309     private Writer getWriter( RemoteResourcesBundle bundle, File f )
1310         throws IOException, UnsupportedEncodingException, FileNotFoundException
1311     {
1312         Writer writer;
1313         if ( bundle.getSourceEncoding() == null )
1314         {
1315             writer = new PrintWriter( new FileWriter( f, true ) );
1316         }
1317         else
1318         {
1319             writer = new PrintWriter( new OutputStreamWriter( new FileOutputStream( f, true ),
1320                                                               bundle.getSourceEncoding() ) );
1321         }
1322         return writer;
1323     }
1324 
1325     protected Model getSupplement( Xpp3Dom supplementModelXml )
1326         throws MojoExecutionException
1327     {
1328         MavenXpp3Reader modelReader = new MavenXpp3Reader();
1329         Model model = null;
1330 
1331         try
1332         {
1333             model = modelReader.read( new StringReader( supplementModelXml.toString() ) );
1334             String groupId = model.getGroupId();
1335             String artifactId = model.getArtifactId();
1336 
1337             if ( groupId == null || groupId.trim().equals( "" ) )
1338             {
1339                 throw new MojoExecutionException( "Supplemental project XML "
1340                     + "requires that a <groupId> element be present." );
1341             }
1342 
1343             if ( artifactId == null || artifactId.trim().equals( "" ) )
1344             {
1345                 throw new MojoExecutionException( "Supplemental project XML "
1346                     + "requires that a <artifactId> element be present." );
1347             }
1348         }
1349         catch ( IOException e )
1350         {
1351             getLog().warn( "Unable to read supplemental XML: " + e.getMessage(), e );
1352         }
1353         catch ( XmlPullParserException e )
1354         {
1355             getLog().warn( "Unable to parse supplemental XML: " + e.getMessage(), e );
1356         }
1357 
1358         return model;
1359     }
1360 
1361     protected Model mergeModels( Model parent, Model child )
1362     {
1363         inheritanceAssembler.assembleModelInheritance( child, parent );
1364         return child;
1365     }
1366 
1367     private static String generateSupplementMapKey( String groupId, String artifactId )
1368     {
1369         return groupId.trim() + ":" + artifactId.trim();
1370     }
1371 
1372     private Map<String, Model> loadSupplements( String models[] )
1373         throws MojoExecutionException
1374     {
1375         if ( models == null )
1376         {
1377             getLog().debug( "Supplemental data models won't be loaded.  " + "No models specified." );
1378             return Collections.emptyMap();
1379         }
1380 
1381         List<Supplement> supplements = new ArrayList<>();
1382         for ( String set : models )
1383         {
1384             getLog().debug( "Preparing ruleset: " + set );
1385             try
1386             {
1387                 File f = locator.getResourceAsFile( set, getLocationTemp( set ) );
1388 
1389                 if ( null == f || !f.exists() )
1390                 {
1391                     throw new MojoExecutionException( "Cold not resolve " + set );
1392                 }
1393                 if ( !f.canRead() )
1394                 {
1395                     throw new MojoExecutionException( "Supplemental data models won't be loaded. " + "File "
1396                         + f.getAbsolutePath() + " cannot be read, check permissions on the file." );
1397                 }
1398 
1399                 getLog().debug( "Loading supplemental models from " + f.getAbsolutePath() );
1400 
1401                 SupplementalDataModelXpp3Reader reader = new SupplementalDataModelXpp3Reader();
1402                 SupplementalDataModel supplementalModel = reader.read( new FileReader( f ) );
1403                 supplements.addAll( supplementalModel.getSupplement() );
1404             }
1405             catch ( Exception e )
1406             {
1407                 String msg = "Error loading supplemental data models: " + e.getMessage();
1408                 getLog().error( msg, e );
1409                 throw new MojoExecutionException( msg, e );
1410             }
1411         }
1412 
1413         getLog().debug( "Loading supplements complete." );
1414 
1415         Map<String, Model> supplementMap = new HashMap<>();
1416         for ( Supplement sd : supplements )
1417         {
1418             Xpp3Dom dom = (Xpp3Dom) sd.getProject();
1419 
1420             Model m = getSupplement( dom );
1421             supplementMap.put( generateSupplementMapKey( m.getGroupId(), m.getArtifactId() ), m );
1422         }
1423 
1424         return supplementMap;
1425     }
1426 
1427     /**
1428      * Convenience method to get the location of the specified file name.
1429      *
1430      * @param name the name of the file whose location is to be resolved
1431      * @return a String that contains the absolute file name of the file
1432      */
1433     private String getLocationTemp( String name )
1434     {
1435         String loc = name;
1436         if ( loc.indexOf( '/' ) != -1 )
1437         {
1438             loc = loc.substring( loc.lastIndexOf( '/' ) + 1 );
1439         }
1440         if ( loc.indexOf( '\\' ) != -1 )
1441         {
1442             loc = loc.substring( loc.lastIndexOf( '\\' ) + 1 );
1443         }
1444         getLog().debug( "Before: " + name + " After: " + loc );
1445         return loc;
1446     }
1447 
1448     class OrganizationComparator
1449         implements Comparator<Organization>
1450     {
1451         @Override
1452         public int compare( Organization org1, Organization org2 )
1453         {
1454             int i = compareStrings( org1.getName(), org2.getName() );
1455             if ( i == 0 )
1456             {
1457                 i = compareStrings( org1.getUrl(), org2.getUrl() );
1458             }
1459             return i;
1460         }
1461 
1462         public boolean equals( Organization o1, Organization o2 )
1463         {
1464             return compare( o1, o2 ) == 0;
1465         }
1466 
1467         private int compareStrings( String s1, String s2 )
1468         {
1469             if ( s1 == null && s2 == null )
1470             {
1471                 return 0;
1472             }
1473             else if ( s1 == null && s2 != null )
1474             {
1475                 return 1;
1476             }
1477             else if ( s1 != null && s2 == null )
1478             {
1479                 return -1;
1480             }
1481 
1482             return s1.compareToIgnoreCase( s2 );
1483         }
1484     }
1485 
1486     class ProjectComparator
1487         implements Comparator<MavenProject>
1488     {
1489         @Override
1490         public int compare( MavenProject p1, MavenProject p2 )
1491         {
1492             return p1.getArtifact().compareTo( p2.getArtifact() );
1493         }
1494 
1495         public boolean equals( MavenProject p1, MavenProject p2 )
1496         {
1497             return p1.getArtifact().equals( p2.getArtifact() );
1498         }
1499     }
1500 
1501     /* LogChute methods */
1502     @Override
1503     public void init( RuntimeServices rs )
1504         throws Exception
1505     {
1506     }
1507 
1508     @Override
1509     public void log( int level, String message )
1510     {
1511         switch ( level )
1512         {
1513             case LogChute.WARN_ID:
1514                 getLog().warn( message );
1515                 break;
1516             case LogChute.INFO_ID:
1517                 // velocity info messages are too verbose, just consider them as debug messages...
1518                 getLog().debug( message );
1519                 break;
1520             case LogChute.DEBUG_ID:
1521                 getLog().debug( message );
1522                 break;
1523             case LogChute.ERROR_ID:
1524                 getLog().error( message );
1525                 break;
1526             default:
1527                 getLog().debug( message );
1528                 break;
1529         }
1530     }
1531 
1532     @Override
1533     public void log( int level, String message, Throwable t )
1534     {
1535         switch ( level )
1536         {
1537             case LogChute.WARN_ID:
1538                 getLog().warn( message, t );
1539                 break;
1540             case LogChute.INFO_ID:
1541                 // velocity info messages are too verbose, just consider them as debug messages...
1542                 getLog().debug( message, t );
1543                 break;
1544             case LogChute.DEBUG_ID:
1545                 getLog().debug( message, t );
1546                 break;
1547             case LogChute.ERROR_ID:
1548                 getLog().error( message, t );
1549                 break;
1550             default:
1551                 getLog().debug( message, t );
1552                 break;
1553         }
1554     }
1555 
1556     @Override
1557     public boolean isLevelEnabled( int level )
1558     {
1559         return false;
1560     }
1561 
1562 }