/** * Copyright (c) 2014 Takari, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package io.takari.maven.plugins.testproperties; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringReader; import java.io.StringWriter; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.execution.ProjectDependencyGraph; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import org.eclipse.m2e.workspace.MutableWorkspaceState; import com.google.common.collect.ImmutableSet; import io.takari.incrementalbuild.BasicBuildContext; import io.takari.incrementalbuild.Incremental; import io.takari.incrementalbuild.Incremental.Configuration; import io.takari.incrementalbuild.ResourceMetadata; import io.takari.maven.plugins.util.PropertiesWriter; import io.takari.resources.filtering.ResourcesProcessor; @Mojo(name = "testProperties", requiresDependencyResolution = ResolutionScope.TEST, configurator = "takari", threadSafe = true) public class TestPropertiesMojo extends AbstractMojo { @Parameter(defaultValue = "${project.properties}", readonly = true) private Properties projectProperties; @Parameter(defaultValue = "${session.executionProperties}", readonly = true) private Properties sessionProperties; @Parameter(defaultValue = "${project}", readonly = true) @Incremental(configuration = Configuration.ignore) protected MavenProject project; @Parameter(defaultValue = "${project.groupId}", readonly = true) private String groupId; @Parameter(defaultValue = "${project.artifactId}", readonly = true) private String artifactId; @Parameter(defaultValue = "${project.version}", readonly = true) private String version; @Parameter(defaultValue = "${localRepository}", readonly = true) private ArtifactRepository localRepository; @Parameter(defaultValue = "${session.request.userSettingsFile}", readonly = true) private File userSettingsFile; @Parameter(defaultValue = "${session.request.globalSettingsFile}", readonly = true) private File globalSettingsFile; @Parameter(defaultValue = "${project.basedir}/src/test/test.properties") private File testProperties; @Parameter(defaultValue = "${project.build.testOutputDirectory}/test.properties") private File outputFile; // @Parameter(defaultValue = "${plugin.artifactMap(io.takari.m2e.workspace:org.eclipse.m2e.workspace.cli)}") @Parameter(defaultValue = "${project.artifactMap(io.takari.m2e.workspace:org.eclipse.m2e.workspace.cli)}", readonly = true) private Artifact workspaceResolver; @Parameter(defaultValue = "${project.build.directory}/workspacestate.properties") private File workspaceState; @Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true) private File outputDirectory; @Parameter(defaultValue = "${project.artifacts}", readonly = true) private Set<Artifact> dependencies; @Parameter(defaultValue = "${session.request.offline}", readonly = true) private boolean offline; @Parameter(defaultValue = "${session.request.updateSnapshots}", readonly = true) private boolean updateSnapshots; @Parameter(defaultValue = "${session.projectDependencyGraph}", readonly = true) @Incremental(configuration = Configuration.ignore) private ProjectDependencyGraph reactorDependencies; @Component private BasicBuildContext context; @Component private ResourcesProcessor resourceProcessor; @Override public void execute() throws MojoExecutionException, MojoFailureException { try { Properties properties = new Properties(); if (testProperties.canRead()) { mergeCustomTestProperties(properties); } if (!context.isProcessingRequired()) { return; } // well-known properties, TODO introduce named constants putIfAbsent(properties, "localRepository", localRepository.getBasedir()); if (isAccessible(userSettingsFile)) { putIfAbsent(properties, "userSettingsFile", userSettingsFile.getAbsolutePath()); } else { logWarningNotAccessibleFile(userSettingsFile); } if (isAccessible(globalSettingsFile)) { putIfAbsent(properties, "globalSettingsFile", globalSettingsFile.getAbsolutePath()); } else{ logWarningNotAccessibleFile(globalSettingsFile); } List<ArtifactRepository> repositories = project.getRemoteArtifactRepositories(); for (int i = 0; i < repositories.size(); i++) { properties.put("repository." + i, toString(repositories.get(i))); } putIfAbsent(properties, "offline", Boolean.toString(offline)); putIfAbsent(properties, "updateSnapshots", Boolean.toString(updateSnapshots)); putIfAbsent(properties, "project.groupId", groupId); putIfAbsent(properties, "project.artifactId", artifactId); putIfAbsent(properties, "project.version", version); // project runtime classpath putIfAbsent(properties, "classpath", getClasspathString()); if (workspaceResolver != null && workspaceResolver.getFile() != null) { putIfAbsent(properties, "workspaceResolver", workspaceResolver.getFile().getAbsolutePath()); } writeWorkspaceState(); putIfAbsent(properties, "workspaceStateProperties", workspaceState.getAbsolutePath()); try (OutputStream os = context.processOutput(outputFile).newOutputStream()) { PropertiesWriter.write(properties, "Generated by " + getClass().getName(), os); } } catch (IOException e) { throw new MojoExecutionException("Could not create test.properties file", e); } } private String toString(ArtifactRepository repository) { StringBuilder sb = new StringBuilder(); sb.append("<id>").append(repository.getId()).append("</id>"); sb.append("<url>").append(repository.getUrl()).append("</url>"); sb.append("<releases><enabled>").append(repository.getReleases().isEnabled()).append("</enabled></releases>"); sb.append("<snapshots><enabled>").append(repository.getSnapshots().isEnabled()).append("</enabled></snapshots>"); return sb.toString(); } private String getClasspathString() { Set<String> scopes = ImmutableSet.of(Artifact.SCOPE_COMPILE, Artifact.SCOPE_RUNTIME); StringBuilder sb = new StringBuilder(); sb.append(outputDirectory.getAbsolutePath()); for (Artifact dependency : dependencies) { if (scopes.contains(dependency.getScope())) { sb.append(File.pathSeparatorChar); sb.append(dependency.getFile().getAbsolutePath()); } } return sb.toString(); } private void writeWorkspaceState() throws MojoExecutionException { MutableWorkspaceState state = new MutableWorkspaceState(); // always include this project's pom and jar artifacts state.putPom(project.getFile(), project.getGroupId(), project.getArtifactId(), project.getVersion()); state.putArtifact(outputDirectory, project.getGroupId(), project.getArtifactId(), // "jar" /* extension */, null/* classifier */, project.getVersion()); if (reactorDependencies != null) { // either runs from m2e or from command line with --non-recursive parameter for (MavenProject other : reactorDependencies.getUpstreamProjects(project, true)) { putProject(state, other); } } try (OutputStream os = context.processOutput(workspaceState).newOutputStream()) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); state.store(buffer); PropertiesWriter.write(buffer.toByteArray(), os); } catch (IOException e) { throw new MojoExecutionException("Could not create reactor state file " + workspaceState, e); } } private static void putProject(MutableWorkspaceState state, MavenProject other) { state.putPom(other.getFile(), other.getGroupId(), other.getArtifactId(), other.getVersion()); if (other.getArtifact().getFile() != null) { putArtifact(state, other.getArtifact()); } for (Artifact artifact : other.getAttachedArtifacts()) { putArtifact(state, artifact); } } private static void putArtifact(MutableWorkspaceState state, Artifact artifact) { state.putArtifact(artifact.getFile(), artifact.getGroupId(), artifact.getArtifactId(), // artifact.getArtifactHandler().getExtension(), artifact.getClassifier(), artifact.getBaseVersion()); } private void mergeCustomTestProperties(Properties properties) throws MojoExecutionException { ResourceMetadata<File> metadata = context.registerInput(testProperties); try (InputStream is = new FileInputStream(metadata.getResource())) { Properties custom = new Properties(); custom.load(is); mergeCustomTestProperties(properties, custom); } catch (IOException e) { // TODO create error marker instead throw new MojoExecutionException("Could not read test.properties file " + testProperties, e); } } private void mergeCustomTestProperties(Properties properties, Properties custom) { // resource filtering configuration should match AbstractProcessResourcesMojo // TODO figure out how to move this to a common component Map<Object, Object> substitutes = new HashMap<Object, Object>(); for (Artifact dependency : dependencies) { StringBuilder key = new StringBuilder(); key.append(dependency.getGroupId()); key.append(':').append(dependency.getArtifactId()); if (dependency.getClassifier() != null) { key.append(':').append(dependency.getClassifier()); } substitutes.put(key.toString(), dependency.getFile()); } substitutes.putAll(projectProperties); substitutes.putAll(sessionProperties); substitutes.put("project", project); substitutes.put("localRepository", localRepository); substitutes.put("userSettingsFile", userSettingsFile); for (String key : custom.stringPropertyNames()) { properties.put(key, expand(custom.getProperty(key), substitutes)); } } private static void putIfAbsent(Properties properties, String key, String value) { if (!properties.containsKey(key)) { properties.put(key, value); } } private String expand(String value, Map<Object, Object> substitutes) { StringWriter writer = new StringWriter(); try { resourceProcessor.filter(new StringReader(value), writer, substitutes); return writer.toString(); } catch (IOException e) { return value; // shouldn't happen } } private boolean isAccessible(File file) { return file != null && file.isFile() && file.canRead(); } private void logWarningNotAccessibleFile(File file) { if (file != null) { String msg = "File '" + file.getAbsolutePath() + "' "; if (file.exists() && !file.isFile()) { msg += "exists, but it is not a regular file!"; } else if (file.exists() && file.isFile() && !file.canRead()) { msg += "exists, but can not be read!"; } else { msg += "does not exist!"; } msg += " It will be ignored."; getLog().warn(msg); } } }