1. /*
  2. * Copyright 2001-2004 The Apache Software Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. package org.apache.tools.ant.taskdefs.optional.jdepend;
  18. import java.io.File;
  19. import java.io.FileWriter;
  20. import java.io.IOException;
  21. import java.io.PrintWriter;
  22. import java.lang.reflect.Constructor;
  23. import java.lang.reflect.Method;
  24. import java.util.Vector;
  25. import java.util.Enumeration;
  26. import org.apache.tools.ant.BuildException;
  27. import org.apache.tools.ant.Project;
  28. import org.apache.tools.ant.Task;
  29. import org.apache.tools.ant.taskdefs.Execute;
  30. import org.apache.tools.ant.taskdefs.ExecuteWatchdog;
  31. import org.apache.tools.ant.taskdefs.LogStreamHandler;
  32. import org.apache.tools.ant.types.Commandline;
  33. import org.apache.tools.ant.types.CommandlineJava;
  34. import org.apache.tools.ant.types.EnumeratedAttribute;
  35. import org.apache.tools.ant.types.Path;
  36. import org.apache.tools.ant.types.PatternSet;
  37. import org.apache.tools.ant.types.Reference;
  38. import org.apache.tools.ant.util.LoaderUtils;
  39. /**
  40. * Runs JDepend tests.
  41. *
  42. * <p>JDepend is a tool to generate design quality metrics for each Java package.
  43. * It has been initially created by Mike Clark. JDepend can be found at <a
  44. * href="http://www.clarkware.com/software/JDepend.html">http://www.clarkware.com/software/JDepend.html</a>.
  45. *
  46. * The current implementation spawn a new Java VM.
  47. *
  48. */
  49. public class JDependTask extends Task {
  50. //private CommandlineJava commandline = new CommandlineJava();
  51. // required attributes
  52. private Path sourcesPath; // Deprecated!
  53. private Path classesPath; // Use this going forward
  54. // optional attributes
  55. private File outputFile;
  56. private File dir;
  57. private Path compileClasspath;
  58. private boolean haltonerror = false;
  59. private boolean fork = false;
  60. private Long timeout = null;
  61. private String jvm = null;
  62. private String format = "text";
  63. private PatternSet defaultPatterns = new PatternSet();
  64. private static Constructor packageFilterC;
  65. private static Method setFilter;
  66. private boolean includeRuntime = false;
  67. private Path runtimeClasses = null;
  68. static {
  69. try {
  70. Class packageFilter =
  71. Class.forName("jdepend.framework.PackageFilter");
  72. packageFilterC =
  73. packageFilter.getConstructor(new Class[] {java.util.Collection.class});
  74. setFilter =
  75. jdepend.textui.JDepend.class.getDeclaredMethod("setFilter",
  76. new Class[] {packageFilter});
  77. } catch (Throwable t) {
  78. if (setFilter == null) {
  79. packageFilterC = null;
  80. }
  81. }
  82. }
  83. /**
  84. * If true,
  85. * include jdepend.jar in the forked VM.
  86. *
  87. * @param b include ant run time yes or no
  88. * @since Ant 1.6
  89. */
  90. public void setIncluderuntime(boolean b) {
  91. includeRuntime = b;
  92. }
  93. /**
  94. * Set the timeout value (in milliseconds).
  95. *
  96. * <p>If the operation is running for more than this value, the jdepend
  97. * will be canceled. (works only when in 'fork' mode).</p>
  98. * @param value the maximum time (in milliseconds) allowed before
  99. * declaring the test as 'timed-out'
  100. * @see #setFork(boolean)
  101. */
  102. public void setTimeout(Long value) {
  103. timeout = value;
  104. }
  105. /**
  106. * @return the timeout value
  107. */
  108. public Long getTimeout() {
  109. return timeout;
  110. }
  111. /**
  112. * The output file name.
  113. *
  114. * @param outputFile the output file name
  115. */
  116. public void setOutputFile(File outputFile) {
  117. this.outputFile = outputFile;
  118. }
  119. /**
  120. * @return the output file name
  121. */
  122. public File getOutputFile() {
  123. return outputFile;
  124. }
  125. /**
  126. * Whether or not to halt on failure. Default: false.
  127. * @param haltonerror the value to set
  128. */
  129. public void setHaltonerror(boolean haltonerror) {
  130. this.haltonerror = haltonerror;
  131. }
  132. /**
  133. * @return the value of the haltonerror attribute
  134. */
  135. public boolean getHaltonerror() {
  136. return haltonerror;
  137. }
  138. /**
  139. * If true, forks into a new JVM. Default: false.
  140. *
  141. * @param value <tt>true</tt> if a JVM should be forked,
  142. * otherwise <tt>false<tt>
  143. */
  144. public void setFork(boolean value) {
  145. fork = value;
  146. }
  147. /**
  148. * @return the value of the fork attribute
  149. */
  150. public boolean getFork() {
  151. return fork;
  152. }
  153. /**
  154. * The command used to invoke a forked Java Virtual Machine.
  155. *
  156. * Default is <tt>java</tt>. Ignored if no JVM is forked.
  157. * @param value the new VM to use instead of <tt>java</tt>
  158. * @see #setFork(boolean)
  159. */
  160. public void setJvm(String value) {
  161. jvm = value;
  162. }
  163. /**
  164. * Adds a path to source code to analyze.
  165. * @return a source path
  166. * @deprecated
  167. */
  168. public Path createSourcespath() {
  169. if (sourcesPath == null) {
  170. sourcesPath = new Path(getProject());
  171. }
  172. return sourcesPath.createPath();
  173. }
  174. /**
  175. * Gets the sourcepath.
  176. * @return the sources path
  177. * @deprecated
  178. *
  179. */
  180. public Path getSourcespath() {
  181. return sourcesPath;
  182. }
  183. /**
  184. * Adds a path to class code to analyze.
  185. * @return a classes path
  186. */
  187. public Path createClassespath() {
  188. if (classesPath == null) {
  189. classesPath = new Path(getProject());
  190. }
  191. return classesPath.createPath();
  192. }
  193. /**
  194. * Gets the classespath.
  195. * @return the classes path
  196. */
  197. public Path getClassespath() {
  198. return classesPath;
  199. }
  200. /**
  201. * The directory to invoke the VM in. Ignored if no JVM is forked.
  202. * @param dir the directory to invoke the JVM from.
  203. * @see #setFork(boolean)
  204. */
  205. public void setDir(File dir) {
  206. this.dir = dir;
  207. }
  208. /**
  209. * @return the dir attribute
  210. */
  211. public File getDir() {
  212. return dir;
  213. }
  214. /**
  215. * Set the classpath to be used for this compilation.
  216. * @param classpath a class path to be used
  217. */
  218. public void setClasspath(Path classpath) {
  219. if (compileClasspath == null) {
  220. compileClasspath = classpath;
  221. } else {
  222. compileClasspath.append(classpath);
  223. }
  224. }
  225. /**
  226. * Gets the classpath to be used for this compilation.
  227. * @return the class path used for compilation
  228. */
  229. public Path getClasspath() {
  230. return compileClasspath;
  231. }
  232. /**
  233. * Adds a path to the classpath.
  234. * @return a classpath
  235. */
  236. public Path createClasspath() {
  237. if (compileClasspath == null) {
  238. compileClasspath = new Path(getProject());
  239. }
  240. return compileClasspath.createPath();
  241. }
  242. /**
  243. * Create a new JVM argument. Ignored if no JVM is forked.
  244. * @param commandline the commandline to create the argument on
  245. * @return create a new JVM argument so that any argument can
  246. * be passed to the JVM.
  247. * @see #setFork(boolean)
  248. */
  249. public Commandline.Argument createJvmarg(CommandlineJava commandline) {
  250. return commandline.createVmArgument();
  251. }
  252. /**
  253. * Adds a reference to a classpath defined elsewhere.
  254. * @param r a classpath reference
  255. */
  256. public void setClasspathRef(Reference r) {
  257. createClasspath().setRefid(r);
  258. }
  259. /**
  260. * add a name entry on the exclude list
  261. * @return a pattern for the excludes
  262. */
  263. public PatternSet.NameEntry createExclude() {
  264. return defaultPatterns.createExclude();
  265. }
  266. /**
  267. * @return the excludes patterns
  268. */
  269. public PatternSet getExcludes() {
  270. return defaultPatterns;
  271. }
  272. /**
  273. * The format to write the output in, "xml" or "text".
  274. *
  275. * @param ea xml or text
  276. */
  277. public void setFormat(FormatAttribute ea) {
  278. format = ea.getValue();
  279. }
  280. /**
  281. * A class for the enumerated attribute format,
  282. * values are xml and text.
  283. * @see EnumeratedAttribute
  284. */
  285. public static class FormatAttribute extends EnumeratedAttribute {
  286. private String [] formats = new String[]{"xml", "text"};
  287. /**
  288. * @return the enumerated values
  289. */
  290. public String[] getValues() {
  291. return formats;
  292. }
  293. }
  294. /**
  295. * No problems with this test.
  296. */
  297. private static final int SUCCESS = 0;
  298. /**
  299. * An error occurred.
  300. */
  301. private static final int ERRORS = 1;
  302. /**
  303. * Search for the given resource and add the directory or archive
  304. * that contains it to the classpath.
  305. *
  306. * <p>Doesn't work for archives in JDK 1.1 as the URL returned by
  307. * getResource doesn't contain the name of the archive.</p>
  308. *
  309. * @param resource resource that one wants to lookup
  310. * @since Ant 1.6
  311. */
  312. private void addClasspathEntry(String resource) {
  313. /*
  314. * pre Ant 1.6 this method used to call getClass().getResource
  315. * while Ant 1.6 will call ClassLoader.getResource().
  316. *
  317. * The difference is that Class.getResource expects a leading
  318. * slash for "absolute" resources and will strip it before
  319. * delegating to ClassLoader.getResource - so we now have to
  320. * emulate Class's behavior.
  321. */
  322. if (resource.startsWith("/")) {
  323. resource = resource.substring(1);
  324. } else {
  325. resource = "org/apache/tools/ant/taskdefs/optional/jdepend/"
  326. + resource;
  327. }
  328. File f = LoaderUtils.getResourceSource(getClass().getClassLoader(),
  329. resource);
  330. if (f != null) {
  331. log("Found " + f.getAbsolutePath(), Project.MSG_DEBUG);
  332. runtimeClasses.createPath().setLocation(f);
  333. } else {
  334. log("Couldn\'t find " + resource, Project.MSG_DEBUG);
  335. }
  336. }
  337. /**
  338. * execute the task
  339. *
  340. * @exception BuildException if an error occurs
  341. */
  342. public void execute() throws BuildException {
  343. CommandlineJava commandline = new CommandlineJava();
  344. if ("text".equals(format)) {
  345. commandline.setClassname("jdepend.textui.JDepend");
  346. } else
  347. if ("xml".equals(format)) {
  348. commandline.setClassname("jdepend.xmlui.JDepend");
  349. }
  350. if (jvm != null) {
  351. commandline.setVm(jvm);
  352. }
  353. if (getSourcespath() == null && getClassespath() == null) {
  354. throw new BuildException("Missing classespath required argument");
  355. } else if (getClassespath() == null) {
  356. String msg =
  357. "sourcespath is deprecated in JDepend >= 2.5 "
  358. + "- please convert to classespath";
  359. log(msg);
  360. }
  361. // execute the test and get the return code
  362. int exitValue = JDependTask.ERRORS;
  363. boolean wasKilled = false;
  364. if (!getFork()) {
  365. exitValue = executeInVM(commandline);
  366. } else {
  367. ExecuteWatchdog watchdog = createWatchdog();
  368. exitValue = executeAsForked(commandline, watchdog);
  369. // null watchdog means no timeout, you'd better not check with null
  370. if (watchdog != null) {
  371. wasKilled = watchdog.killedProcess();
  372. }
  373. }
  374. // if there is an error/failure and that it should halt, stop
  375. // everything otherwise just log a statement
  376. boolean errorOccurred = exitValue == JDependTask.ERRORS || wasKilled;
  377. if (errorOccurred) {
  378. String errorMessage = "JDepend FAILED"
  379. + (wasKilled ? " - Timed out" : "");
  380. if (getHaltonerror()) {
  381. throw new BuildException(errorMessage, getLocation());
  382. } else {
  383. log(errorMessage, Project.MSG_ERR);
  384. }
  385. }
  386. }
  387. // this comment extract from JUnit Task may also apply here
  388. // "in VM is not very nice since it could probably hang the
  389. // whole build. IMHO this method should be avoided and it would be best
  390. // to remove it in future versions. TBD. (SBa)"
  391. /**
  392. * Execute inside VM.
  393. *
  394. * @param commandline the command line
  395. * @return the return value of the mvm
  396. * @exception BuildException if an error occurs
  397. */
  398. public int executeInVM(CommandlineJava commandline) throws BuildException {
  399. jdepend.textui.JDepend jdepend;
  400. if ("xml".equals(format)) {
  401. jdepend = new jdepend.xmlui.JDepend();
  402. } else {
  403. jdepend = new jdepend.textui.JDepend();
  404. }
  405. FileWriter fw = null;
  406. if (getOutputFile() != null) {
  407. try {
  408. fw = new FileWriter(getOutputFile().getPath());
  409. } catch (IOException e) {
  410. String msg = "JDepend Failed when creating the output file: "
  411. + e.getMessage();
  412. log(msg);
  413. throw new BuildException(msg);
  414. }
  415. jdepend.setWriter(new PrintWriter(fw));
  416. log("Output to be stored in " + getOutputFile().getPath());
  417. }
  418. try {
  419. if (getClassespath() != null) {
  420. // This is the new, better way - use classespath instead
  421. // of sourcespath. The code is currently the same - you
  422. // need class files in a directory to use this - jar files
  423. // coming soon....
  424. String[] classesPath = getClassespath().list();
  425. for (int i = 0; i < classesPath.length; i++) {
  426. File f = new File(classesPath[i]);
  427. // not necessary as JDepend would fail, but why loose
  428. // some time?
  429. if (!f.exists() || !f.isDirectory()) {
  430. String msg = "\""
  431. + f.getPath()
  432. + "\" does not represent a valid"
  433. + " directory. JDepend would fail.";
  434. log(msg);
  435. throw new BuildException(msg);
  436. }
  437. try {
  438. jdepend.addDirectory(f.getPath());
  439. } catch (IOException e) {
  440. String msg =
  441. "JDepend Failed when adding a class directory: "
  442. + e.getMessage();
  443. log(msg);
  444. throw new BuildException(msg);
  445. }
  446. }
  447. } else if (getSourcespath() != null) {
  448. // This is the old way and is deprecated - classespath is
  449. // the right way to do this and is above
  450. String[] sourcesPath = getSourcespath().list();
  451. for (int i = 0; i < sourcesPath.length; i++) {
  452. File f = new File(sourcesPath[i]);
  453. // not necessary as JDepend would fail, but why loose
  454. // some time?
  455. if (!f.exists() || !f.isDirectory()) {
  456. String msg = "\""
  457. + f.getPath()
  458. + "\" does not represent a valid"
  459. + " directory. JDepend would fail.";
  460. log(msg);
  461. throw new BuildException(msg);
  462. }
  463. try {
  464. jdepend.addDirectory(f.getPath());
  465. } catch (IOException e) {
  466. String msg =
  467. "JDepend Failed when adding a source directory: "
  468. + e.getMessage();
  469. log(msg);
  470. throw new BuildException(msg);
  471. }
  472. }
  473. }
  474. // This bit turns <exclude> child tags into patters to ignore
  475. String[] patterns = defaultPatterns.getExcludePatterns(getProject());
  476. if (patterns != null && patterns.length > 0) {
  477. if (setFilter != null) {
  478. Vector v = new Vector();
  479. for (int i = 0; i < patterns.length; i++) {
  480. v.addElement(patterns[i]);
  481. }
  482. try {
  483. Object o = packageFilterC.newInstance(new Object[] {v});
  484. setFilter.invoke(jdepend, new Object[] {o});
  485. } catch (Throwable e) {
  486. log("excludes will be ignored as JDepend doesn't like me: "
  487. + e.getMessage(), Project.MSG_WARN);
  488. }
  489. } else {
  490. log("Sorry, your version of JDepend doesn't support excludes",
  491. Project.MSG_WARN);
  492. }
  493. }
  494. jdepend.analyze();
  495. } finally {
  496. if (fw != null) {
  497. try {
  498. fw.close();
  499. } catch (Throwable t) {
  500. // Ignore
  501. }
  502. }
  503. }
  504. return SUCCESS;
  505. }
  506. /**
  507. * Execute the task by forking a new JVM. The command will block until
  508. * it finishes. To know if the process was destroyed or not, use the
  509. * <tt>killedProcess()</tt> method of the watchdog class.
  510. * @param commandline the commandline for forked jvm
  511. * @param watchdog the watchdog in charge of cancelling the test if it
  512. * exceeds a certain amount of time. Can be <tt>null</tt>.
  513. * @return the result of running the jdepend
  514. * @throws BuildException in case of error
  515. */
  516. // JL: comment extracted from JUnitTask (and slightly modified)
  517. public int executeAsForked(CommandlineJava commandline,
  518. ExecuteWatchdog watchdog) throws BuildException {
  519. runtimeClasses = new Path(getProject());
  520. addClasspathEntry("/jdepend/textui/JDepend.class");
  521. // if not set, auto-create the ClassPath from the project
  522. createClasspath();
  523. // not sure whether this test is needed but cost nothing to put.
  524. // hope it will be reviewed by anybody competent
  525. if (getClasspath().toString().length() > 0) {
  526. createJvmarg(commandline).setValue("-classpath");
  527. createJvmarg(commandline).setValue(getClasspath().toString());
  528. }
  529. if (includeRuntime) {
  530. Vector v = Execute.getProcEnvironment();
  531. Enumeration e = v.elements();
  532. while (e.hasMoreElements()) {
  533. String s = (String) e.nextElement();
  534. if (s.startsWith("CLASSPATH=")) {
  535. commandline.createClasspath(getProject()).createPath()
  536. .append(new Path(getProject(),
  537. s.substring("CLASSPATH=".length()
  538. )));
  539. }
  540. }
  541. log("Implicitly adding " + runtimeClasses + " to CLASSPATH",
  542. Project.MSG_VERBOSE);
  543. commandline.createClasspath(getProject()).createPath()
  544. .append(runtimeClasses);
  545. }
  546. if (getOutputFile() != null) {
  547. // having a space between the file and its path causes commandline
  548. // to add quotes around the argument thus making JDepend not taking
  549. // it into account. Thus we split it in two
  550. commandline.createArgument().setValue("-file");
  551. commandline.createArgument().setValue(outputFile.getPath());
  552. // we have to find a cleaner way to put this output
  553. }
  554. if (getSourcespath() != null) {
  555. // This is deprecated - use classespath in the future
  556. String[] sourcesPath = getSourcespath().list();
  557. for (int i = 0; i < sourcesPath.length; i++) {
  558. File f = new File(sourcesPath[i]);
  559. // not necessary as JDepend would fail, but why loose
  560. // some time?
  561. if (!f.exists() || !f.isDirectory()) {
  562. throw new BuildException("\"" + f.getPath()
  563. + "\" does not represent a valid"
  564. + " directory. JDepend would"
  565. + " fail.");
  566. }
  567. commandline.createArgument().setValue(f.getPath());
  568. }
  569. }
  570. if (getClassespath() != null) {
  571. // This is the new way - use classespath - code is the
  572. // same for now
  573. String[] classesPath = getClassespath().list();
  574. for (int i = 0; i < classesPath.length; i++) {
  575. File f = new File(classesPath[i]);
  576. // not necessary as JDepend would fail, but why loose
  577. // some time?
  578. if (!f.exists() || !f.isDirectory()) {
  579. throw new BuildException("\"" + f.getPath()
  580. + "\" does not represent a valid"
  581. + " directory. JDepend would"
  582. + " fail.");
  583. }
  584. commandline.createArgument().setValue(f.getPath());
  585. }
  586. }
  587. Execute execute = new Execute(new LogStreamHandler(this,
  588. Project.MSG_INFO, Project.MSG_WARN), watchdog);
  589. execute.setCommandline(commandline.getCommandline());
  590. if (getDir() != null) {
  591. execute.setWorkingDirectory(getDir());
  592. execute.setAntRun(getProject());
  593. }
  594. if (getOutputFile() != null) {
  595. log("Output to be stored in " + getOutputFile().getPath());
  596. }
  597. log(commandline.describeCommand(), Project.MSG_VERBOSE);
  598. try {
  599. return execute.execute();
  600. } catch (IOException e) {
  601. throw new BuildException("Process fork failed.", e, getLocation());
  602. }
  603. }
  604. /**
  605. * @return <tt>null</tt> if there is a timeout value, otherwise the
  606. * watchdog instance.
  607. * @throws BuildException in case of error
  608. */
  609. protected ExecuteWatchdog createWatchdog() throws BuildException {
  610. if (getTimeout() == null) {
  611. return null;
  612. }
  613. return new ExecuteWatchdog(getTimeout().longValue());
  614. }
  615. }