/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.cpd;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.FilenameFilter;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import net.sourceforge.pmd.AbstractConfiguration;
import net.sourceforge.pmd.util.FileFinder;
import net.sourceforge.pmd.util.FileUtil;
import com.beust.jcommander.IStringConverter;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.converters.FileConverter;
/**
*
* @author Brian Remedios
* @author Romain Pelisse - <belaran@gmail.com>
*/
public class CPDConfiguration extends AbstractConfiguration {
public static final String DEFAULT_LANGUAGE = "java";
public static final String DEFAULT_RENDERER = "text";
private static final Map<String, Class<? extends Renderer>> RENDERERS = new HashMap<>();
@Parameter(names = "--language", description = "Sources code language. Default value is " + DEFAULT_LANGUAGE,
required = false, converter = LanguageConverter.class)
private Language language;
@Parameter(names = "--minimum-tokens",
description = "The minimum token length which should be reported as a duplicate.", required = true)
private int minimumTileSize;
@Parameter(names = "--skip-duplicate-files",
description = "Ignore multiple copies of files of the same name and length in comparison", required = false)
private boolean skipDuplicates;
@Parameter(names = "--format", description = "Report format. Default value is " + DEFAULT_RENDERER,
required = false)
private String rendererName;
/**
* The actual renderer. constructed by using the {@link #rendererName}. This
* property is only valid after {@link #postContruct()} has been called!
*/
private Renderer renderer;
private String encoding;
@Parameter(names = "--ignore-literals",
description = "Ignore number values and string contents when comparing text", required = false)
private boolean ignoreLiterals;
@Parameter(names = "--ignore-identifiers", description = "Ignore constant and variable names when comparing text",
required = false)
private boolean ignoreIdentifiers;
@Parameter(names = "--ignore-annotations", description = "Ignore language annotations when comparing text",
required = false)
private boolean ignoreAnnotations;
@Parameter(names = "--ignore-usings", description = "Ignore using directives in C#", required = false)
private boolean ignoreUsings;
@Parameter(names = "--skip-lexical-errors",
description = "Skip files which can't be tokenized due to invalid characters instead of aborting CPD",
required = false)
private boolean skipLexicalErrors = false;
@Parameter(names = "--no-skip-blocks",
description = "Do not skip code blocks marked with --skip-blocks-pattern (e.g. #if 0 until #endif)",
required = false)
private boolean noSkipBlocks = false;
@Parameter(names = "--skip-blocks-pattern",
description = "Pattern to find the blocks to skip. Start and End pattern separated by |. " + "Default is \""
+ Tokenizer.DEFAULT_SKIP_BLOCKS_PATTERN + "\".",
required = false)
private String skipBlocksPattern = Tokenizer.DEFAULT_SKIP_BLOCKS_PATTERN;
@Parameter(names = "--files", variableArity = true, description = "List of files and directories to process",
required = false, converter = FileConverter.class)
private List<File> files;
@Parameter(names = "--filelist", description = "Path to a file containing a list of files to analyze.",
required = false)
private String fileListPath;
@Parameter(names = "--exclude", variableArity = true, description = "Files to be excluded from CPD check",
required = false, converter = FileConverter.class)
private List<File> excludes;
@Parameter(names = "--non-recursive", description = "Don't scan subdirectiories", required = false)
private boolean nonRecursive;
@Parameter(names = "--uri", description = "URI to process", required = false)
private String uri;
@Parameter(names = { "--help", "-h" }, description = "Print help text", required = false, help = true)
private boolean help;
@Parameter(names = { "--failOnViolation", "-failOnViolation" }, arity = 1,
description = "By default CPD exits with status 4 if code duplications are found. Disable this option with '-failOnViolation false' to exit with 0 instead and just write the report.")
private boolean failOnViolation = true;
// this has to be a public static class, so that JCommander can use it!
public static class LanguageConverter implements IStringConverter<Language> {
@Override
public Language convert(String languageString) {
if (languageString == null || "".equals(languageString)) {
languageString = DEFAULT_LANGUAGE;
}
return LanguageFactory.createLanguage(languageString);
}
}
public CPDConfiguration() {
}
@Deprecated
public CPDConfiguration(int minimumTileSize, Language language, String encoding) {
setMinimumTileSize(minimumTileSize);
setLanguage(language);
setEncoding(encoding);
}
@Parameter(names = "--encoding", description = "Character encoding to use when processing files", required = false)
public void setEncoding(String encoding) {
this.encoding = encoding;
setSourceEncoding(encoding);
}
public SourceCode sourceCodeFor(File file) {
return new SourceCode(new SourceCode.FileCodeLoader(file, getSourceEncoding()));
}
public SourceCode sourceCodeFor(Reader reader, String sourceCodeName) {
return new SourceCode(new SourceCode.ReaderCodeLoader(reader, sourceCodeName));
}
public void postContruct() {
if (this.getLanguage() == null) {
this.setLanguage(CPDConfiguration.getLanguageFromString(DEFAULT_LANGUAGE));
}
if (this.getRendererName() == null) {
this.setRendererName(DEFAULT_RENDERER);
}
if (this.getRenderer() == null) {
this.setRenderer(getRendererFromString(getRendererName(), this.getEncoding()));
}
}
/**
* Gets a renderer with the platform's default encoding.
*
* @param name
* renderer name
* @return a fresh renderer instance
* @deprecated use {@link #getRendererFromString(String, String)} instead
*/
@Deprecated
public static Renderer getRendererFromString(String name) {
return getRendererFromString(name, System.getProperty("file.encoding"));
}
static {
RENDERERS.put(DEFAULT_RENDERER, SimpleRenderer.class);
RENDERERS.put("xml", XMLRenderer.class);
RENDERERS.put("csv", CSVRenderer.class);
RENDERERS.put("csv_with_linecount_per_file", CSVWithLinecountPerFileRenderer.class);
RENDERERS.put("vs", VSRenderer.class);
}
public static Renderer getRendererFromString(String name, String encoding) {
String clazzname = name;
if (clazzname == null || "".equals(clazzname)) {
clazzname = DEFAULT_RENDERER;
}
Class<? extends Renderer> clazz = RENDERERS.get(clazzname.toLowerCase(Locale.ROOT));
if (clazz == null) {
try {
clazz = Class.forName(clazzname).asSubclass(Renderer.class);
} catch (ClassNotFoundException e) {
System.err.println("Can't find class '" + name + "', defaulting to SimpleRenderer.");
clazz = SimpleRenderer.class;
}
}
try {
Renderer renderer = clazz.getDeclaredConstructor().newInstance();
setRendererEncoding(renderer, encoding);
return renderer;
} catch (Exception e) {
System.err.println("Couldn't instantiate renderer, defaulting to SimpleRenderer: " + e);
return new SimpleRenderer();
}
}
private static void setRendererEncoding(Renderer renderer, String encoding)
throws IllegalAccessException, InvocationTargetException {
try {
PropertyDescriptor encodingProperty = new PropertyDescriptor("encoding", renderer.getClass());
Method method = encodingProperty.getWriteMethod();
if (method != null) {
method.invoke(renderer, encoding);
}
} catch (IntrospectionException e) {
// ignored - maybe this renderer doesn't have a encoding property
}
}
public static String[] getRenderers() {
String[] result = RENDERERS.keySet().toArray(new String[RENDERERS.size()]);
Arrays.sort(result);
return result;
}
public static Language getLanguageFromString(String languageString) {
return LanguageFactory.createLanguage(languageString);
}
public static void setSystemProperties(CPDConfiguration configuration) {
Properties properties = new Properties();
if (configuration.isIgnoreLiterals()) {
properties.setProperty(Tokenizer.IGNORE_LITERALS, "true");
} else {
properties.remove(Tokenizer.IGNORE_LITERALS);
}
if (configuration.isIgnoreIdentifiers()) {
properties.setProperty(Tokenizer.IGNORE_IDENTIFIERS, "true");
} else {
properties.remove(Tokenizer.IGNORE_IDENTIFIERS);
}
if (configuration.isIgnoreAnnotations()) {
properties.setProperty(Tokenizer.IGNORE_ANNOTATIONS, "true");
} else {
properties.remove(Tokenizer.IGNORE_ANNOTATIONS);
}
if (configuration.isIgnoreUsings()) {
properties.setProperty(Tokenizer.IGNORE_USINGS, "true");
} else {
properties.remove(Tokenizer.IGNORE_USINGS);
}
properties.setProperty(Tokenizer.OPTION_SKIP_BLOCKS, Boolean.toString(!configuration.isNoSkipBlocks()));
properties.setProperty(Tokenizer.OPTION_SKIP_BLOCKS_PATTERN, configuration.getSkipBlocksPattern());
configuration.getLanguage().setProperties(properties);
}
public Language getLanguage() {
return language;
}
public void setLanguage(Language language) {
this.language = language;
}
public int getMinimumTileSize() {
return minimumTileSize;
}
public void setMinimumTileSize(int minimumTileSize) {
this.minimumTileSize = minimumTileSize;
}
public boolean isSkipDuplicates() {
return skipDuplicates;
}
public void setSkipDuplicates(boolean skipDuplicates) {
this.skipDuplicates = skipDuplicates;
}
public String getRendererName() {
return rendererName;
}
public void setRendererName(String rendererName) {
this.rendererName = rendererName;
}
public Renderer getRenderer() {
return renderer;
}
public Tokenizer tokenizer() {
if (language == null) {
throw new IllegalStateException("Language is null.");
}
return language.getTokenizer();
}
public FilenameFilter filenameFilter() {
if (language == null) {
throw new IllegalStateException("Language is null.");
}
final FilenameFilter languageFilter = language.getFileFilter();
final Set<String> exclusions = new HashSet<>();
if (excludes != null) {
FileFinder finder = new FileFinder();
for (File excludedFile : excludes) {
if (excludedFile.isDirectory()) {
List<File> files = finder.findFilesFrom(excludedFile, languageFilter, true);
for (File f : files) {
exclusions.add(FileUtil.normalizeFilename(f.getAbsolutePath()));
}
} else {
exclusions.add(FileUtil.normalizeFilename(excludedFile.getAbsolutePath()));
}
}
}
FilenameFilter filter = new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
File f = new File(dir, name);
if (exclusions.contains(FileUtil.normalizeFilename(f.getAbsolutePath()))) {
System.err.println("Excluding " + f.getAbsolutePath());
return false;
}
return languageFilter.accept(dir, name);
}
};
return filter;
}
public void setRenderer(Renderer renderer) {
this.renderer = renderer;
}
public boolean isIgnoreLiterals() {
return ignoreLiterals;
}
public void setIgnoreLiterals(boolean ignoreLiterals) {
this.ignoreLiterals = ignoreLiterals;
}
public boolean isIgnoreIdentifiers() {
return ignoreIdentifiers;
}
public void setIgnoreIdentifiers(boolean ignoreIdentifiers) {
this.ignoreIdentifiers = ignoreIdentifiers;
}
public boolean isIgnoreAnnotations() {
return ignoreAnnotations;
}
public void setIgnoreAnnotations(boolean ignoreAnnotations) {
this.ignoreAnnotations = ignoreAnnotations;
}
public boolean isIgnoreUsings() {
return ignoreUsings;
}
public void setIgnoreUsings(boolean ignoreUsings) {
this.ignoreUsings = ignoreUsings;
}
public boolean isSkipLexicalErrors() {
return skipLexicalErrors;
}
public void setSkipLexicalErrors(boolean skipLexicalErrors) {
this.skipLexicalErrors = skipLexicalErrors;
}
public List<File> getFiles() {
return files;
}
public void setFiles(List<File> files) {
this.files = files;
}
public String getFileListPath() {
return fileListPath;
}
public void setFileListPath(String fileListPath) {
this.fileListPath = fileListPath;
}
public String getURI() {
return uri;
}
public void setURI(String uri) {
this.uri = uri;
}
public List<File> getExcludes() {
return excludes;
}
public void setExcludes(List<File> excludes) {
this.excludes = excludes;
}
public boolean isNonRecursive() {
return nonRecursive;
}
public void setNonRecursive(boolean nonRecursive) {
this.nonRecursive = nonRecursive;
}
public boolean isHelp() {
return help;
}
public void setHelp(boolean help) {
this.help = help;
}
public String getEncoding() {
return encoding;
}
public boolean isNoSkipBlocks() {
return noSkipBlocks;
}
public void setNoSkipBlocks(boolean noSkipBlocks) {
this.noSkipBlocks = noSkipBlocks;
}
public String getSkipBlocksPattern() {
return skipBlocksPattern;
}
public void setSkipBlocksPattern(String skipBlocksPattern) {
this.skipBlocksPattern = skipBlocksPattern;
}
public boolean isFailOnViolation() {
return failOnViolation;
}
public void setFailOnViolation(boolean failOnViolation) {
this.failOnViolation = failOnViolation;
}
}