| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.maven.plugin.resources.remote; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.FileReader; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.PrintWriter; |
| import java.io.Reader; |
| import java.io.StringReader; |
| import java.io.Writer; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.nio.file.Files; |
| import java.text.SimpleDateFormat; |
| import java.util.AbstractMap; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Date; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| import java.util.TreeMap; |
| |
| import org.apache.commons.io.output.DeferredFileOutputStream; |
| import org.apache.maven.RepositoryUtils; |
| import org.apache.maven.archiver.MavenArchiver; |
| import org.apache.maven.artifact.Artifact; |
| import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; |
| import org.apache.maven.execution.MavenSession; |
| import org.apache.maven.model.Model; |
| import org.apache.maven.model.Organization; |
| import org.apache.maven.model.Resource; |
| import org.apache.maven.model.building.ModelBuildingRequest; |
| import org.apache.maven.model.io.xpp3.MavenXpp3Reader; |
| import org.apache.maven.plugin.AbstractMojo; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.plugin.resources.remote.io.xpp3.RemoteResourcesBundleXpp3Reader; |
| import org.apache.maven.plugin.resources.remote.io.xpp3.SupplementalDataModelXpp3Reader; |
| import org.apache.maven.plugins.annotations.Component; |
| import org.apache.maven.plugins.annotations.Parameter; |
| import org.apache.maven.project.DefaultProjectBuildingRequest; |
| import org.apache.maven.project.MavenProject; |
| import org.apache.maven.project.ProjectBuilder; |
| import org.apache.maven.project.ProjectBuildingException; |
| import org.apache.maven.project.ProjectBuildingRequest; |
| import org.apache.maven.project.ProjectBuildingResult; |
| import org.apache.maven.shared.artifact.filter.collection.ArtifactFilterException; |
| import org.apache.maven.shared.artifact.filter.collection.ArtifactIdFilter; |
| import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts; |
| import org.apache.maven.shared.artifact.filter.collection.GroupIdFilter; |
| import org.apache.maven.shared.artifact.filter.collection.ProjectTransitivityFilter; |
| import org.apache.maven.shared.artifact.filter.collection.ScopeFilter; |
| import org.apache.maven.shared.filtering.MavenFileFilter; |
| import org.apache.maven.shared.filtering.MavenFileFilterRequest; |
| import org.apache.maven.shared.filtering.MavenFilteringException; |
| import org.apache.velocity.VelocityContext; |
| import org.apache.velocity.app.Velocity; |
| import org.apache.velocity.app.VelocityEngine; |
| import org.apache.velocity.exception.MethodInvocationException; |
| import org.apache.velocity.exception.ParseErrorException; |
| import org.apache.velocity.exception.ResourceNotFoundException; |
| import org.apache.velocity.exception.VelocityException; |
| import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; |
| import org.codehaus.plexus.resource.ResourceManager; |
| import org.codehaus.plexus.resource.loader.FileResourceLoader; |
| import org.codehaus.plexus.util.FileUtils; |
| import org.codehaus.plexus.util.IOUtil; |
| import org.codehaus.plexus.util.ReaderFactory; |
| import org.codehaus.plexus.util.StringUtils; |
| import org.codehaus.plexus.util.WriterFactory; |
| import org.codehaus.plexus.util.xml.Xpp3Dom; |
| import org.codehaus.plexus.util.xml.pull.XmlPullParserException; |
| import org.eclipse.aether.RepositorySystem; |
| import org.eclipse.aether.artifact.ArtifactType; |
| import org.eclipse.aether.artifact.DefaultArtifact; |
| import org.eclipse.aether.resolution.ArtifactRequest; |
| import org.eclipse.aether.resolution.ArtifactResolutionException; |
| import org.eclipse.aether.resolution.ArtifactResult; |
| import org.eclipse.aether.util.artifact.JavaScopes; |
| |
| /** |
| * <p> |
| * Pull down resourceBundles containing remote resources and process the resources contained inside. When that is done, |
| * the resources are injected into the current (in-memory) Maven project, making them available to the process-resources |
| * phase. |
| * </p> |
| * <p> |
| * Resources that end in ".vm" are treated as Velocity templates. For those, the ".vm" is stripped off for the final |
| * artifact name and it's fed through Velocity to have properties expanded, conditions processed, etc... |
| * </p> |
| * Resources that don't end in ".vm" are copied "as is". |
| * <p> |
| * This is a support abstract class, with two non-aggregating and aggregating implementations. |
| * </p> |
| */ |
| public abstract class AbstractProcessRemoteResourcesMojo extends AbstractMojo { |
| private static final String TEMPLATE_SUFFIX = ".vm"; |
| |
| /** |
| * <p> |
| * In cases where a local resource overrides one from a remote resource bundle, that resource should be filtered if |
| * the resource set specifies it. In those cases, this parameter defines the list of delimiters for filterable |
| * expressions. These delimiters are specified in the form 'beginToken*endToken'. If no '*' is given, the delimiter |
| * is assumed to be the same for start and end. |
| * </p> |
| * <p> |
| * So, the default filtering delimiters might be specified as: |
| * </p> |
| * |
| * <pre> |
| * <delimiters> |
| * <delimiter>${*}</delimiter> |
| * <delimiter>@</delimiter> |
| * </delimiters> |
| * </pre> |
| * Since the '@' delimiter is the same on both ends, we don't need to specify '@*@' (though we can). |
| * |
| * @since 1.1 |
| */ |
| @Parameter |
| protected List<String> filterDelimiters; |
| |
| /** |
| * @since 1.1 |
| */ |
| @Parameter(defaultValue = "true") |
| protected boolean useDefaultFilterDelimiters; |
| |
| /** |
| * The character encoding scheme to be applied when filtering resources. |
| */ |
| @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}") |
| protected String encoding; |
| |
| /** |
| * The directory where processed resources will be placed for packaging. |
| */ |
| @Parameter(defaultValue = "${project.build.directory}/maven-shared-archive-resources") |
| private File outputDirectory; |
| |
| /** |
| * The directory containing extra information appended to the generated resources. |
| */ |
| @Parameter(defaultValue = "${basedir}/src/main/appended-resources") |
| private File appendedResourcesDirectory; |
| |
| /** |
| * Supplemental model data. Useful when processing |
| * artifacts with incomplete POM metadata. |
| * <p/> |
| * By default, this Mojo looks for supplemental model data in the file |
| * "<code>${appendedResourcesDirectory}/supplemental-models.xml</code>". |
| * |
| * @since 1.0-alpha-5 |
| */ |
| @Parameter |
| private String[] supplementalModels; |
| |
| /** |
| * List of artifacts that are added to the search path when looking |
| * for supplementalModels, expressed with <code>groupId:artifactId:version[:type[:classifier]]</code> format. |
| * |
| * @since 1.1 |
| */ |
| @Parameter |
| private List<String> supplementalModelArtifacts; |
| |
| /** |
| * The resource bundles that will be retrieved and processed, |
| * expressed with <code>groupId:artifactId:version[:type[:classifier]]</code> format. |
| */ |
| @Parameter(required = true) |
| private List<String> resourceBundles; |
| |
| /** |
| * Skip remote-resource processing |
| * |
| * @since 1.0-alpha-5 |
| */ |
| @Parameter(property = "remoteresources.skip", defaultValue = "false") |
| private boolean skip; |
| |
| /** |
| * Attaches the resources to the main build of the project as a resource directory. |
| * |
| * @since 1.5 |
| */ |
| @Parameter(defaultValue = "true", property = "attachToMain") |
| private boolean attachToMain; |
| |
| /** |
| * Attaches the resources to the test build of the project as a resource directory. |
| * |
| * @since 1.5 |
| */ |
| @Parameter(defaultValue = "true", property = "attachToTest") |
| private boolean attachToTest; |
| |
| /** |
| * Additional properties to be passed to Velocity. |
| * Several properties are automatically added:<ul> |
| * <li><code>project</code> - the current MavenProject </li> |
| * <li><code>projects</code> - the list of dependency projects</li> |
| * <li><code>projectsSortedByOrganization</code> - the list of dependency projects sorted by organization</li> |
| * <li><code>projectTimespan</code> - the timespan of the current project (requires inceptionYear in pom)</li> |
| * <li><code>locator</code> - the ResourceManager that can be used to retrieve additional resources</li> |
| * </ul> |
| * See <a |
| * href="https://maven.apache.org/ref/current/maven-project/apidocs/org/apache/maven/project/MavenProject.html"> the |
| * javadoc for MavenProject</a> for information about the properties on the MavenProject. |
| */ |
| @Parameter |
| protected Map<String, Object> properties = new HashMap<>(); |
| |
| /** |
| * Whether to include properties defined in the project when filtering resources. |
| * |
| * @since 1.2 |
| */ |
| @Parameter(defaultValue = "false") |
| protected boolean includeProjectProperties = false; |
| |
| /** |
| * When the result of velocity transformation fits in memory, it is compared with the actual contents on disk |
| * to eliminate unnecessary destination file overwrite. This improves build times since further build steps |
| * typically rely on the modification date. |
| * |
| * @since 1.6 |
| */ |
| @Parameter(defaultValue = "5242880") |
| protected int velocityFilterInMemoryThreshold = 5 * 1024 * 1024; |
| |
| /** |
| * The Maven session. |
| */ |
| @Parameter(defaultValue = "${session}", readonly = true, required = true) |
| protected MavenSession mavenSession; |
| |
| /** |
| * The current project. |
| */ |
| @Parameter(defaultValue = "${project}", readonly = true, required = true) |
| protected MavenProject project; |
| |
| /** |
| * Scope to include. An Empty string indicates all scopes (default is "runtime"). |
| * |
| * @since 1.0 |
| */ |
| @Parameter(property = "includeScope", defaultValue = "runtime") |
| protected String includeScope; |
| |
| /** |
| * Scope to exclude. An Empty string indicates no scopes (default). |
| * |
| * @since 1.0 |
| */ |
| @Parameter(property = "excludeScope", defaultValue = "") |
| protected String excludeScope; |
| |
| /** |
| * When resolving project dependencies, specify the scopes to include. |
| * The default is the same as "includeScope" if there are no exclude scopes set. |
| * Otherwise, it defaults to "test" to grab all the dependencies so the |
| * exclude filters can filter out what is not needed. |
| * |
| * @since 1.5 |
| */ |
| @Parameter |
| protected String[] resolveScopes; |
| |
| /** |
| * Comma separated list of Artifact names to exclude. |
| * |
| * @since 1.0 |
| */ |
| @Parameter(property = "excludeArtifactIds", defaultValue = "") |
| protected String excludeArtifactIds; |
| |
| /** |
| * Comma separated list of Artifact names to include. |
| * |
| * @since 1.0 |
| */ |
| @Parameter(property = "includeArtifactIds", defaultValue = "") |
| protected String includeArtifactIds; |
| |
| /** |
| * Comma separated list of GroupId Names to exclude. |
| * |
| * @since 1.0 |
| */ |
| @Parameter(property = "excludeGroupIds", defaultValue = "") |
| protected String excludeGroupIds; |
| |
| /** |
| * Comma separated list of GroupIds to include. |
| * |
| * @since 1.0 |
| */ |
| @Parameter(property = "includeGroupIds", defaultValue = "") |
| protected String includeGroupIds; |
| |
| /** |
| * If we should exclude transitive dependencies |
| * |
| * @since 1.0 |
| */ |
| @Parameter(property = "excludeTransitive", defaultValue = "false") |
| protected boolean excludeTransitive; |
| |
| /** |
| * Timestamp for reproducible output archive entries, either formatted as ISO 8601 |
| * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like |
| * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>). |
| */ |
| @Parameter(defaultValue = "${project.build.outputTimestamp}") |
| private String outputTimestamp; |
| |
| @Component |
| protected RepositorySystem repoSystem; |
| |
| /** |
| * Filtering support, for local resources that override those in the remote bundle. |
| */ |
| @Component |
| private MavenFileFilter fileFilter; |
| |
| @Component |
| private ResourceManager locator; |
| |
| @Component |
| private ProjectBuilder projectBuilder; |
| |
| @Component |
| private ArtifactHandlerManager artifactHandlerManager; |
| |
| /** |
| * Map of artifacts to supplemental project object models. |
| */ |
| private Map<String, Model> supplementModels; |
| |
| /** |
| * Merges supplemental data model with artifact metadata. Useful when processing artifacts with |
| * incomplete POM metadata. |
| */ |
| private final ModelInheritanceAssembler inheritanceAssembler = new ModelInheritanceAssembler(); |
| |
| private VelocityEngine velocity; |
| |
| @Override |
| public void execute() throws MojoExecutionException { |
| if (skip) { |
| getLog().info("Skipping remote resources execution."); |
| return; |
| } |
| |
| if (encoding == null || encoding.isEmpty()) { |
| getLog().warn("File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING |
| + ", i.e. build is platform dependent!"); |
| } |
| |
| if (resolveScopes == null) { |
| resolveScopes = new String[] { |
| (this.includeScope == null || this.includeScope.isEmpty()) ? JavaScopes.TEST : this.includeScope |
| }; |
| } |
| |
| if (supplementalModels == null) { |
| File sups = new File(appendedResourcesDirectory, "supplemental-models.xml"); |
| if (sups.exists()) { |
| try { |
| supplementalModels = new String[] {sups.toURI().toURL().toString()}; |
| } catch (MalformedURLException e) { |
| // ignore |
| getLog().debug("URL issue with supplemental-models.xml: " + e); |
| } |
| } |
| } |
| |
| configureLocator(); |
| |
| if (includeProjectProperties) { |
| final Properties projectProperties = project.getProperties(); |
| for (Object key : projectProperties.keySet()) { |
| properties.put(key.toString(), projectProperties.get(key).toString()); |
| } |
| } |
| |
| ClassLoader origLoader = Thread.currentThread().getContextClassLoader(); |
| try { |
| validate(); |
| |
| List<File> resourceBundleArtifacts = downloadBundles(resourceBundles); |
| supplementModels = loadSupplements(supplementalModels); |
| |
| ClassLoader classLoader = initalizeClassloader(resourceBundleArtifacts); |
| |
| Thread.currentThread().setContextClassLoader(classLoader); |
| |
| velocity = new VelocityEngine(); |
| velocity.setProperty("resource.loaders", "classpath"); |
| velocity.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName()); |
| velocity.init(); |
| |
| VelocityContext context = buildVelocityContext(properties); |
| |
| processResourceBundles(classLoader, context); |
| |
| if (outputDirectory.exists()) { |
| // ---------------------------------------------------------------------------- |
| // Push our newly generated resources directory into the MavenProject so that |
| // these resources can be picked up by the process-resources phase. |
| // ---------------------------------------------------------------------------- |
| Resource resource = new Resource(); |
| resource.setDirectory(outputDirectory.getAbsolutePath()); |
| // MRRESOURCES-61 handle main and test resources separately |
| if (attachToMain) { |
| project.getResources().add(resource); |
| } |
| if (attachToTest) { |
| project.getTestResources().add(resource); |
| } |
| |
| // ---------------------------------------------------------------------------- |
| // Write out archiver dot file |
| // ---------------------------------------------------------------------------- |
| try { |
| File dotFile = new File(project.getBuild().getDirectory(), ".plxarc"); |
| FileUtils.mkdir(dotFile.getParentFile().getAbsolutePath()); |
| FileUtils.fileWrite(dotFile.getAbsolutePath(), outputDirectory.getName()); |
| } catch (IOException e) { |
| throw new MojoExecutionException("Error creating dot file for archiving instructions.", e); |
| } |
| } |
| } finally { |
| Thread.currentThread().setContextClassLoader(origLoader); |
| } |
| } |
| |
| private void configureLocator() throws MojoExecutionException { |
| if (supplementalModelArtifacts != null && !supplementalModelArtifacts.isEmpty()) { |
| List<File> artifacts = downloadBundles(supplementalModelArtifacts); |
| |
| for (File artifact : artifacts) { |
| if (artifact.isDirectory()) { |
| locator.addSearchPath(FileResourceLoader.ID, artifact.getAbsolutePath()); |
| } else { |
| try { |
| locator.addSearchPath( |
| "jar", "jar:" + artifact.toURI().toURL().toExternalForm()); |
| } catch (MalformedURLException e) { |
| throw new MojoExecutionException("Could not use jar " + artifact.getAbsolutePath(), e); |
| } |
| } |
| } |
| } |
| |
| locator.addSearchPath( |
| FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath()); |
| if (appendedResourcesDirectory != null) { |
| locator.addSearchPath(FileResourceLoader.ID, appendedResourcesDirectory.getAbsolutePath()); |
| } |
| locator.addSearchPath("url", ""); |
| locator.setOutputDirectory(new File(project.getBuild().getDirectory())); |
| } |
| |
| protected List<MavenProject> getProjects() { |
| List<MavenProject> projects = new ArrayList<>(); |
| |
| // add filters in well known order, least specific to most specific |
| FilterArtifacts filter = new FilterArtifacts(); |
| |
| Set<Artifact> artifacts = new LinkedHashSet<>(); |
| artifacts.addAll(getAllDependencies()); |
| if (this.excludeTransitive) { |
| filter.addFilter(new ProjectTransitivityFilter(getDirectDependencies(), true)); |
| } |
| |
| filter.addFilter(new ScopeFilter(this.includeScope, this.excludeScope)); |
| filter.addFilter(new GroupIdFilter(this.includeGroupIds, this.excludeGroupIds)); |
| filter.addFilter(new ArtifactIdFilter(this.includeArtifactIds, this.excludeArtifactIds)); |
| |
| // perform filtering |
| try { |
| artifacts = filter.filter(artifacts); |
| } catch (ArtifactFilterException e) { |
| throw new IllegalStateException(e.getMessage(), e); |
| } |
| |
| getLog().debug("PROJECTS: " + artifacts); |
| |
| for (Artifact artifact : artifacts) { |
| if (artifact.isSnapshot()) { |
| artifact.setVersion(artifact.getBaseVersion()); |
| } |
| |
| getLog().debug("Building project for " + artifact); |
| MavenProject p; |
| try { |
| ProjectBuildingRequest req = new DefaultProjectBuildingRequest() |
| .setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL) |
| .setProcessPlugins(false) |
| .setRepositorySession(mavenSession.getRepositorySession()) |
| .setSystemProperties(mavenSession.getSystemProperties()) |
| .setUserProperties(mavenSession.getUserProperties()) |
| .setLocalRepository(mavenSession.getLocalRepository()) |
| .setRemoteRepositories(project.getRemoteArtifactRepositories()); |
| ProjectBuildingResult res = projectBuilder.build(artifact, req); |
| p = res.getProject(); |
| } catch (ProjectBuildingException e) { |
| getLog().warn("Invalid project model for artifact [" + artifact.getGroupId() + ":" |
| + artifact.getArtifactId() + ":" + artifact.getVersion() + "]. " |
| + "It will be ignored by the remote resources Mojo."); |
| continue; |
| } |
| |
| String supplementKey = generateSupplementMapKey( |
| p.getModel().getGroupId(), p.getModel().getArtifactId()); |
| |
| if (supplementModels.containsKey(supplementKey)) { |
| Model mergedModel = mergeModels(p.getModel(), supplementModels.get(supplementKey)); |
| MavenProject mergedProject = new MavenProject(mergedModel); |
| projects.add(mergedProject); |
| mergedProject.setArtifact(artifact); |
| mergedProject.setVersion(artifact.getVersion()); |
| getLog().debug("Adding project with groupId [" + mergedProject.getGroupId() + "] (supplemented)"); |
| } else { |
| projects.add(p); |
| getLog().debug("Adding project with groupId [" + p.getGroupId() + "]"); |
| } |
| } |
| projects.sort(new ProjectComparator()); |
| return projects; |
| } |
| |
| /** |
| * Returns all the transitive hull of all the involved maven projects. |
| */ |
| protected abstract Set<Artifact> getAllDependencies(); |
| |
| /** |
| * Returns all the direct dependencies of all the involved maven projects. |
| */ |
| protected abstract Set<Artifact> getDirectDependencies(); |
| |
| protected Map<Organization, List<MavenProject>> getProjectsSortedByOrganization(List<MavenProject> projects) { |
| Map<Organization, List<MavenProject>> organizations = new TreeMap<>(new OrganizationComparator()); |
| List<MavenProject> unknownOrganization = new ArrayList<>(); |
| |
| for (MavenProject p : projects) { |
| if (p.getOrganization() != null |
| && StringUtils.isNotEmpty(p.getOrganization().getName())) { |
| List<MavenProject> sortedProjects = organizations.get(p.getOrganization()); |
| if (sortedProjects == null) { |
| sortedProjects = new ArrayList<>(); |
| } |
| sortedProjects.add(p); |
| |
| organizations.put(p.getOrganization(), sortedProjects); |
| } else { |
| unknownOrganization.add(p); |
| } |
| } |
| if (!unknownOrganization.isEmpty()) { |
| Organization unknownOrg = new Organization(); |
| unknownOrg.setName("an unknown organization"); |
| organizations.put(unknownOrg, unknownOrganization); |
| } |
| |
| return organizations; |
| } |
| |
| protected boolean copyResourceIfExists(File file, String relFileName, VelocityContext context) |
| throws IOException, MojoExecutionException { |
| for (Resource resource : project.getResources()) { |
| File resourceDirectory = new File(resource.getDirectory()); |
| |
| if (!resourceDirectory.exists()) { |
| continue; |
| } |
| |
| // TODO - really should use the resource includes/excludes and name mapping |
| File source = new File(resourceDirectory, relFileName); |
| File templateSource = new File(resourceDirectory, relFileName + TEMPLATE_SUFFIX); |
| |
| if (!source.exists() && templateSource.exists()) { |
| source = templateSource; |
| } |
| |
| if (source.exists() && !source.equals(file)) { |
| if (source == templateSource) { |
| try (DeferredFileOutputStream os = |
| new DeferredFileOutputStream(velocityFilterInMemoryThreshold, file)) { |
| try (Reader reader = getReader(source); |
| Writer writer = getWriter(os)) { |
| velocity.evaluate(context, writer, "", reader); |
| } catch (ParseErrorException | MethodInvocationException | ResourceNotFoundException e) { |
| throw new MojoExecutionException("Error rendering velocity resource: " + source, e); |
| } |
| fileWriteIfDiffers(os); |
| } |
| } else if (resource.isFiltering()) { |
| |
| MavenFileFilterRequest req = setupRequest(resource, source, file); |
| |
| try { |
| fileFilter.copyFile(req); |
| } catch (MavenFilteringException e) { |
| throw new MojoExecutionException("Error filtering resource: " + source, e); |
| } |
| } else { |
| FileUtils.copyFile(source, file); |
| } |
| |
| // exclude the original (so eclipse doesn't complain about duplicate resources) |
| resource.addExclude(relFileName); |
| |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private Reader getReader(File source) throws IOException { |
| if (encoding != null) { |
| return new InputStreamReader(Files.newInputStream(source.toPath()), encoding); |
| } else { |
| return ReaderFactory.newPlatformReader(source); |
| } |
| } |
| |
| private Writer getWriter(OutputStream os) throws IOException { |
| if (encoding != null) { |
| return new OutputStreamWriter(os, encoding); |
| } else { |
| return WriterFactory.newPlatformWriter(os); |
| } |
| } |
| |
| /** |
| * If the transformation result fits in memory and the destination file already exists |
| * then both are compared. |
| * <p>If destination file is byte-by-byte equal, then it is not overwritten. |
| * This improves subsequent compilation times since upstream plugins property see that |
| * the resource was not modified. |
| * <p>Note: the method should be called after {@link DeferredFileOutputStream#close} |
| * |
| * @param outStream Deferred stream |
| * @throws IOException On IO error. |
| */ |
| private void fileWriteIfDiffers(DeferredFileOutputStream outStream) throws IOException { |
| File file = outStream.getFile(); |
| if (outStream.isThresholdExceeded()) { |
| getLog().info("File " + file + " was overwritten due to content limit threshold " + outStream.getThreshold() |
| + " reached"); |
| return; |
| } |
| boolean needOverwrite = true; |
| |
| if (file.exists()) { |
| try (InputStream is = Files.newInputStream(file.toPath()); |
| InputStream newContents = new ByteArrayInputStream(outStream.getData())) { |
| needOverwrite = !IOUtil.contentEquals(is, newContents); |
| if (getLog().isDebugEnabled()) { |
| getLog().debug("File " + file + " contents " + (needOverwrite ? "differs" : "does not differ")); |
| } |
| } |
| } |
| |
| if (!needOverwrite) { |
| getLog().debug("File " + file + " is up to date"); |
| return; |
| } |
| getLog().debug("Writing " + file); |
| |
| try (OutputStream os = Files.newOutputStream(file.toPath())) { |
| outStream.writeTo(os); |
| } |
| } |
| |
| private MavenFileFilterRequest setupRequest(Resource resource, File source, File file) { |
| MavenFileFilterRequest req = new MavenFileFilterRequest(); |
| req.setFrom(source); |
| req.setTo(file); |
| req.setFiltering(resource.isFiltering()); |
| |
| req.setMavenProject(project); |
| req.setMavenSession(mavenSession); |
| req.setInjectProjectBuildFilters(true); |
| |
| if (encoding != null) { |
| req.setEncoding(encoding); |
| } |
| |
| if (filterDelimiters != null && !filterDelimiters.isEmpty()) { |
| LinkedHashSet<String> delims = new LinkedHashSet<>(); |
| if (useDefaultFilterDelimiters) { |
| delims.addAll(req.getDelimiters()); |
| } |
| |
| for (String delim : filterDelimiters) { |
| if (delim == null) { |
| delims.add("${*}"); |
| } else { |
| delims.add(delim); |
| } |
| } |
| |
| req.setDelimiters(delims); |
| } |
| |
| return req; |
| } |
| |
| protected void validate() throws MojoExecutionException { |
| int bundleCount = 1; |
| |
| for (String artifactDescriptor : resourceBundles) { |
| // groupId:artifactId:version, groupId:artifactId:version:type |
| // or groupId:artifactId:version:type:classifier |
| String[] s = StringUtils.split(artifactDescriptor, ":"); |
| |
| if (s.length < 3 || s.length > 5) { |
| String position; |
| |
| if (bundleCount == 1) { |
| position = "1st"; |
| } else if (bundleCount == 2) { |
| position = "2nd"; |
| } else if (bundleCount == 3) { |
| position = "3rd"; |
| } else { |
| position = bundleCount + "th"; |
| } |
| |
| throw new MojoExecutionException("The " + position |
| + " resource bundle configured must specify a groupId, artifactId, " |
| + " version and, optionally, type and classifier for a remote resource bundle. " |
| + "Must be of the form <resourceBundle>groupId:artifactId:version</resourceBundle>, " |
| + "<resourceBundle>groupId:artifactId:version:type</resourceBundle> or " |
| + "<resourceBundle>groupId:artifactId:version:type:classifier</resourceBundle>"); |
| } |
| |
| bundleCount++; |
| } |
| } |
| |
| private static final String KEY_PROJECTS = "projects"; |
| private static final String KEY_PROJECTS_ORGS = "projectsSortedByOrganization"; |
| |
| protected VelocityContext buildVelocityContext(Map<String, Object> properties) { |
| // the following properties are expensive to calculate, so we provide them lazily |
| VelocityContext context = new VelocityContext(properties) { |
| @Override |
| public Object internalGet(String key) { |
| Object result = super.internalGet(key); |
| if (result == null && key != null && key.startsWith(KEY_PROJECTS) && containsKey(key)) { |
| // calculate and put projects* properties |
| List<MavenProject> projects = getProjects(); |
| put(KEY_PROJECTS, projects); |
| put(KEY_PROJECTS_ORGS, getProjectsSortedByOrganization(projects)); |
| return super.internalGet(key); |
| } |
| return result; |
| } |
| }; |
| // to have a consistent getKeys()/containsKey() behaviour, keys must be present from the start |
| context.put(KEY_PROJECTS, null); |
| context.put(KEY_PROJECTS_ORGS, null); |
| // the following properties are cheap to calculate, so we provide them eagerly |
| |
| // Reproducible Builds: try to use reproducible output timestamp |
| MavenArchiver archiver = new MavenArchiver(); |
| Date outputDate = archiver.parseOutputTimestamp(outputTimestamp); |
| |
| String inceptionYear = project.getInceptionYear(); |
| String year = new SimpleDateFormat("yyyy").format((outputDate == null) ? new Date() : outputDate); |
| |
| if (inceptionYear == null || inceptionYear.isEmpty()) { |
| if (getLog().isDebugEnabled()) { |
| getLog().debug("inceptionYear not specified, defaulting to " + year); |
| } |
| |
| inceptionYear = year; |
| } |
| context.put("project", project); |
| context.put("presentYear", year); |
| context.put("locator", locator); |
| |
| if (inceptionYear.equals(year)) { |
| context.put("projectTimespan", year); |
| } else { |
| context.put("projectTimespan", inceptionYear + "-" + year); |
| } |
| return context; |
| } |
| |
| private List<File> downloadBundles(List<String> bundles) throws MojoExecutionException { |
| List<File> bundleArtifacts = new ArrayList<>(); |
| |
| for (String artifactDescriptor : bundles) { |
| getLog().info("Preparing remote bundle " + artifactDescriptor); |
| // groupId:artifactId:version[:type[:classifier]] |
| String[] s = artifactDescriptor.split(":"); |
| |
| File artifactFile = null; |
| // check if the artifact is part of the reactor |
| if (mavenSession != null) { |
| List<MavenProject> list = mavenSession.getProjects(); |
| for (MavenProject p : list) { |
| if (s[0].equals(p.getGroupId()) && s[1].equals(p.getArtifactId()) && s[2].equals(p.getVersion())) { |
| if (s.length >= 4 && "test-jar".equals(s[3])) { |
| artifactFile = new File(p.getBuild().getTestOutputDirectory()); |
| } else { |
| artifactFile = new File(p.getBuild().getOutputDirectory()); |
| } |
| } |
| } |
| } |
| if (artifactFile == null || !artifactFile.exists()) { |
| String g = s[0]; |
| String a = s[1]; |
| String v = s[2]; |
| String type = s.length >= 4 ? s[3] : "jar"; |
| ArtifactType artifactType = |
| RepositoryUtils.newArtifactType(type, artifactHandlerManager.getArtifactHandler(type)); |
| String classifier = s.length == 5 ? s[4] : artifactType.getClassifier(); |
| |
| DefaultArtifact artifact = |
| new DefaultArtifact(g, a, classifier, artifactType.getExtension(), v, artifactType); |
| |
| try { |
| ArtifactRequest request = |
| new ArtifactRequest(artifact, project.getRemoteProjectRepositories(), "remote-resources"); |
| ArtifactResult result = repoSystem.resolveArtifact(mavenSession.getRepositorySession(), request); |
| artifactFile = result.getArtifact().getFile(); |
| } catch (ArtifactResolutionException e) { |
| throw new MojoExecutionException("Error processing remote resources", e); |
| } |
| } |
| bundleArtifacts.add(artifactFile); |
| } |
| |
| return bundleArtifacts; |
| } |
| |
| private ClassLoader initalizeClassloader(List<File> artifacts) throws MojoExecutionException { |
| RemoteResourcesClassLoader cl = new RemoteResourcesClassLoader(null); |
| try { |
| for (File artifact : artifacts) { |
| cl.addURL(artifact.toURI().toURL()); |
| } |
| return cl; |
| } catch (MalformedURLException e) { |
| throw new MojoExecutionException("Unable to configure resources classloader: " + e.getMessage(), e); |
| } |
| } |
| |
| protected void processResourceBundles(ClassLoader classLoader, VelocityContext context) |
| throws MojoExecutionException { |
| List<Map.Entry<String, RemoteResourcesBundle>> remoteResources = new ArrayList<>(); |
| int bundleCount = 0; |
| int resourceCount = 0; |
| |
| // list remote resources form bundles |
| try { |
| RemoteResourcesBundleXpp3Reader bundleReader = new RemoteResourcesBundleXpp3Reader(); |
| |
| for (Enumeration<URL> e = classLoader.getResources(BundleRemoteResourcesMojo.RESOURCES_MANIFEST); |
| e.hasMoreElements(); ) { |
| URL url = e.nextElement(); |
| bundleCount++; |
| getLog().debug("processResourceBundle on bundle#" + bundleCount + " " + url); |
| |
| RemoteResourcesBundle bundle; |
| |
| try (InputStream in = url.openStream()) { |
| bundle = bundleReader.read(in); |
| } |
| |
| int n = 0; |
| for (String bundleResource : bundle.getRemoteResources()) { |
| n++; |
| resourceCount++; |
| getLog().debug("bundle#" + bundleCount + " resource#" + n + " " + bundleResource); |
| remoteResources.add(new AbstractMap.SimpleEntry<>(bundleResource, bundle)); |
| } |
| } |
| } catch (IOException ioe) { |
| throw new MojoExecutionException("Error finding remote resources manifests", ioe); |
| } catch (XmlPullParserException xppe) { |
| throw new MojoExecutionException("Error parsing remote resource bundle descriptor.", xppe); |
| } |
| |
| getLog().info("Copying " + resourceCount + " resource" + ((resourceCount > 1) ? "s" : "") + " from " |
| + bundleCount + " bundle" + ((bundleCount > 1) ? "s" : "") + "."); |
| |
| String velocityResource = null; |
| try { |
| |
| for (Map.Entry<String, RemoteResourcesBundle> entry : remoteResources) { |
| String bundleResource = entry.getKey(); |
| RemoteResourcesBundle bundle = entry.getValue(); |
| |
| String projectResource = bundleResource; |
| |
| boolean doVelocity = false; |
| if (projectResource.endsWith(TEMPLATE_SUFFIX)) { |
| projectResource = projectResource.substring(0, projectResource.length() - 3); |
| velocityResource = bundleResource; |
| doVelocity = true; |
| } |
| |
| // Don't overwrite resource that are already being provided. |
| |
| File f = new File(outputDirectory, projectResource); |
| |
| FileUtils.mkdir(f.getParentFile().getAbsolutePath()); |
| |
| if (!copyResourceIfExists(f, projectResource, context)) { |
| if (doVelocity) { |
| try (DeferredFileOutputStream os = |
| new DeferredFileOutputStream(velocityFilterInMemoryThreshold, f)) { |
| try (Writer writer = bundle.getSourceEncoding() == null |
| ? new OutputStreamWriter(os) |
| : new OutputStreamWriter(os, bundle.getSourceEncoding())) { |
| if (bundle.getSourceEncoding() == null) { |
| // TODO: Is this correct? Shouldn't we behave like the rest of maven and fail |
| // down to JVM default instead ISO-8859-1 ? |
| velocity.mergeTemplate(bundleResource, "ISO-8859-1", context, writer); |
| } else { |
| velocity.mergeTemplate(bundleResource, bundle.getSourceEncoding(), context, writer); |
| } |
| } |
| fileWriteIfDiffers(os); |
| } |
| } else { |
| URL resUrl = classLoader.getResource(bundleResource); |
| if (resUrl != null) { |
| FileUtils.copyURLToFile(resUrl, f); |
| } |
| } |
| |
| File appendedResourceFile = new File(appendedResourcesDirectory, projectResource); |
| File appendedVmResourceFile = new File(appendedResourcesDirectory, projectResource + ".vm"); |
| |
| if (appendedResourceFile.exists()) { |
| getLog().info("Copying appended resource: " + projectResource); |
| try (InputStream in = Files.newInputStream(appendedResourceFile.toPath()); |
| OutputStream out = new FileOutputStream(f, true)) { |
| IOUtil.copy(in, out); |
| } |
| |
| } else if (appendedVmResourceFile.exists()) { |
| getLog().info("Filtering appended resource: " + projectResource + ".vm"); |
| |
| try (Reader reader = new FileReader(appendedVmResourceFile); |
| Writer writer = getWriter(bundle, f)) { |
| Velocity.init(); |
| Velocity.evaluate(context, writer, "remote-resources", reader); |
| } |
| } |
| } |
| } |
| } catch (IOException ioe) { |
| throw new MojoExecutionException("Error reading remote resource", ioe); |
| } catch (VelocityException e) { |
| throw new MojoExecutionException("Error rendering Velocity resource '" + velocityResource + "'", e); |
| } |
| } |
| |
| private Writer getWriter(RemoteResourcesBundle bundle, File f) throws IOException { |
| Writer writer; |
| if (bundle.getSourceEncoding() == null) { |
| writer = new PrintWriter(new FileWriter(f, true)); |
| } else { |
| writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(f, true), bundle.getSourceEncoding())); |
| } |
| return writer; |
| } |
| |
| protected Model getSupplement(Xpp3Dom supplementModelXml) throws MojoExecutionException { |
| MavenXpp3Reader modelReader = new MavenXpp3Reader(); |
| Model model = null; |
| |
| try { |
| model = modelReader.read(new StringReader(supplementModelXml.toString())); |
| String groupId = model.getGroupId(); |
| String artifactId = model.getArtifactId(); |
| |
| if (groupId == null || groupId.trim().equals("")) { |
| throw new MojoExecutionException( |
| "Supplemental project XML " + "requires that a <groupId> element be present."); |
| } |
| |
| if (artifactId == null || artifactId.trim().equals("")) { |
| throw new MojoExecutionException( |
| "Supplemental project XML " + "requires that a <artifactId> element be present."); |
| } |
| } catch (IOException e) { |
| getLog().warn("Unable to read supplemental XML: " + e.getMessage(), e); |
| } catch (XmlPullParserException e) { |
| getLog().warn("Unable to parse supplemental XML: " + e.getMessage(), e); |
| } |
| |
| return model; |
| } |
| |
| protected Model mergeModels(Model parent, Model child) { |
| inheritanceAssembler.assembleModelInheritance(child, parent); |
| return child; |
| } |
| |
| private static String generateSupplementMapKey(String groupId, String artifactId) { |
| return groupId.trim() + ":" + artifactId.trim(); |
| } |
| |
| private Map<String, Model> loadSupplements(String[] models) throws MojoExecutionException { |
| if (models == null) { |
| getLog().debug("Supplemental data models won't be loaded. No models specified."); |
| return Collections.emptyMap(); |
| } |
| |
| List<Supplement> supplements = new ArrayList<>(); |
| for (String set : models) { |
| getLog().debug("Preparing ruleset: " + set); |
| try { |
| File f = locator.getResourceAsFile(set, getLocationTemp(set)); |
| |
| if (null == f || !f.exists()) { |
| throw new MojoExecutionException("Cold not resolve " + set); |
| } |
| if (!f.canRead()) { |
| throw new MojoExecutionException("Supplemental data models won't be loaded. " + "File " |
| + f.getAbsolutePath() + " cannot be read, check permissions on the file."); |
| } |
| |
| getLog().debug("Loading supplemental models from " + f.getAbsolutePath()); |
| |
| SupplementalDataModelXpp3Reader reader = new SupplementalDataModelXpp3Reader(); |
| SupplementalDataModel supplementalModel = reader.read(new FileReader(f)); |
| supplements.addAll(supplementalModel.getSupplement()); |
| } catch (Exception e) { |
| String msg = "Error loading supplemental data models: " + e.getMessage(); |
| getLog().error(msg, e); |
| throw new MojoExecutionException(msg, e); |
| } |
| } |
| |
| getLog().debug("Loading supplements complete."); |
| |
| Map<String, Model> supplementMap = new HashMap<>(); |
| for (Supplement sd : supplements) { |
| Xpp3Dom dom = (Xpp3Dom) sd.getProject(); |
| |
| Model m = getSupplement(dom); |
| supplementMap.put(generateSupplementMapKey(m.getGroupId(), m.getArtifactId()), m); |
| } |
| |
| return supplementMap; |
| } |
| |
| /** |
| * Convenience method to get the location of the specified file name. |
| * |
| * @param name the name of the file whose location is to be resolved |
| * @return a String that contains the absolute file name of the file |
| */ |
| private String getLocationTemp(String name) { |
| String loc = name; |
| if (loc.indexOf('/') != -1) { |
| loc = loc.substring(loc.lastIndexOf('/') + 1); |
| } |
| if (loc.indexOf('\\') != -1) { |
| loc = loc.substring(loc.lastIndexOf('\\') + 1); |
| } |
| getLog().debug("Before: " + name + " After: " + loc); |
| return loc; |
| } |
| |
| static class OrganizationComparator implements Comparator<Organization> { |
| @Override |
| public int compare(Organization org1, Organization org2) { |
| int i = compareStrings(org1.getName(), org2.getName()); |
| if (i == 0) { |
| i = compareStrings(org1.getUrl(), org2.getUrl()); |
| } |
| return i; |
| } |
| |
| private int compareStrings(String s1, String s2) { |
| if (s1 == null && s2 == null) { |
| return 0; |
| } else if (s1 == null) { |
| return 1; |
| } else if (s2 == null) { |
| return -1; |
| } |
| |
| return s1.compareToIgnoreCase(s2); |
| } |
| } |
| |
| static class ProjectComparator implements Comparator<MavenProject> { |
| @Override |
| public int compare(MavenProject p1, MavenProject p2) { |
| return p1.getArtifact().compareTo(p2.getArtifact()); |
| } |
| } |
| } |