/* * 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.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="http://www.clarkware.com/software/JDepend.html">http://www.clarkware.com/software/JDepend.html</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 <tt>true</tt> if a JVM should be forked, * otherwise <tt>false<tt> */ 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 <tt>java</tt>. Ignored if no JVM is forked. * @param value the new VM to use instead of <tt>java</tt> * @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 * <tt>killedProcess()</tt> 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 <tt>null</tt>. * @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().length() > 0) { 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 <tt>null</tt> 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().longValue()); } private Optional<Path> getWorkingPath() { Optional<Path> result = Optional.ofNullable(getClassespath()); if (result.isPresent()) { return result; } result = Optional.ofNullable(getSourcespath()); if (result.isPresent()) { log("nested sourcespath is deprecated; please use classespath"); } return result; } }