View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugin.resources.remote;
20  
21  import java.io.ByteArrayInputStream;
22  import java.io.File;
23  import java.io.FileOutputStream;
24  import java.io.FileReader;
25  import java.io.FileWriter;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.InputStreamReader;
29  import java.io.OutputStream;
30  import java.io.OutputStreamWriter;
31  import java.io.PrintWriter;
32  import java.io.Reader;
33  import java.io.StringReader;
34  import java.io.Writer;
35  import java.net.MalformedURLException;
36  import java.net.URL;
37  import java.nio.file.Files;
38  import java.text.SimpleDateFormat;
39  import java.util.AbstractMap;
40  import java.util.ArrayList;
41  import java.util.Collections;
42  import java.util.Comparator;
43  import java.util.Date;
44  import java.util.Enumeration;
45  import java.util.HashMap;
46  import java.util.LinkedHashSet;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.Properties;
50  import java.util.Set;
51  import java.util.TreeMap;
52  
53  import org.apache.commons.io.output.DeferredFileOutputStream;
54  import org.apache.maven.RepositoryUtils;
55  import org.apache.maven.archiver.MavenArchiver;
56  import org.apache.maven.artifact.Artifact;
57  import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
58  import org.apache.maven.execution.MavenSession;
59  import org.apache.maven.model.Model;
60  import org.apache.maven.model.Organization;
61  import org.apache.maven.model.Resource;
62  import org.apache.maven.model.building.ModelBuildingRequest;
63  import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
64  import org.apache.maven.plugin.AbstractMojo;
65  import org.apache.maven.plugin.MojoExecutionException;
66  import org.apache.maven.plugin.resources.remote.io.xpp3.RemoteResourcesBundleXpp3Reader;
67  import org.apache.maven.plugin.resources.remote.io.xpp3.SupplementalDataModelXpp3Reader;
68  import org.apache.maven.plugins.annotations.Component;
69  import org.apache.maven.plugins.annotations.Parameter;
70  import org.apache.maven.project.DefaultProjectBuildingRequest;
71  import org.apache.maven.project.MavenProject;
72  import org.apache.maven.project.ProjectBuilder;
73  import org.apache.maven.project.ProjectBuildingException;
74  import org.apache.maven.project.ProjectBuildingRequest;
75  import org.apache.maven.project.ProjectBuildingResult;
76  import org.apache.maven.shared.artifact.filter.collection.ArtifactFilterException;
77  import org.apache.maven.shared.artifact.filter.collection.ArtifactIdFilter;
78  import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts;
79  import org.apache.maven.shared.artifact.filter.collection.GroupIdFilter;
80  import org.apache.maven.shared.artifact.filter.collection.ProjectTransitivityFilter;
81  import org.apache.maven.shared.artifact.filter.collection.ScopeFilter;
82  import org.apache.maven.shared.filtering.MavenFileFilter;
83  import org.apache.maven.shared.filtering.MavenFileFilterRequest;
84  import org.apache.maven.shared.filtering.MavenFilteringException;
85  import org.apache.velocity.VelocityContext;
86  import org.apache.velocity.app.Velocity;
87  import org.apache.velocity.app.VelocityEngine;
88  import org.apache.velocity.exception.MethodInvocationException;
89  import org.apache.velocity.exception.ParseErrorException;
90  import org.apache.velocity.exception.ResourceNotFoundException;
91  import org.apache.velocity.exception.VelocityException;
92  import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
93  import org.codehaus.plexus.resource.ResourceManager;
94  import org.codehaus.plexus.resource.loader.FileResourceLoader;
95  import org.codehaus.plexus.util.FileUtils;
96  import org.codehaus.plexus.util.IOUtil;
97  import org.codehaus.plexus.util.ReaderFactory;
98  import org.codehaus.plexus.util.StringUtils;
99  import org.codehaus.plexus.util.WriterFactory;
100 import org.codehaus.plexus.util.xml.Xpp3Dom;
101 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
102 import org.eclipse.aether.RepositorySystem;
103 import org.eclipse.aether.artifact.ArtifactType;
104 import org.eclipse.aether.artifact.DefaultArtifact;
105 import org.eclipse.aether.resolution.ArtifactRequest;
106 import org.eclipse.aether.resolution.ArtifactResolutionException;
107 import org.eclipse.aether.resolution.ArtifactResult;
108 import org.eclipse.aether.util.artifact.JavaScopes;
109 
110 /**
111  * <p>
112  * Pull down resourceBundles containing remote resources and process the resources contained inside. When that is done,
113  * the resources are injected into the current (in-memory) Maven project, making them available to the process-resources
114  * phase.
115  * </p>
116  * <p>
117  * Resources that end in ".vm" are treated as Velocity templates. For those, the ".vm" is stripped off for the final
118  * artifact name and it's fed through Velocity to have properties expanded, conditions processed, etc...
119  * </p>
120  * Resources that don't end in ".vm" are copied "as is".
121  * <p>
122  * This is a support abstract class, with two non-aggregating and aggregating implementations.
123  * </p>
124  */
125 public abstract class AbstractProcessRemoteResourcesMojo extends AbstractMojo {
126     private static final String TEMPLATE_SUFFIX = ".vm";
127 
128     /**
129      * <p>
130      * In cases where a local resource overrides one from a remote resource bundle, that resource should be filtered if
131      * the resource set specifies it. In those cases, this parameter defines the list of delimiters for filterable
132      * expressions. These delimiters are specified in the form 'beginToken*endToken'. If no '*' is given, the delimiter
133      * is assumed to be the same for start and end.
134      * </p>
135      * <p>
136      * So, the default filtering delimiters might be specified as:
137      * </p>
138      *
139      * <pre>
140      * &lt;delimiters&gt;
141      *   &lt;delimiter&gt;${*}&lt;/delimiter&gt;
142      *   &lt;delimiter&gt;@&lt;/delimiter&gt;
143      * &lt;/delimiters&gt;
144      * </pre>
145      * Since the '@' delimiter is the same on both ends, we don't need to specify '@*@' (though we can).
146      *
147      * @since 1.1
148      */
149     @Parameter
150     protected List<String> filterDelimiters;
151 
152     /**
153      * @since 1.1
154      */
155     @Parameter(defaultValue = "true")
156     protected boolean useDefaultFilterDelimiters;
157 
158     /**
159      * The character encoding scheme to be applied when filtering resources.
160      */
161     @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}")
162     protected String encoding;
163 
164     /**
165      * The directory where processed resources will be placed for packaging.
166      */
167     @Parameter(defaultValue = "${project.build.directory}/maven-shared-archive-resources")
168     private File outputDirectory;
169 
170     /**
171      * The directory containing extra information appended to the generated resources.
172      */
173     @Parameter(defaultValue = "${basedir}/src/main/appended-resources")
174     private File appendedResourcesDirectory;
175 
176     /**
177      * Supplemental model data. Useful when processing
178      * artifacts with incomplete POM metadata.
179      * <p/>
180      * By default, this Mojo looks for supplemental model data in the file
181      * "<code>${appendedResourcesDirectory}/supplemental-models.xml</code>".
182      *
183      * @since 1.0-alpha-5
184      */
185     @Parameter
186     private String[] supplementalModels;
187 
188     /**
189      * List of artifacts that are added to the search path when looking
190      * for supplementalModels, expressed with <code>groupId:artifactId:version[:type[:classifier]]</code> format.
191      *
192      * @since 1.1
193      */
194     @Parameter
195     private List<String> supplementalModelArtifacts;
196 
197     /**
198      * The resource bundles that will be retrieved and processed,
199      * expressed with <code>groupId:artifactId:version[:type[:classifier]]</code> format.
200      */
201     @Parameter(required = true)
202     private List<String> resourceBundles;
203 
204     /**
205      * Skip remote-resource processing
206      *
207      * @since 1.0-alpha-5
208      */
209     @Parameter(property = "remoteresources.skip", defaultValue = "false")
210     private boolean skip;
211 
212     /**
213      * Attaches the resources to the main build of the project as a resource directory.
214      *
215      * @since 1.5
216      */
217     @Parameter(defaultValue = "true", property = "attachToMain")
218     private boolean attachToMain;
219 
220     /**
221      * Attaches the resources to the test build of the project as a resource directory.
222      *
223      * @since 1.5
224      */
225     @Parameter(defaultValue = "true", property = "attachToTest")
226     private boolean attachToTest;
227 
228     /**
229      * Additional properties to be passed to Velocity.
230      * Several properties are automatically added:<ul>
231      * <li><code>project</code> - the current MavenProject </li>
232      * <li><code>projects</code> - the list of dependency projects</li>
233      * <li><code>projectsSortedByOrganization</code> - the list of dependency projects sorted by organization</li>
234      * <li><code>projectTimespan</code> - the timespan of the current project (requires inceptionYear in pom)</li>
235      * <li><code>locator</code> - the ResourceManager that can be used to retrieve additional resources</li>
236      * </ul>
237      * See <a
238      * href="https://maven.apache.org/ref/current/maven-project/apidocs/org/apache/maven/project/MavenProject.html"> the
239      * javadoc for MavenProject</a> for information about the properties on the MavenProject.
240      */
241     @Parameter
242     protected Map<String, Object> properties = new HashMap<>();
243 
244     /**
245      * Whether to include properties defined in the project when filtering resources.
246      *
247      * @since 1.2
248      */
249     @Parameter(defaultValue = "false")
250     protected boolean includeProjectProperties = false;
251 
252     /**
253      * When the result of velocity transformation fits in memory, it is compared with the actual contents on disk
254      * to eliminate unnecessary destination file overwrite. This improves build times since further build steps
255      * typically rely on the modification date.
256      *
257      * @since 1.6
258      */
259     @Parameter(defaultValue = "5242880")
260     protected int velocityFilterInMemoryThreshold = 5 * 1024 * 1024;
261 
262     /**
263      * The Maven session.
264      */
265     @Parameter(defaultValue = "${session}", readonly = true, required = true)
266     protected MavenSession mavenSession;
267 
268     /**
269      * The current project.
270      */
271     @Parameter(defaultValue = "${project}", readonly = true, required = true)
272     protected MavenProject project;
273 
274     /**
275      * Scope to include. An Empty string indicates all scopes (default is "runtime").
276      *
277      * @since 1.0
278      */
279     @Parameter(property = "includeScope", defaultValue = "runtime")
280     protected String includeScope;
281 
282     /**
283      * Scope to exclude. An Empty string indicates no scopes (default).
284      *
285      * @since 1.0
286      */
287     @Parameter(property = "excludeScope", defaultValue = "")
288     protected String excludeScope;
289 
290     /**
291      * When resolving project dependencies, specify the scopes to include.
292      * The default is the same as "includeScope" if there are no exclude scopes set.
293      * Otherwise, it defaults to "test" to grab all the dependencies so the
294      * exclude filters can filter out what is not needed.
295      *
296      * @since 1.5
297      */
298     @Parameter
299     protected String[] resolveScopes;
300 
301     /**
302      * Comma separated list of Artifact names to exclude.
303      *
304      * @since 1.0
305      */
306     @Parameter(property = "excludeArtifactIds", defaultValue = "")
307     protected String excludeArtifactIds;
308 
309     /**
310      * Comma separated list of Artifact names to include.
311      *
312      * @since 1.0
313      */
314     @Parameter(property = "includeArtifactIds", defaultValue = "")
315     protected String includeArtifactIds;
316 
317     /**
318      * Comma separated list of GroupId Names to exclude.
319      *
320      * @since 1.0
321      */
322     @Parameter(property = "excludeGroupIds", defaultValue = "")
323     protected String excludeGroupIds;
324 
325     /**
326      * Comma separated list of GroupIds to include.
327      *
328      * @since 1.0
329      */
330     @Parameter(property = "includeGroupIds", defaultValue = "")
331     protected String includeGroupIds;
332 
333     /**
334      * If we should exclude transitive dependencies
335      *
336      * @since 1.0
337      */
338     @Parameter(property = "excludeTransitive", defaultValue = "false")
339     protected boolean excludeTransitive;
340 
341     /**
342      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
343      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
344      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
345      */
346     @Parameter(defaultValue = "${project.build.outputTimestamp}")
347     private String outputTimestamp;
348 
349     @Component
350     protected RepositorySystem repoSystem;
351 
352     /**
353      * Filtering support, for local resources that override those in the remote bundle.
354      */
355     @Component
356     private MavenFileFilter fileFilter;
357 
358     @Component
359     private ResourceManager locator;
360 
361     @Component
362     private ProjectBuilder projectBuilder;
363 
364     @Component
365     private ArtifactHandlerManager artifactHandlerManager;
366 
367     /**
368      * Map of artifacts to supplemental project object models.
369      */
370     private Map<String, Model> supplementModels;
371 
372     /**
373      * Merges supplemental data model with artifact metadata. Useful when processing artifacts with
374      * incomplete POM metadata.
375      */
376     private final ModelInheritanceAssembler inheritanceAssembler = new ModelInheritanceAssembler();
377 
378     private VelocityEngine velocity;
379 
380     @Override
381     public void execute() throws MojoExecutionException {
382         if (skip) {
383             getLog().info("Skipping remote resources execution.");
384             return;
385         }
386 
387         if (StringUtils.isEmpty(encoding)) {
388             getLog().warn("File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
389                     + ", i.e. build is platform dependent!");
390         }
391 
392         if (resolveScopes == null) {
393             resolveScopes = new String[] {StringUtils.isEmpty(this.includeScope) ? JavaScopes.TEST : this.includeScope};
394         }
395 
396         if (supplementalModels == null) {
397             File sups = new File(appendedResourcesDirectory, "supplemental-models.xml");
398             if (sups.exists()) {
399                 try {
400                     supplementalModels = new String[] {sups.toURI().toURL().toString()};
401                 } catch (MalformedURLException e) {
402                     // ignore
403                     getLog().debug("URL issue with supplemental-models.xml: " + e);
404                 }
405             }
406         }
407 
408         configureLocator();
409 
410         if (includeProjectProperties) {
411             final Properties projectProperties = project.getProperties();
412             for (Object key : projectProperties.keySet()) {
413                 properties.put(key.toString(), projectProperties.get(key).toString());
414             }
415         }
416 
417         ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
418         try {
419             validate();
420 
421             List<File> resourceBundleArtifacts = downloadBundles(resourceBundles);
422             supplementModels = loadSupplements(supplementalModels);
423 
424             ClassLoader classLoader = initalizeClassloader(resourceBundleArtifacts);
425 
426             Thread.currentThread().setContextClassLoader(classLoader);
427 
428             velocity = new VelocityEngine();
429             velocity.setProperty("resource.loaders", "classpath");
430             velocity.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
431             velocity.init();
432 
433             VelocityContext context = buildVelocityContext(properties);
434 
435             processResourceBundles(classLoader, context);
436 
437             if (outputDirectory.exists()) {
438                 // ----------------------------------------------------------------------------
439                 // Push our newly generated resources directory into the MavenProject so that
440                 // these resources can be picked up by the process-resources phase.
441                 // ----------------------------------------------------------------------------
442                 Resource resource = new Resource();
443                 resource.setDirectory(outputDirectory.getAbsolutePath());
444                 // MRRESOURCES-61 handle main and test resources separately
445                 if (attachToMain) {
446                     project.getResources().add(resource);
447                 }
448                 if (attachToTest) {
449                     project.getTestResources().add(resource);
450                 }
451 
452                 // ----------------------------------------------------------------------------
453                 // Write out archiver dot file
454                 // ----------------------------------------------------------------------------
455                 try {
456                     File dotFile = new File(project.getBuild().getDirectory(), ".plxarc");
457                     FileUtils.mkdir(dotFile.getParentFile().getAbsolutePath());
458                     FileUtils.fileWrite(dotFile.getAbsolutePath(), outputDirectory.getName());
459                 } catch (IOException e) {
460                     throw new MojoExecutionException("Error creating dot file for archiving instructions.", e);
461                 }
462             }
463         } finally {
464             Thread.currentThread().setContextClassLoader(origLoader);
465         }
466     }
467 
468     private void configureLocator() throws MojoExecutionException {
469         if (supplementalModelArtifacts != null && !supplementalModelArtifacts.isEmpty()) {
470             List<File> artifacts = downloadBundles(supplementalModelArtifacts);
471 
472             for (File artifact : artifacts) {
473                 if (artifact.isDirectory()) {
474                     locator.addSearchPath(FileResourceLoader.ID, artifact.getAbsolutePath());
475                 } else {
476                     try {
477                         locator.addSearchPath(
478                                 "jar", "jar:" + artifact.toURI().toURL().toExternalForm());
479                     } catch (MalformedURLException e) {
480                         throw new MojoExecutionException("Could not use jar " + artifact.getAbsolutePath(), e);
481                     }
482                 }
483             }
484         }
485 
486         locator.addSearchPath(
487                 FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath());
488         if (appendedResourcesDirectory != null) {
489             locator.addSearchPath(FileResourceLoader.ID, appendedResourcesDirectory.getAbsolutePath());
490         }
491         locator.addSearchPath("url", "");
492         locator.setOutputDirectory(new File(project.getBuild().getDirectory()));
493     }
494 
495     protected List<MavenProject> getProjects() {
496         List<MavenProject> projects = new ArrayList<>();
497 
498         // add filters in well known order, least specific to most specific
499         FilterArtifacts filter = new FilterArtifacts();
500 
501         Set<Artifact> artifacts = new LinkedHashSet<>();
502         artifacts.addAll(getAllDependencies());
503         if (this.excludeTransitive) {
504             filter.addFilter(new ProjectTransitivityFilter(getDirectDependencies(), true));
505         }
506 
507         filter.addFilter(new ScopeFilter(this.includeScope, this.excludeScope));
508         filter.addFilter(new GroupIdFilter(this.includeGroupIds, this.excludeGroupIds));
509         filter.addFilter(new ArtifactIdFilter(this.includeArtifactIds, this.excludeArtifactIds));
510 
511         // perform filtering
512         try {
513             artifacts = filter.filter(artifacts);
514         } catch (ArtifactFilterException e) {
515             throw new IllegalStateException(e.getMessage(), e);
516         }
517 
518         getLog().debug("PROJECTS: " + artifacts);
519 
520         for (Artifact artifact : artifacts) {
521             if (artifact.isSnapshot()) {
522                 artifact.setVersion(artifact.getBaseVersion());
523             }
524 
525             getLog().debug("Building project for " + artifact);
526             MavenProject p;
527             try {
528                 ProjectBuildingRequest req = new DefaultProjectBuildingRequest()
529                         .setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL)
530                         .setProcessPlugins(false)
531                         .setRepositorySession(mavenSession.getRepositorySession())
532                         .setSystemProperties(mavenSession.getSystemProperties())
533                         .setUserProperties(mavenSession.getUserProperties())
534                         .setLocalRepository(mavenSession.getLocalRepository())
535                         .setRemoteRepositories(project.getRemoteArtifactRepositories());
536                 ProjectBuildingResult res = projectBuilder.build(artifact, req);
537                 p = res.getProject();
538             } catch (ProjectBuildingException e) {
539                 getLog().warn("Invalid project model for artifact [" + artifact.getGroupId() + ":"
540                         + artifact.getArtifactId() + ":" + artifact.getVersion() + "]. "
541                         + "It will be ignored by the remote resources Mojo.");
542                 continue;
543             }
544 
545             String supplementKey = generateSupplementMapKey(
546                     p.getModel().getGroupId(), p.getModel().getArtifactId());
547 
548             if (supplementModels.containsKey(supplementKey)) {
549                 Model mergedModel = mergeModels(p.getModel(), supplementModels.get(supplementKey));
550                 MavenProject mergedProject = new MavenProject(mergedModel);
551                 projects.add(mergedProject);
552                 mergedProject.setArtifact(artifact);
553                 mergedProject.setVersion(artifact.getVersion());
554                 getLog().debug("Adding project with groupId [" + mergedProject.getGroupId() + "] (supplemented)");
555             } else {
556                 projects.add(p);
557                 getLog().debug("Adding project with groupId [" + p.getGroupId() + "]");
558             }
559         }
560         projects.sort(new ProjectComparator());
561         return projects;
562     }
563 
564     /**
565      * Returns all the transitive hull of all the involved maven projects.
566      */
567     protected abstract Set<Artifact> getAllDependencies();
568 
569     /**
570      * Returns all the direct dependencies of all the involved maven projects.
571      */
572     protected abstract Set<Artifact> getDirectDependencies();
573 
574     protected Map<Organization, List<MavenProject>> getProjectsSortedByOrganization(List<MavenProject> projects) {
575         Map<Organization, List<MavenProject>> organizations = new TreeMap<>(new OrganizationComparator());
576         List<MavenProject> unknownOrganization = new ArrayList<>();
577 
578         for (MavenProject p : projects) {
579             if (p.getOrganization() != null
580                     && StringUtils.isNotEmpty(p.getOrganization().getName())) {
581                 List<MavenProject> sortedProjects = organizations.get(p.getOrganization());
582                 if (sortedProjects == null) {
583                     sortedProjects = new ArrayList<>();
584                 }
585                 sortedProjects.add(p);
586 
587                 organizations.put(p.getOrganization(), sortedProjects);
588             } else {
589                 unknownOrganization.add(p);
590             }
591         }
592         if (!unknownOrganization.isEmpty()) {
593             Organization unknownOrg = new Organization();
594             unknownOrg.setName("an unknown organization");
595             organizations.put(unknownOrg, unknownOrganization);
596         }
597 
598         return organizations;
599     }
600 
601     protected boolean copyResourceIfExists(File file, String relFileName, VelocityContext context)
602             throws IOException, MojoExecutionException {
603         for (Resource resource : project.getResources()) {
604             File resourceDirectory = new File(resource.getDirectory());
605 
606             if (!resourceDirectory.exists()) {
607                 continue;
608             }
609 
610             // TODO - really should use the resource includes/excludes and name mapping
611             File source = new File(resourceDirectory, relFileName);
612             File templateSource = new File(resourceDirectory, relFileName + TEMPLATE_SUFFIX);
613 
614             if (!source.exists() && templateSource.exists()) {
615                 source = templateSource;
616             }
617 
618             if (source.exists() && !source.equals(file)) {
619                 if (source == templateSource) {
620                     try (DeferredFileOutputStream os =
621                             new DeferredFileOutputStream(velocityFilterInMemoryThreshold, file)) {
622                         try (Reader reader = getReader(source);
623                                 Writer writer = getWriter(os)) {
624                             velocity.evaluate(context, writer, "", reader);
625                         } catch (ParseErrorException | MethodInvocationException | ResourceNotFoundException e) {
626                             throw new MojoExecutionException("Error rendering velocity resource: " + source, e);
627                         }
628                         fileWriteIfDiffers(os);
629                     }
630                 } else if (resource.isFiltering()) {
631 
632                     MavenFileFilterRequest req = setupRequest(resource, source, file);
633 
634                     try {
635                         fileFilter.copyFile(req);
636                     } catch (MavenFilteringException e) {
637                         throw new MojoExecutionException("Error filtering resource: " + source, e);
638                     }
639                 } else {
640                     FileUtils.copyFile(source, file);
641                 }
642 
643                 // exclude the original (so eclipse doesn't complain about duplicate resources)
644                 resource.addExclude(relFileName);
645 
646                 return true;
647             }
648         }
649         return false;
650     }
651 
652     private Reader getReader(File source) throws IOException {
653         if (encoding != null) {
654             return new InputStreamReader(Files.newInputStream(source.toPath()), encoding);
655         } else {
656             return ReaderFactory.newPlatformReader(source);
657         }
658     }
659 
660     private Writer getWriter(OutputStream os) throws IOException {
661         if (encoding != null) {
662             return new OutputStreamWriter(os, encoding);
663         } else {
664             return WriterFactory.newPlatformWriter(os);
665         }
666     }
667 
668     /**
669      * If the transformation result fits in memory and the destination file already exists
670      * then both are compared.
671      * <p>If destination file is byte-by-byte equal, then it is not overwritten.
672      * This improves subsequent compilation times since upstream plugins property see that
673      * the resource was not modified.
674      * <p>Note: the method should be called after {@link DeferredFileOutputStream#close}
675      *
676      * @param outStream Deferred stream
677      * @throws IOException On IO error.
678      */
679     private void fileWriteIfDiffers(DeferredFileOutputStream outStream) throws IOException {
680         File file = outStream.getFile();
681         if (outStream.isThresholdExceeded()) {
682             getLog().info("File " + file + " was overwritten due to content limit threshold " + outStream.getThreshold()
683                     + " reached");
684             return;
685         }
686         boolean needOverwrite = true;
687 
688         if (file.exists()) {
689             try (InputStream is = Files.newInputStream(file.toPath());
690                     InputStream newContents = new ByteArrayInputStream(outStream.getData())) {
691                 needOverwrite = !IOUtil.contentEquals(is, newContents);
692                 if (getLog().isDebugEnabled()) {
693                     getLog().debug("File " + file + " contents " + (needOverwrite ? "differs" : "does not differ"));
694                 }
695             }
696         }
697 
698         if (!needOverwrite) {
699             getLog().debug("File " + file + " is up to date");
700             return;
701         }
702         getLog().debug("Writing " + file);
703 
704         try (OutputStream os = Files.newOutputStream(file.toPath())) {
705             outStream.writeTo(os);
706         }
707     }
708 
709     private MavenFileFilterRequest setupRequest(Resource resource, File source, File file) {
710         MavenFileFilterRequest req = new MavenFileFilterRequest();
711         req.setFrom(source);
712         req.setTo(file);
713         req.setFiltering(resource.isFiltering());
714 
715         req.setMavenProject(project);
716         req.setMavenSession(mavenSession);
717         req.setInjectProjectBuildFilters(true);
718 
719         if (encoding != null) {
720             req.setEncoding(encoding);
721         }
722 
723         if (filterDelimiters != null && !filterDelimiters.isEmpty()) {
724             LinkedHashSet<String> delims = new LinkedHashSet<>();
725             if (useDefaultFilterDelimiters) {
726                 delims.addAll(req.getDelimiters());
727             }
728 
729             for (String delim : filterDelimiters) {
730                 if (delim == null) {
731                     delims.add("${*}");
732                 } else {
733                     delims.add(delim);
734                 }
735             }
736 
737             req.setDelimiters(delims);
738         }
739 
740         return req;
741     }
742 
743     protected void validate() throws MojoExecutionException {
744         int bundleCount = 1;
745 
746         for (String artifactDescriptor : resourceBundles) {
747             // groupId:artifactId:version, groupId:artifactId:version:type
748             // or groupId:artifactId:version:type:classifier
749             String[] s = StringUtils.split(artifactDescriptor, ":");
750 
751             if (s.length < 3 || s.length > 5) {
752                 String position;
753 
754                 if (bundleCount == 1) {
755                     position = "1st";
756                 } else if (bundleCount == 2) {
757                     position = "2nd";
758                 } else if (bundleCount == 3) {
759                     position = "3rd";
760                 } else {
761                     position = bundleCount + "th";
762                 }
763 
764                 throw new MojoExecutionException("The " + position
765                         + " resource bundle configured must specify a groupId, artifactId, "
766                         + " version and, optionally, type and classifier for a remote resource bundle. "
767                         + "Must be of the form <resourceBundle>groupId:artifactId:version</resourceBundle>, "
768                         + "<resourceBundle>groupId:artifactId:version:type</resourceBundle> or "
769                         + "<resourceBundle>groupId:artifactId:version:type:classifier</resourceBundle>");
770             }
771 
772             bundleCount++;
773         }
774     }
775 
776     private static final String KEY_PROJECTS = "projects";
777     private static final String KEY_PROJECTS_ORGS = "projectsSortedByOrganization";
778 
779     protected VelocityContext buildVelocityContext(Map<String, Object> properties) {
780         // the following properties are expensive to calculate, so we provide them lazily
781         VelocityContext context = new VelocityContext(properties) {
782             @Override
783             public Object internalGet(String key) {
784                 Object result = super.internalGet(key);
785                 if (result == null && key != null && key.startsWith(KEY_PROJECTS) && containsKey(key)) {
786                     // calculate and put projects* properties
787                     List<MavenProject> projects = getProjects();
788                     put(KEY_PROJECTS, projects);
789                     put(KEY_PROJECTS_ORGS, getProjectsSortedByOrganization(projects));
790                     return super.internalGet(key);
791                 }
792                 return result;
793             }
794         };
795         // to have a consistent getKeys()/containsKey() behaviour, keys must be present from the start
796         context.put(KEY_PROJECTS, null);
797         context.put(KEY_PROJECTS_ORGS, null);
798         // the following properties are cheap to calculate, so we provide them eagerly
799 
800         // Reproducible Builds: try to use reproducible output timestamp
801         MavenArchiver archiver = new MavenArchiver();
802         Date outputDate = archiver.parseOutputTimestamp(outputTimestamp);
803 
804         String inceptionYear = project.getInceptionYear();
805         String year = new SimpleDateFormat("yyyy").format((outputDate == null) ? new Date() : outputDate);
806 
807         if (StringUtils.isEmpty(inceptionYear)) {
808             if (getLog().isDebugEnabled()) {
809                 getLog().debug("inceptionYear not specified, defaulting to " + year);
810             }
811 
812             inceptionYear = year;
813         }
814         context.put("project", project);
815         context.put("presentYear", year);
816         context.put("locator", locator);
817 
818         if (inceptionYear.equals(year)) {
819             context.put("projectTimespan", year);
820         } else {
821             context.put("projectTimespan", inceptionYear + "-" + year);
822         }
823         return context;
824     }
825 
826     private List<File> downloadBundles(List<String> bundles) throws MojoExecutionException {
827         List<File> bundleArtifacts = new ArrayList<>();
828 
829         for (String artifactDescriptor : bundles) {
830             getLog().info("Preparing remote bundle " + artifactDescriptor);
831             // groupId:artifactId:version[:type[:classifier]]
832             String[] s = artifactDescriptor.split(":");
833 
834             File artifactFile = null;
835             // check if the artifact is part of the reactor
836             if (mavenSession != null) {
837                 List<MavenProject> list = mavenSession.getProjects();
838                 for (MavenProject p : list) {
839                     if (s[0].equals(p.getGroupId()) && s[1].equals(p.getArtifactId()) && s[2].equals(p.getVersion())) {
840                         if (s.length >= 4 && "test-jar".equals(s[3])) {
841                             artifactFile = new File(p.getBuild().getTestOutputDirectory());
842                         } else {
843                             artifactFile = new File(p.getBuild().getOutputDirectory());
844                         }
845                     }
846                 }
847             }
848             if (artifactFile == null || !artifactFile.exists()) {
849                 String g = s[0];
850                 String a = s[1];
851                 String v = s[2];
852                 String type = (s.length >= 4 ? s[3] : "jar");
853                 ArtifactType artifactType =
854                         RepositoryUtils.newArtifactType(type, artifactHandlerManager.getArtifactHandler(type));
855                 String classifier = (s.length == 5 ? s[4] : artifactType.getClassifier());
856 
857                 DefaultArtifact artifact =
858                         new DefaultArtifact(g, a, classifier, artifactType.getExtension(), v, artifactType);
859 
860                 try {
861                     ArtifactRequest request =
862                             new ArtifactRequest(artifact, project.getRemoteProjectRepositories(), "remote-resources");
863                     ArtifactResult result = repoSystem.resolveArtifact(mavenSession.getRepositorySession(), request);
864                     artifactFile = result.getArtifact().getFile();
865                 } catch (ArtifactResolutionException e) {
866                     throw new MojoExecutionException("Error processing remote resources", e);
867                 }
868             }
869             bundleArtifacts.add(artifactFile);
870         }
871 
872         return bundleArtifacts;
873     }
874 
875     private ClassLoader initalizeClassloader(List<File> artifacts) throws MojoExecutionException {
876         RemoteResourcesClassLoader cl = new RemoteResourcesClassLoader(null);
877         try {
878             for (File artifact : artifacts) {
879                 cl.addURL(artifact.toURI().toURL());
880             }
881             return cl;
882         } catch (MalformedURLException e) {
883             throw new MojoExecutionException("Unable to configure resources classloader: " + e.getMessage(), e);
884         }
885     }
886 
887     protected void processResourceBundles(ClassLoader classLoader, VelocityContext context)
888             throws MojoExecutionException {
889         List<Map.Entry<String, RemoteResourcesBundle>> remoteResources = new ArrayList<>();
890         int bundleCount = 0;
891         int resourceCount = 0;
892 
893         // list remote resources form bundles
894         try {
895             RemoteResourcesBundleXpp3Reader bundleReader = new RemoteResourcesBundleXpp3Reader();
896 
897             for (Enumeration<URL> e = classLoader.getResources(BundleRemoteResourcesMojo.RESOURCES_MANIFEST);
898                     e.hasMoreElements(); ) {
899                 URL url = e.nextElement();
900                 bundleCount++;
901                 getLog().debug("processResourceBundle on bundle#" + bundleCount + " " + url);
902 
903                 RemoteResourcesBundle bundle;
904 
905                 try (InputStream in = url.openStream()) {
906                     bundle = bundleReader.read(in);
907                 }
908 
909                 int n = 0;
910                 for (String bundleResource : bundle.getRemoteResources()) {
911                     n++;
912                     resourceCount++;
913                     getLog().debug("bundle#" + bundleCount + " resource#" + n + " " + bundleResource);
914                     remoteResources.add(new AbstractMap.SimpleEntry<>(bundleResource, bundle));
915                 }
916             }
917         } catch (IOException ioe) {
918             throw new MojoExecutionException("Error finding remote resources manifests", ioe);
919         } catch (XmlPullParserException xppe) {
920             throw new MojoExecutionException("Error parsing remote resource bundle descriptor.", xppe);
921         }
922 
923         getLog().info("Copying " + resourceCount + " resource" + ((resourceCount > 1) ? "s" : "") + " from "
924                 + bundleCount + " bundle" + ((bundleCount > 1) ? "s" : "") + ".");
925 
926         String velocityResource = null;
927         try {
928 
929             for (Map.Entry<String, RemoteResourcesBundle> entry : remoteResources) {
930                 String bundleResource = entry.getKey();
931                 RemoteResourcesBundle bundle = entry.getValue();
932 
933                 String projectResource = bundleResource;
934 
935                 boolean doVelocity = false;
936                 if (projectResource.endsWith(TEMPLATE_SUFFIX)) {
937                     projectResource = projectResource.substring(0, projectResource.length() - 3);
938                     velocityResource = bundleResource;
939                     doVelocity = true;
940                 }
941 
942                 // Don't overwrite resource that are already being provided.
943 
944                 File f = new File(outputDirectory, projectResource);
945 
946                 FileUtils.mkdir(f.getParentFile().getAbsolutePath());
947 
948                 if (!copyResourceIfExists(f, projectResource, context)) {
949                     if (doVelocity) {
950                         try (DeferredFileOutputStream os =
951                                 new DeferredFileOutputStream(velocityFilterInMemoryThreshold, f)) {
952                             try (Writer writer = bundle.getSourceEncoding() == null
953                                     ? new OutputStreamWriter(os)
954                                     : new OutputStreamWriter(os, bundle.getSourceEncoding())) {
955                                 if (bundle.getSourceEncoding() == null) {
956                                     // TODO: Is this correct? Shouldn't we behave like the rest of maven and fail
957                                     // down to JVM default instead ISO-8859-1 ?
958                                     velocity.mergeTemplate(bundleResource, "ISO-8859-1", context, writer);
959                                 } else {
960                                     velocity.mergeTemplate(bundleResource, bundle.getSourceEncoding(), context, writer);
961                                 }
962                             }
963                             fileWriteIfDiffers(os);
964                         }
965                     } else {
966                         URL resUrl = classLoader.getResource(bundleResource);
967                         if (resUrl != null) {
968                             FileUtils.copyURLToFile(resUrl, f);
969                         }
970                     }
971 
972                     File appendedResourceFile = new File(appendedResourcesDirectory, projectResource);
973                     File appendedVmResourceFile = new File(appendedResourcesDirectory, projectResource + ".vm");
974 
975                     if (appendedResourceFile.exists()) {
976                         getLog().info("Copying appended resource: " + projectResource);
977                         try (InputStream in = Files.newInputStream(appendedResourceFile.toPath());
978                                 OutputStream out = new FileOutputStream(f, true)) {
979                             IOUtil.copy(in, out);
980                         }
981 
982                     } else if (appendedVmResourceFile.exists()) {
983                         getLog().info("Filtering appended resource: " + projectResource + ".vm");
984 
985                         try (Reader reader = new FileReader(appendedVmResourceFile);
986                                 Writer writer = getWriter(bundle, f)) {
987                             Velocity.init();
988                             Velocity.evaluate(context, writer, "remote-resources", reader);
989                         }
990                     }
991                 }
992             }
993         } catch (IOException ioe) {
994             throw new MojoExecutionException("Error reading remote resource", ioe);
995         } catch (VelocityException e) {
996             throw new MojoExecutionException("Error rendering Velocity resource '" + velocityResource + "'", e);
997         }
998     }
999 
1000     private Writer getWriter(RemoteResourcesBundle bundle, File f) throws IOException {
1001         Writer writer;
1002         if (bundle.getSourceEncoding() == null) {
1003             writer = new PrintWriter(new FileWriter(f, true));
1004         } else {
1005             writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(f, true), bundle.getSourceEncoding()));
1006         }
1007         return writer;
1008     }
1009 
1010     protected Model getSupplement(Xpp3Dom supplementModelXml) throws MojoExecutionException {
1011         MavenXpp3Reader modelReader = new MavenXpp3Reader();
1012         Model model = null;
1013 
1014         try {
1015             model = modelReader.read(new StringReader(supplementModelXml.toString()));
1016             String groupId = model.getGroupId();
1017             String artifactId = model.getArtifactId();
1018 
1019             if (groupId == null || groupId.trim().equals("")) {
1020                 throw new MojoExecutionException(
1021                         "Supplemental project XML " + "requires that a <groupId> element be present.");
1022             }
1023 
1024             if (artifactId == null || artifactId.trim().equals("")) {
1025                 throw new MojoExecutionException(
1026                         "Supplemental project XML " + "requires that a <artifactId> element be present.");
1027             }
1028         } catch (IOException e) {
1029             getLog().warn("Unable to read supplemental XML: " + e.getMessage(), e);
1030         } catch (XmlPullParserException e) {
1031             getLog().warn("Unable to parse supplemental XML: " + e.getMessage(), e);
1032         }
1033 
1034         return model;
1035     }
1036 
1037     protected Model mergeModels(Model parent, Model child) {
1038         inheritanceAssembler.assembleModelInheritance(child, parent);
1039         return child;
1040     }
1041 
1042     private static String generateSupplementMapKey(String groupId, String artifactId) {
1043         return groupId.trim() + ":" + artifactId.trim();
1044     }
1045 
1046     private Map<String, Model> loadSupplements(String[] models) throws MojoExecutionException {
1047         if (models == null) {
1048             getLog().debug("Supplemental data models won't be loaded. No models specified.");
1049             return Collections.emptyMap();
1050         }
1051 
1052         List<Supplement> supplements = new ArrayList<>();
1053         for (String set : models) {
1054             getLog().debug("Preparing ruleset: " + set);
1055             try {
1056                 File f = locator.getResourceAsFile(set, getLocationTemp(set));
1057 
1058                 if (null == f || !f.exists()) {
1059                     throw new MojoExecutionException("Cold not resolve " + set);
1060                 }
1061                 if (!f.canRead()) {
1062                     throw new MojoExecutionException("Supplemental data models won't be loaded. " + "File "
1063                             + f.getAbsolutePath() + " cannot be read, check permissions on the file.");
1064                 }
1065 
1066                 getLog().debug("Loading supplemental models from " + f.getAbsolutePath());
1067 
1068                 SupplementalDataModelXpp3Reader reader = new SupplementalDataModelXpp3Reader();
1069                 SupplementalDataModel supplementalModel = reader.read(new FileReader(f));
1070                 supplements.addAll(supplementalModel.getSupplement());
1071             } catch (Exception e) {
1072                 String msg = "Error loading supplemental data models: " + e.getMessage();
1073                 getLog().error(msg, e);
1074                 throw new MojoExecutionException(msg, e);
1075             }
1076         }
1077 
1078         getLog().debug("Loading supplements complete.");
1079 
1080         Map<String, Model> supplementMap = new HashMap<>();
1081         for (Supplement sd : supplements) {
1082             Xpp3Dom dom = (Xpp3Dom) sd.getProject();
1083 
1084             Model m = getSupplement(dom);
1085             supplementMap.put(generateSupplementMapKey(m.getGroupId(), m.getArtifactId()), m);
1086         }
1087 
1088         return supplementMap;
1089     }
1090 
1091     /**
1092      * Convenience method to get the location of the specified file name.
1093      *
1094      * @param name the name of the file whose location is to be resolved
1095      * @return a String that contains the absolute file name of the file
1096      */
1097     private String getLocationTemp(String name) {
1098         String loc = name;
1099         if (loc.indexOf('/') != -1) {
1100             loc = loc.substring(loc.lastIndexOf('/') + 1);
1101         }
1102         if (loc.indexOf('\\') != -1) {
1103             loc = loc.substring(loc.lastIndexOf('\\') + 1);
1104         }
1105         getLog().debug("Before: " + name + " After: " + loc);
1106         return loc;
1107     }
1108 
1109     static class OrganizationComparator implements Comparator<Organization> {
1110         @Override
1111         public int compare(Organization org1, Organization org2) {
1112             int i = compareStrings(org1.getName(), org2.getName());
1113             if (i == 0) {
1114                 i = compareStrings(org1.getUrl(), org2.getUrl());
1115             }
1116             return i;
1117         }
1118 
1119         private int compareStrings(String s1, String s2) {
1120             if (s1 == null && s2 == null) {
1121                 return 0;
1122             } else if (s1 == null) {
1123                 return 1;
1124             } else if (s2 == null) {
1125                 return -1;
1126             }
1127 
1128             return s1.compareToIgnoreCase(s2);
1129         }
1130     }
1131 
1132     static class ProjectComparator implements Comparator<MavenProject> {
1133         @Override
1134         public int compare(MavenProject p1, MavenProject p2) {
1135             return p1.getArtifact().compareTo(p2.getArtifact());
1136         }
1137     }
1138 }