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