blob: ef9d0210a82c3b3336899942bd72046c96e7064d [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
*
* https://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.tools.ant.taskdefs.optional.jdepend;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.ExecuteWatchdog;
import org.apache.tools.ant.taskdefs.LogStreamHandler;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.CommandlineJava;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.PatternSet;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.LoaderUtils;
import jdepend.textui.JDepend;
/**
* Runs JDepend tests.
*
* <p>JDepend is a tool to generate design quality metrics for each Java package.
* It has been initially created by Mike Clark. JDepend can be found at <a
* href="https://github.com/clarkware/jdepend">https://github.com/clarkware/jdepend</a>.
*
* The current implementation spawn a new Java VM.
*
*/
public class JDependTask extends Task {
// required attributes
private Path sourcesPath; // Deprecated!
private Path classesPath; // Use this going forward
// optional attributes
private File outputFile;
private File dir;
private Path compileClasspath;
private boolean haltonerror = false;
private boolean fork = false;
private Long timeout = null;
private String jvm = null;
private String format = "text";
private PatternSet defaultPatterns = new PatternSet();
private static Constructor<?> packageFilterC;
private static Method setFilter;
private boolean includeRuntime = false;
private Path runtimeClasses = null;
static {
try {
Class<?> packageFilter =
Class.forName("jdepend.framework.PackageFilter");
packageFilterC =
packageFilter.getConstructor(Collection.class);
setFilter =
JDepend.class.getDeclaredMethod("setFilter", packageFilter);
} catch (Throwable t) {
if (setFilter == null) {
packageFilterC = null;
}
}
}
/**
* If true,
* include jdepend.jar in the forked VM.
*
* @param b include ant run time yes or no
* @since Ant 1.6
*/
public void setIncluderuntime(boolean b) {
includeRuntime = b;
}
/**
* Set the timeout value (in milliseconds).
*
* <p>If the operation is running for more than this value, the jdepend
* will be canceled. (works only when in 'fork' mode).</p>
* @param value the maximum time (in milliseconds) allowed before
* declaring the test as 'timed-out'
* @see #setFork(boolean)
*/
public void setTimeout(Long value) {
timeout = value;
}
/**
* @return the timeout value
*/
public Long getTimeout() {
return timeout;
}
/**
* The output file name.
*
* @param outputFile the output file name
*/
public void setOutputFile(File outputFile) {
this.outputFile = outputFile;
}
/**
* @return the output file name
*/
public File getOutputFile() {
return outputFile;
}
/**
* Whether or not to halt on failure. Default: false.
* @param haltonerror the value to set
*/
public void setHaltonerror(boolean haltonerror) {
this.haltonerror = haltonerror;
}
/**
* @return the value of the haltonerror attribute
*/
public boolean getHaltonerror() {
return haltonerror;
}
/**
* If true, forks into a new JVM. Default: false.
*
* @param value <code>true</code> if a JVM should be forked,
* otherwise <code>false</code>
*/
public void setFork(boolean value) {
fork = value;
}
/**
* @return the value of the fork attribute
*/
public boolean getFork() {
return fork;
}
/**
* The command used to invoke a forked Java Virtual Machine.
*
* Default is <code>java</code>. Ignored if no JVM is forked.
* @param value the new VM to use instead of <code>java</code>
* @see #setFork(boolean)
*/
public void setJvm(String value) {
jvm = value;
}
/**
* Adds a path to source code to analyze.
* @return a source path
* @deprecated since 1.6.x.
*/
@Deprecated
public Path createSourcespath() {
if (sourcesPath == null) {
sourcesPath = new Path(getProject());
}
return sourcesPath.createPath();
}
/**
* Gets the sourcepath.
* @return the sources path
* @deprecated since 1.6.x.
*/
@Deprecated
public Path getSourcespath() {
return sourcesPath;
}
/**
* Adds a path to class code to analyze.
* @return a classes path
*/
public Path createClassespath() {
if (classesPath == null) {
classesPath = new Path(getProject());
}
return classesPath.createPath();
}
/**
* Gets the classespath.
* @return the classes path
*/
public Path getClassespath() {
return classesPath;
}
/**
* The directory to invoke the VM in. Ignored if no JVM is forked.
* @param dir the directory to invoke the JVM from.
* @see #setFork(boolean)
*/
public void setDir(File dir) {
this.dir = dir;
}
/**
* @return the dir attribute
*/
public File getDir() {
return dir;
}
/**
* Set the classpath to be used for this compilation.
* @param classpath a class path to be used
*/
public void setClasspath(Path classpath) {
if (compileClasspath == null) {
compileClasspath = classpath;
} else {
compileClasspath.append(classpath);
}
}
/**
* Gets the classpath to be used for this compilation.
* @return the class path used for compilation
*/
public Path getClasspath() {
return compileClasspath;
}
/**
* Adds a path to the classpath.
* @return a classpath
*/
public Path createClasspath() {
if (compileClasspath == null) {
compileClasspath = new Path(getProject());
}
return compileClasspath.createPath();
}
/**
* Create a new JVM argument. Ignored if no JVM is forked.
* @param commandline the commandline to create the argument on
* @return create a new JVM argument so that any argument can
* be passed to the JVM.
* @see #setFork(boolean)
*/
public Commandline.Argument createJvmarg(CommandlineJava commandline) {
return commandline.createVmArgument();
}
/**
* Adds a reference to a classpath defined elsewhere.
* @param r a classpath reference
*/
public void setClasspathRef(Reference r) {
createClasspath().setRefid(r);
}
/**
* add a name entry on the exclude list
* @return a pattern for the excludes
*/
public PatternSet.NameEntry createExclude() {
return defaultPatterns.createExclude();
}
/**
* @return the excludes patterns
*/
public PatternSet getExcludes() {
return defaultPatterns;
}
/**
* The format to write the output in, "xml" or "text".
*
* @param ea xml or text
*/
public void setFormat(FormatAttribute ea) {
format = ea.getValue();
}
/**
* A class for the enumerated attribute format,
* values are xml and text.
* @see EnumeratedAttribute
*/
public static class FormatAttribute extends EnumeratedAttribute {
private String[] formats = new String[] {"xml", "text"};
/**
* @return the enumerated values
*/
@Override
public String[] getValues() {
return formats;
}
}
/**
* No problems with this test.
*/
private static final int SUCCESS = 0;
/**
* An error occurred.
*/
private static final int ERRORS = 1;
/**
* Search for the given resource and add the directory or archive
* that contains it to the classpath.
*
* <p>Doesn't work for archives in JDK 1.1 as the URL returned by
* getResource doesn't contain the name of the archive.</p>
*
* @param resource resource that one wants to lookup
* @since Ant 1.6
*/
private void addClasspathEntry(String resource) {
/*
* pre Ant 1.6 this method used to call getClass().getResource
* while Ant 1.6 will call ClassLoader.getResource().
*
* The difference is that Class.getResource expects a leading
* slash for "absolute" resources and will strip it before
* delegating to ClassLoader.getResource - so we now have to
* emulate Class's behavior.
*/
if (resource.startsWith("/")) {
resource = resource.substring(1);
} else {
resource = "org/apache/tools/ant/taskdefs/optional/jdepend/"
+ resource;
}
File f = LoaderUtils.getResourceSource(getClass().getClassLoader(),
resource);
if (f == null) {
log("Couldn't find " + resource, Project.MSG_DEBUG);
} else {
log("Found " + f.getAbsolutePath(), Project.MSG_DEBUG);
runtimeClasses.createPath().setLocation(f);
}
}
/**
* execute the task
*
* @exception BuildException if an error occurs
*/
@Override
public void execute() throws BuildException {
CommandlineJava commandline = new CommandlineJava();
if ("text".equals(format)) {
commandline.setClassname("jdepend.textui.JDepend");
} else if ("xml".equals(format)) {
commandline.setClassname("jdepend.xmlui.JDepend");
}
if (jvm != null) {
commandline.setVm(jvm);
}
if (getSourcespath() == null && getClassespath() == null) {
throw new BuildException("Missing classespath required argument");
}
if (getClassespath() == null) {
log("sourcespath is deprecated in JDepend >= 2.5 - please convert to classespath");
}
// execute the test and get the return code
int exitValue;
boolean wasKilled = false;
if (!getFork()) {
exitValue = executeInVM(commandline);
} else {
ExecuteWatchdog watchdog = createWatchdog();
exitValue = executeAsForked(commandline, watchdog);
// null watchdog means no timeout, you'd better not check with null
if (watchdog != null) {
wasKilled = watchdog.killedProcess();
}
}
// if there is an error/failure and that it should halt, stop
// everything otherwise just log a statement
boolean errorOccurred = exitValue == JDependTask.ERRORS || wasKilled;
if (errorOccurred) {
String errorMessage = "JDepend FAILED"
+ (wasKilled ? " - Timed out" : "");
if (getHaltonerror()) {
throw new BuildException(errorMessage, getLocation());
}
log(errorMessage, Project.MSG_ERR);
}
}
// this comment extract from JUnit Task may also apply here
// "in VM is not very nice since it could probably hang the
// whole build. IMHO this method should be avoided and it would be best
// to remove it in future versions. TBD. (SBa)"
/**
* Execute inside VM.
*
* @param commandline the command line
* @return the return value of the mvm
* @exception BuildException if an error occurs
*/
public int executeInVM(CommandlineJava commandline) throws BuildException {
jdepend.textui.JDepend jdepend;
if ("xml".equals(format)) {
jdepend = new jdepend.xmlui.JDepend();
} else {
jdepend = new jdepend.textui.JDepend();
}
FileWriter fw = null;
PrintWriter pw = null;
if (getOutputFile() != null) {
try {
fw = new FileWriter(getOutputFile().getPath()); //NOSONAR
} catch (IOException e) {
String msg = "JDepend Failed when creating the output file: "
+ e.getMessage();
log(msg);
throw new BuildException(msg);
}
pw = new PrintWriter(fw);
jdepend.setWriter(pw);
log("Output to be stored in " + getOutputFile().getPath());
}
try {
getWorkingPath().ifPresent(path -> {
for (String filepath : path.list()) {
File f = new File(filepath);
// not necessary as JDepend would fail, but why loose
// some time?
if (!f.exists()) {
String msg = "\""
+ f.getPath()
+ "\" does not represent a valid"
+ " file or directory. JDepend would fail.";
log(msg);
throw new BuildException(msg);
}
try {
jdepend.addDirectory(f.getPath());
} catch (IOException e) {
String msg =
"JDepend Failed when adding a class directory: "
+ e.getMessage();
log(msg);
throw new BuildException(msg);
}
}
});
// This bit turns <exclude> child tags into patters to ignore
String[] patterns = defaultPatterns.getExcludePatterns(getProject());
if (patterns != null && patterns.length > 0) {
if (setFilter != null) {
List<String> v = new ArrayList<>();
Collections.addAll(v, patterns);
try {
Object o = packageFilterC.newInstance(v);
setFilter.invoke(jdepend, o);
} catch (Throwable e) {
log("excludes will be ignored as JDepend doesn't like me: "
+ e.getMessage(), Project.MSG_WARN);
}
} else {
log("Sorry, your version of JDepend doesn't support excludes",
Project.MSG_WARN);
}
}
jdepend.analyze();
if (pw != null && pw.checkError()) {
throw new IOException(
"Encountered an error writing JDepend output");
}
} catch (IOException ex) {
throw new BuildException(ex);
} finally {
FileUtils.close(pw);
FileUtils.close(fw);
}
return SUCCESS;
}
/**
* Execute the task by forking a new JVM. The command will block until
* it finishes. To know if the process was destroyed or not, use the
* <code>killedProcess()</code> method of the watchdog class.
* @param commandline the commandline for forked jvm
* @param watchdog the watchdog in charge of cancelling the test if it
* exceeds a certain amount of time. Can be <code>null</code>.
* @return the result of running the jdepend
* @throws BuildException in case of error
*/
// JL: comment extracted from JUnitTask (and slightly modified)
public int executeAsForked(CommandlineJava commandline,
ExecuteWatchdog watchdog) throws BuildException {
runtimeClasses = new Path(getProject());
addClasspathEntry("/jdepend/textui/JDepend.class");
// if not set, auto-create the ClassPath from the project
createClasspath();
// not sure whether this test is needed but cost nothing to put.
// hope it will be reviewed by anybody competent
if (!getClasspath().toString().isEmpty()) {
createJvmarg(commandline).setValue("-classpath");
createJvmarg(commandline).setValue(getClasspath().toString());
}
if (includeRuntime) {
Map<String, String> env = Execute.getEnvironmentVariables();
String cp = env.get("CLASSPATH");
if (cp != null) {
commandline.createClasspath(getProject()).createPath()
.append(new Path(getProject(), cp));
}
log("Implicitly adding " + runtimeClasses + " to CLASSPATH",
Project.MSG_VERBOSE);
commandline.createClasspath(getProject()).createPath()
.append(runtimeClasses);
}
if (getOutputFile() != null) {
// having a space between the file and its path causes commandline
// to add quotes around the argument thus making JDepend not taking
// it into account. Thus we split it in two
commandline.createArgument().setValue("-file");
commandline.createArgument().setValue(outputFile.getPath());
// we have to find a cleaner way to put this output
}
getWorkingPath().ifPresent(path -> {
for (String filepath : path.list()) {
File f = new File(filepath);
// not necessary as JDepend would fail, but why loose
// some time?
if (!f.exists() || !f.isDirectory()) {
throw new BuildException(
"\"%s\" does not represent a valid directory. JDepend would fail.",
f.getPath());
}
commandline.createArgument().setValue(f.getPath());
}
});
Execute execute = new Execute(new LogStreamHandler(this,
Project.MSG_INFO, Project.MSG_WARN), watchdog);
execute.setCommandline(commandline.getCommandline());
if (getDir() != null) {
execute.setWorkingDirectory(getDir());
execute.setAntRun(getProject());
}
if (getOutputFile() != null) {
log("Output to be stored in " + getOutputFile().getPath());
}
log(commandline.describeCommand(), Project.MSG_VERBOSE);
try {
return execute.execute();
} catch (IOException e) {
throw new BuildException("Process fork failed.", e, getLocation());
}
}
/**
* @return <code>null</code> if there is a timeout value, otherwise the
* watchdog instance.
* @throws BuildException in case of error
*/
protected ExecuteWatchdog createWatchdog() throws BuildException {
if (getTimeout() == null) {
return null;
}
return new ExecuteWatchdog(getTimeout());
}
private Optional<Path> getWorkingPath() {
Optional<Path> result = Optional.ofNullable(getClassespath());
if (result.isPresent()) {
return result;
}
result = Optional.ofNullable(getSourcespath());
result.ifPresent(resources -> log("nested sourcespath is deprecated; please use classespath"));
return result;
}
}