blob: 0c8688f8db135140f027f81b9727a31058bc8f54 [file] [log] [blame]
/*
* 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.shared.filtering;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.model.Resource;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.Scanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonatype.plexus.build.incremental.BuildContext;
import static java.util.Objects.requireNonNull;
/**
* @author Olivier Lamy
*/
@Singleton
@Named
public class DefaultMavenResourcesFiltering implements MavenResourcesFiltering {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMavenResourcesFiltering.class);
private static final String[] EMPTY_STRING_ARRAY = {};
private static final String[] DEFAULT_INCLUDES = {"**/**"};
private final List<String> defaultNonFilteredFileExtensions;
private final MavenFileFilter mavenFileFilter;
private final BuildContext buildContext;
@Inject
public DefaultMavenResourcesFiltering(MavenFileFilter mavenFileFilter, BuildContext buildContext) {
this.mavenFileFilter = requireNonNull(mavenFileFilter);
this.buildContext = requireNonNull(buildContext);
this.defaultNonFilteredFileExtensions = new ArrayList<>(5);
this.defaultNonFilteredFileExtensions.add("jpg");
this.defaultNonFilteredFileExtensions.add("jpeg");
this.defaultNonFilteredFileExtensions.add("gif");
this.defaultNonFilteredFileExtensions.add("bmp");
this.defaultNonFilteredFileExtensions.add("png");
this.defaultNonFilteredFileExtensions.add("ico");
}
@Override
public boolean filteredFileExtension(String fileName, List<String> userNonFilteredFileExtensions) {
List<String> nonFilteredFileExtensions = new ArrayList<>(getDefaultNonFilteredFileExtensions());
if (userNonFilteredFileExtensions != null) {
nonFilteredFileExtensions.addAll(userNonFilteredFileExtensions);
}
String extension = getExtension(fileName);
boolean filteredFileExtension = !nonFilteredFileExtensions.contains(extension);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("file " + fileName + " has a" + (filteredFileExtension ? " " : " non ")
+ "filtered file extension");
}
return filteredFileExtension;
}
private static String getExtension(String fileName) {
String rawExt = FilenameUtils.getExtension(fileName);
return rawExt == null ? null : rawExt.toLowerCase(Locale.ROOT);
}
@Override
public List<String> getDefaultNonFilteredFileExtensions() {
return this.defaultNonFilteredFileExtensions;
}
@Override
public void filterResources(MavenResourcesExecution mavenResourcesExecution) throws MavenFilteringException {
if (mavenResourcesExecution == null) {
throw new MavenFilteringException("mavenResourcesExecution cannot be null");
}
if (mavenResourcesExecution.getResources() == null) {
LOGGER.info("No resources configured skip copying/filtering");
return;
}
if (mavenResourcesExecution.getOutputDirectory() == null) {
throw new MavenFilteringException("outputDirectory cannot be null");
}
if (mavenResourcesExecution.isUseDefaultFilterWrappers()) {
handleDefaultFilterWrappers(mavenResourcesExecution);
}
if (mavenResourcesExecution.getEncoding() == null
|| mavenResourcesExecution.getEncoding().length() < 1) {
LOGGER.warn("Using platform encoding (" + System.getProperty("file.encoding")
+ " actually) to copy filtered resources, i.e. build is platform dependent!");
} else {
LOGGER.debug("Using '" + mavenResourcesExecution.getEncoding() + "' encoding to copy filtered resources.");
}
if (mavenResourcesExecution.getPropertiesEncoding() == null
|| mavenResourcesExecution.getPropertiesEncoding().length() < 1) {
LOGGER.debug("Using '" + mavenResourcesExecution.getEncoding()
+ "' encoding to copy filtered properties files.");
} else {
LOGGER.debug("Using '" + mavenResourcesExecution.getPropertiesEncoding()
+ "' encoding to copy filtered properties files.");
}
// Keep track of filtering being used and the properties files being filtered
boolean isFilteringUsed = false;
List<File> propertiesFiles = new ArrayList<>();
for (Resource resource : mavenResourcesExecution.getResources()) {
if (LOGGER.isDebugEnabled()) {
String ls = System.lineSeparator();
StringBuilder debugMessage = new StringBuilder("resource with targetPath ")
.append(resource.getTargetPath())
.append(ls);
debugMessage
.append("directory ")
.append(resource.getDirectory())
.append(ls);
// @formatter:off
debugMessage
.append("excludes ")
.append(
resource.getExcludes() == null
? " empty "
: resource.getExcludes().toString())
.append(ls);
debugMessage
.append("includes ")
.append(
resource.getIncludes() == null
? " empty "
: resource.getIncludes().toString());
// @formatter:on
LOGGER.debug(debugMessage.toString());
}
String targetPath = resource.getTargetPath();
File resourceDirectory = (resource.getDirectory() == null) ? null : new File(resource.getDirectory());
if (resourceDirectory != null && !resourceDirectory.isAbsolute()) {
resourceDirectory =
new File(mavenResourcesExecution.getResourcesBaseDirectory(), resourceDirectory.getPath());
}
if (resourceDirectory == null || !resourceDirectory.exists()) {
LOGGER.info("skip non existing resourceDirectory " + resourceDirectory);
continue;
}
// this part is required in case the user specified "../something"
// as destination
// see MNG-1345
File outputDirectory = mavenResourcesExecution.getOutputDirectory();
if (!outputDirectory.mkdirs() && !outputDirectory.exists()) {
throw new MavenFilteringException("Cannot create resource output directory: " + outputDirectory);
}
if (resource.isFiltering()) {
isFilteringUsed = true;
}
boolean filtersFileChanged = buildContext.hasDelta(mavenResourcesExecution.getFileFilters());
Path resourcePath = resourceDirectory.toPath();
DirectoryScanner scanner = new DirectoryScanner() {
@Override
protected boolean isSelected(String name, File file) {
if (filtersFileChanged) {
// if the file filters file has a change we must assume everything is out of
// date
return true;
}
if (file.isFile()) {
try {
File targetFile = getTargetFile(file);
if (targetFile.isFile() && buildContext.isUptodate(targetFile, file)) {
return false;
}
} catch (MavenFilteringException e) {
// can't really do anything than to assume we must copy the file...
}
}
return true;
}
private File getTargetFile(File file) throws MavenFilteringException {
Path relativize = resourcePath.relativize(file.toPath());
return getDestinationFile(
outputDirectory, targetPath, relativize.toString(), mavenResourcesExecution);
}
};
scanner.setBasedir(resourceDirectory);
setupScanner(resource, scanner, mavenResourcesExecution.isAddDefaultExcludes());
scanner.scan();
if (mavenResourcesExecution.isIncludeEmptyDirs()) {
try {
File targetDirectory = targetPath == null ? outputDirectory : new File(outputDirectory, targetPath);
copyDirectoryLayout(resourceDirectory, targetDirectory, scanner);
} catch (IOException e) {
throw new MavenFilteringException("Cannot copy directory structure from "
+ resourceDirectory.getPath() + " to " + outputDirectory.getPath());
}
}
List<String> includedFiles = Arrays.asList(scanner.getIncludedFiles());
try {
Path basedir = mavenResourcesExecution
.getMavenProject()
.getBasedir()
.getAbsoluteFile()
.toPath();
Path destination = getDestinationFile(outputDirectory, targetPath, "", mavenResourcesExecution)
.getAbsoluteFile()
.toPath();
String origin = basedir.relativize(
resourceDirectory.getAbsoluteFile().toPath())
.toString();
if (StringUtils.isEmpty(origin)) {
origin = ".";
}
LOGGER.info("Copying " + includedFiles.size() + " resource" + (includedFiles.size() > 1 ? "s" : "")
+ " from "
+ origin
+ " to "
+ basedir.relativize(destination));
} catch (Exception e) {
// be foolproof: if for ANY reason throws, do not abort, just fall back to old message
LOGGER.info("Copying " + includedFiles.size() + " resource" + (includedFiles.size() > 1 ? "s" : "")
+ (targetPath == null ? "" : " to " + targetPath));
}
for (String name : includedFiles) {
LOGGER.debug("Copying file " + name);
File source = new File(resourceDirectory, name);
File destinationFile = getDestinationFile(outputDirectory, targetPath, name, mavenResourcesExecution);
if (mavenResourcesExecution.isFlatten() && destinationFile.exists()) {
if (mavenResourcesExecution.isOverwrite()) {
LOGGER.warn("existing file " + destinationFile.getName() + " will be overwritten by " + name);
} else {
throw new MavenFilteringException("existing file " + destinationFile.getName()
+ " will be overwritten by " + name + " and overwrite was not set to true");
}
}
boolean filteredExt =
filteredFileExtension(source.getName(), mavenResourcesExecution.getNonFilteredFileExtensions());
if (resource.isFiltering() && isPropertiesFile(source)) {
propertiesFiles.add(source);
}
// Determine which encoding to use when filtering this file
String encoding = getEncoding(
source, mavenResourcesExecution.getEncoding(), mavenResourcesExecution.getPropertiesEncoding());
LOGGER.debug("Using '" + encoding + "' encoding to copy filtered resource '" + source.getName() + "'.");
mavenFileFilter.copyFile(
source,
destinationFile,
resource.isFiltering() && filteredExt,
mavenResourcesExecution.getFilterWrappers(),
encoding,
mavenResourcesExecution.isOverwrite());
}
// deal with deleted source files
Scanner deleteScanner = buildContext.newDeleteScanner(resourceDirectory);
setupScanner(resource, deleteScanner, mavenResourcesExecution.isAddDefaultExcludes());
deleteScanner.scan();
for (String name : deleteScanner.getIncludedFiles()) {
File destinationFile = getDestinationFile(outputDirectory, targetPath, name, mavenResourcesExecution);
destinationFile.delete();
buildContext.refresh(destinationFile);
}
}
// Warn the user if all of the following requirements are met, to avoid those that are not affected
// - the propertiesEncoding parameter has not been set
// - properties is a filtered extension
// - filtering is enabled for at least one resource
// - there is at least one properties file in one of the resources that has filtering enabled
if ((mavenResourcesExecution.getPropertiesEncoding() == null
|| mavenResourcesExecution.getPropertiesEncoding().length() < 1)
&& !mavenResourcesExecution.getNonFilteredFileExtensions().contains("properties")
&& isFilteringUsed
&& propertiesFiles.size() > 0) {
// @todo Sometime in the future we should change this to be a warning
LOGGER.info("The encoding used to copy filtered properties files has not been set."
+ " This means that the same encoding will be used to copy filtered properties files"
+ " as when copying other filtered resources. This might not be what you want!"
+ " Run your build with --debug to see which files might be affected."
+ " Read more at "
+ "https://maven.apache.org/plugins/maven-resources-plugin/"
+ "examples/filtering-properties-files.html");
StringBuilder affectedFiles = new StringBuilder();
affectedFiles.append("Here is a list of the filtered properties files in your project that might be"
+ " affected by encoding problems: ");
for (File propertiesFile : propertiesFiles) {
affectedFiles.append(System.lineSeparator()).append(" - ").append(propertiesFile.getPath());
}
LOGGER.debug(affectedFiles.toString());
}
}
/**
* Get the encoding to use when filtering the specified file. Properties files can be configured to use a different
* encoding than regular files.
*
* @param file The file to check
* @param encoding The encoding to use for regular files
* @param propertiesEncoding The encoding to use for properties files
* @return The encoding to use when filtering the specified file
* @since 3.2.0
*/
static String getEncoding(File file, String encoding, String propertiesEncoding) {
if (isPropertiesFile(file)) {
if (propertiesEncoding == null) {
// Since propertiesEncoding is a new feature, not all plugins will have implemented support for it.
// These plugins will have propertiesEncoding set to null.
return encoding;
} else {
return propertiesEncoding;
}
} else {
return encoding;
}
}
/**
* Determine whether a file is a properties file or not.
*
* @param file The file to check
* @return <code>true</code> if the file name has an extension of "properties", otherwise <code>false</code>
* @since 3.2.0
*/
static boolean isPropertiesFile(File file) {
return "properties".equals(getExtension(file.getName()));
}
private void handleDefaultFilterWrappers(MavenResourcesExecution mavenResourcesExecution)
throws MavenFilteringException {
List<FilterWrapper> filterWrappers = new ArrayList<>();
if (mavenResourcesExecution.getFilterWrappers() != null) {
filterWrappers.addAll(mavenResourcesExecution.getFilterWrappers());
}
filterWrappers.addAll(mavenFileFilter.getDefaultFilterWrappers(mavenResourcesExecution));
mavenResourcesExecution.setFilterWrappers(filterWrappers);
}
private File getDestinationFile(
File outputDirectory, String targetPath, String name, MavenResourcesExecution mavenResourcesExecution)
throws MavenFilteringException {
String destination;
if (!mavenResourcesExecution.isFlatten()) {
destination = name;
} else {
Path path = Paths.get(name);
Path filePath = path.getFileName();
destination = filePath.toString();
}
if (mavenResourcesExecution.isFilterFilenames()
&& mavenResourcesExecution.getFilterWrappers().size() > 0) {
destination = filterFileName(destination, mavenResourcesExecution.getFilterWrappers());
}
if (targetPath != null) {
destination = targetPath + "/" + destination;
}
File destinationFile = new File(destination);
if (!destinationFile.isAbsolute()) {
destinationFile = new File(outputDirectory, destination);
}
if (!destinationFile.getParentFile().exists()) {
destinationFile.getParentFile().mkdirs();
}
return destinationFile;
}
private String[] setupScanner(Resource resource, Scanner scanner, boolean addDefaultExcludes) {
String[] includes;
if (resource.getIncludes() != null && !resource.getIncludes().isEmpty()) {
includes = resource.getIncludes().toArray(EMPTY_STRING_ARRAY);
} else {
includes = DEFAULT_INCLUDES;
}
scanner.setIncludes(includes);
String[] excludes = null;
if (resource.getExcludes() != null && !resource.getExcludes().isEmpty()) {
excludes = resource.getExcludes().toArray(EMPTY_STRING_ARRAY);
scanner.setExcludes(excludes);
}
if (addDefaultExcludes) {
scanner.addDefaultExcludes();
}
return includes;
}
private void copyDirectoryLayout(File sourceDirectory, File destinationDirectory, Scanner scanner)
throws IOException {
if (sourceDirectory == null) {
throw new IOException("source directory can't be null.");
}
if (destinationDirectory == null) {
throw new IOException("destination directory can't be null.");
}
if (sourceDirectory.equals(destinationDirectory)) {
throw new IOException("source and destination are the same directory.");
}
if (!sourceDirectory.exists()) {
throw new IOException("Source directory doesn't exist (" + sourceDirectory.getAbsolutePath() + ").");
}
for (String name : scanner.getIncludedDirectories()) {
File source = new File(sourceDirectory, name);
if (source.equals(sourceDirectory)) {
continue;
}
File destination = new File(destinationDirectory, name);
destination.mkdirs();
}
}
private String getRelativeOutputDirectory(MavenResourcesExecution execution) {
String relOutDir = execution.getOutputDirectory().getAbsolutePath();
if (execution.getMavenProject() != null && execution.getMavenProject().getBasedir() != null) {
String basedir = execution.getMavenProject().getBasedir().getAbsolutePath();
relOutDir = FilteringUtils.getRelativeFilePath(basedir, relOutDir);
if (relOutDir == null) {
relOutDir = execution.getOutputDirectory().getPath();
} else {
relOutDir = relOutDir.replace('\\', '/');
}
}
return relOutDir;
}
/*
* Filter the name of a file using the same mechanism for filtering the content of the file.
*/
private String filterFileName(String name, List<FilterWrapper> wrappers) throws MavenFilteringException {
Reader reader = new StringReader(name);
for (FilterWrapper wrapper : wrappers) {
reader = wrapper.getReader(reader);
}
try (StringWriter writer = new StringWriter()) {
IOUtils.copy(reader, writer);
String filteredFilename = writer.toString();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("renaming filename " + name + " to " + filteredFilename);
}
return filteredFilename;
} catch (IOException e) {
throw new MavenFilteringException("Failed filtering filename" + name, e);
}
}
}