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