package hudson.plugins.analysis.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.digester.Digester;
import org.apache.commons.lang.StringUtils;
import org.xml.sax.SAXException;
/**
* Detects module names by parsing the name of a source file, the Maven pom.xml file or the ANT build.xml file.
*
* @author Ulli Hafner
*/
public class ModuleDetector {
private static final String BACK_SLASH = "\\";
private static final String SLASH = "/";
private static final String ALL_DIRECTORIES = "**/";
/** Filename of Maven pom. */
protected static final String MAVEN_POM = "pom.xml";
/** Filename of Ant project file. */
protected static final String ANT_PROJECT = "build.xml";
/** Prefix of a Maven target folder. */
private static final String TARGET = "/target";
/** The factory to create input streams with. */
private FileInputStreamFactory factory = new DefaultFileInputStreamFactory();
/** Maps file names to module names. */
private final Map<String, String> fileNameToModuleName;
/** Sorted list of file name prefixes. */
private final List<String> prefixes;
/**
* Creates a new instance of {@link ModuleDetector}.
*/
public ModuleDetector() {
fileNameToModuleName = new HashMap<String, String>();
prefixes = new ArrayList<String>();
}
/**
* Creates a new instance of {@link ModuleDetector}.
*
* @param workspace
* the workspace to scan for maven pom.xml or ant build.xml files
*/
public ModuleDetector(final File workspace) {
this(workspace, new DefaultFileInputStreamFactory());
}
/**
* Creates a new instance of {@link ModuleDetector}.
*
* @param workspace
* the workspace to scan for Maven pom.xml or ant build.xml files
* @param fileInputStreamFactory the value to set
*/
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public ModuleDetector(final File workspace, final FileInputStreamFactory fileInputStreamFactory) {
setFileInputStreamFactory(fileInputStreamFactory);
fileNameToModuleName = createFilesToModuleMapping(workspace);
prefixes = new ArrayList<String>(fileNameToModuleName.keySet());
Collections.sort(prefixes);
}
/**
* Sets the factory to the specified value.
*
* @param fileInputStreamFactory the value to set
*/
public final void setFileInputStreamFactory(final FileInputStreamFactory fileInputStreamFactory) {
factory = fileInputStreamFactory;
}
/**
* Returns a mapping of path prefixes to module names.
*
* @param workspace
* the workspace to start scanning for files
* @return the mapping of path prefixes to module names
*/
private Map<String, String> createFilesToModuleMapping(final File workspace) {
String[] projects = findMavenModules(workspace);
Map<String, String> mapping = new HashMap<String, String>();
if (projects.length > 0) {
for (int i = 0; i < projects.length; i++) {
String fileName = projects[i];
String moduleName = parsePom(fileName);
if (StringUtils.isNotBlank(moduleName)) {
mapping.put(StringUtils.substringBeforeLast(fileName, MAVEN_POM), moduleName);
}
}
}
if (mapping.isEmpty()) {
projects = findAntProjects(workspace);
for (int i = 0; i < projects.length; i++) {
String fileName = projects[i];
String moduleName = parseBuildXml(fileName);
if (StringUtils.isNotBlank(moduleName)) {
mapping.put(StringUtils.substringBeforeLast(fileName, ANT_PROJECT), moduleName);
}
}
}
return mapping;
}
/**
* Uses the path prefixes of pom.xml or build.xml files to guess a module
* name for the specified file.
*
* @param originalFileName
* file name to guess a module for
* @return a module name or an empty string
*/
public String guessModuleName(final String originalFileName) {
String fullPath = originalFileName.replace('\\', '/');
String guessedModule = StringUtils.EMPTY;
for (String path : prefixes) {
if (fullPath.startsWith(path)) {
guessedModule = fileNameToModuleName.get(path);
}
}
return guessedModule;
}
/**
* Returns the maven modules in the workspace.
*
* @param workspace the workspace
* @return the maven modules in the workspace
*/
private String[] findMavenModules(final File workspace) {
return find(workspace, ALL_DIRECTORIES + MAVEN_POM);
}
/**
* Returns the Ant projects in the workspace.
*
* @param workspace the workspace
* @return the Ant projects in the workspace
*/
private String[] findAntProjects(final File workspace) {
return find(workspace, ALL_DIRECTORIES + ANT_PROJECT);
}
/**
* Finds files of the matching pattern.
*
* @param path
* root path to scan in
* @param pattern
* pattern of files
* @return the found files
*/
protected String[] find(final File path, final String pattern) {
String[] relativeFileNames = new FileFinder(pattern).find(path);
String[] absoluteFileNames = new String[relativeFileNames.length];
String absolutePath = path.getAbsolutePath();
for (int file = 0; file < absoluteFileNames.length; file++) {
absoluteFileNames[file] = (absolutePath + SLASH + relativeFileNames[file]).replace(BACK_SLASH, SLASH);
}
return absoluteFileNames;
}
/**
* Guesses a module name based on the source folder or the content in the pom.xml or build.xml files.
*
* @param fileName
* the absolute path of the file (UNIX style) to guess the module
* for
* @param isMavenBuild
* determines whether this build uses maven
* @param isAntBuild
* determines whether this build uses maven
* @return the guessed module name or an empty string if the name could not be
* resolved
*/
public String guessModuleName(final String fileName, final boolean isMavenBuild, final boolean isAntBuild) {
String unixName = fileName.replace(BACK_SLASH, SLASH);
if (isMavenBuild) {
String projectName = parsePom(unixName);
if (StringUtils.isNotBlank(projectName)) {
return projectName;
}
}
String path = StringUtils.substringBeforeLast(unixName, SLASH);
if (isAntBuild) {
String projectName = parseBuildXml(path);
if (StringUtils.isNotBlank(projectName)) {
return projectName;
}
}
if (path.contains(SLASH)) {
return StringUtils.substringAfterLast(path, SLASH);
}
else {
return path;
}
}
/**
* Returns the project name stored in the build.xml.
*
* @param path
* root folder
* @return the project name or an empty string if the name could not be
* resolved
*/
private String parseBuildXml(final String path) {
try {
String fileName;
if (StringUtils.isBlank(path)) {
fileName = ANT_PROJECT;
}
else {
fileName = path + "/build.xml";
}
InputStream pom = factory.create(fileName);
Digester digester = new Digester();
digester.setValidating(false);
digester.setClassLoader(ModuleDetector.class.getClassLoader());
digester.push(new StringBuffer());
String xPath = "project";
digester.addCallMethod(xPath, "append", 1);
digester.addCallParam(xPath, 0, "name");
StringBuffer result = (StringBuffer)digester.parse(pom);
return result.toString();
}
catch (IOException exception) {
// ignore
}
catch (SAXException exception) {
// ignore
}
return StringUtils.EMPTY;
}
/**
* Returns the project name stored in the POM.
*
* @param fileName
* maven module root folder
* @return the project name or an empty string if the name could not be
* resolved
*/
private String parsePom(final String fileName) {
try {
InputStream pom = null;
if (fileName.endsWith(MAVEN_POM)) {
pom = factory.create(fileName);
}
else if (fileName.contains(TARGET)) {
String module = StringUtils.substringBeforeLast(fileName, TARGET);
pom = factory.create(module + "/pom.xml");
}
if (pom != null) {
Digester digester = new Digester();
digester.setValidating(false);
digester.setClassLoader(ModuleDetector.class.getClassLoader());
digester.push(new StringBuffer());
digester.addCallMethod("project/name", "append", 0);
StringBuffer result = (StringBuffer)digester.parse(pom);
return result.toString();
}
}
catch (IOException exception) {
// ignore
}
catch (SAXException exception) {
// ignore
}
return StringUtils.EMPTY;
}
/**
* A input stream factory based on a {@link FileInputStream}.
*/
private static final class DefaultFileInputStreamFactory implements FileInputStreamFactory {
/** {@inheritDoc} */
public InputStream create(final String fileName) throws FileNotFoundException {
return new FileInputStream(new File(fileName));
}
}
}