001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.tools.ant.taskdefs.optional.jdepend;
020:
021: import java.io.File;
022: import java.io.FileWriter;
023: import java.io.IOException;
024: import java.io.PrintWriter;
025: import java.lang.reflect.Constructor;
026: import java.lang.reflect.Method;
027: import java.util.Vector;
028: import java.util.Enumeration;
029: import org.apache.tools.ant.BuildException;
030: import org.apache.tools.ant.Project;
031: import org.apache.tools.ant.Task;
032: import org.apache.tools.ant.taskdefs.Execute;
033: import org.apache.tools.ant.taskdefs.ExecuteWatchdog;
034: import org.apache.tools.ant.taskdefs.LogStreamHandler;
035: import org.apache.tools.ant.types.Commandline;
036: import org.apache.tools.ant.types.CommandlineJava;
037: import org.apache.tools.ant.types.EnumeratedAttribute;
038: import org.apache.tools.ant.types.Path;
039: import org.apache.tools.ant.types.PatternSet;
040: import org.apache.tools.ant.types.Reference;
041: import org.apache.tools.ant.util.FileUtils;
042: import org.apache.tools.ant.util.LoaderUtils;
043:
044: /**
045: * Runs JDepend tests.
046: *
047: * <p>JDepend is a tool to generate design quality metrics for each Java package.
048: * It has been initially created by Mike Clark. JDepend can be found at <a
049: * href="http://www.clarkware.com/software/JDepend.html">http://www.clarkware.com/software/JDepend.html</a>.
050: *
051: * The current implementation spawn a new Java VM.
052: *
053: */
054: public class JDependTask extends Task {
055: //private CommandlineJava commandline = new CommandlineJava();
056:
057: // required attributes
058: private Path sourcesPath; // Deprecated!
059: private Path classesPath; // Use this going forward
060:
061: // optional attributes
062: private File outputFile;
063: private File dir;
064: private Path compileClasspath;
065: private boolean haltonerror = false;
066: private boolean fork = false;
067: private Long timeout = null;
068:
069: private String jvm = null;
070: private String format = "text";
071: private PatternSet defaultPatterns = new PatternSet();
072:
073: private static Constructor packageFilterC;
074: private static Method setFilter;
075:
076: private boolean includeRuntime = false;
077: private Path runtimeClasses = null;
078:
079: static {
080: try {
081: Class packageFilter = Class
082: .forName("jdepend.framework.PackageFilter");
083: packageFilterC = packageFilter
084: .getConstructor(new Class[] { java.util.Collection.class });
085: setFilter = jdepend.textui.JDepend.class.getDeclaredMethod(
086: "setFilter", new Class[] { packageFilter });
087: } catch (Throwable t) {
088: if (setFilter == null) {
089: packageFilterC = null;
090: }
091: }
092: }
093:
094: /**
095: * If true,
096: * include jdepend.jar in the forked VM.
097: *
098: * @param b include ant run time yes or no
099: * @since Ant 1.6
100: */
101: public void setIncluderuntime(boolean b) {
102: includeRuntime = b;
103: }
104:
105: /**
106: * Set the timeout value (in milliseconds).
107: *
108: * <p>If the operation is running for more than this value, the jdepend
109: * will be canceled. (works only when in 'fork' mode).</p>
110: * @param value the maximum time (in milliseconds) allowed before
111: * declaring the test as 'timed-out'
112: * @see #setFork(boolean)
113: */
114: public void setTimeout(Long value) {
115: timeout = value;
116: }
117:
118: /**
119: * @return the timeout value
120: */
121: public Long getTimeout() {
122: return timeout;
123: }
124:
125: /**
126: * The output file name.
127: *
128: * @param outputFile the output file name
129: */
130: public void setOutputFile(File outputFile) {
131: this .outputFile = outputFile;
132: }
133:
134: /**
135: * @return the output file name
136: */
137: public File getOutputFile() {
138: return outputFile;
139: }
140:
141: /**
142: * Whether or not to halt on failure. Default: false.
143: * @param haltonerror the value to set
144: */
145: public void setHaltonerror(boolean haltonerror) {
146: this .haltonerror = haltonerror;
147: }
148:
149: /**
150: * @return the value of the haltonerror attribute
151: */
152: public boolean getHaltonerror() {
153: return haltonerror;
154: }
155:
156: /**
157: * If true, forks into a new JVM. Default: false.
158: *
159: * @param value <tt>true</tt> if a JVM should be forked,
160: * otherwise <tt>false<tt>
161: */
162: public void setFork(boolean value) {
163: fork = value;
164: }
165:
166: /**
167: * @return the value of the fork attribute
168: */
169: public boolean getFork() {
170: return fork;
171: }
172:
173: /**
174: * The command used to invoke a forked Java Virtual Machine.
175: *
176: * Default is <tt>java</tt>. Ignored if no JVM is forked.
177: * @param value the new VM to use instead of <tt>java</tt>
178: * @see #setFork(boolean)
179: */
180: public void setJvm(String value) {
181: jvm = value;
182:
183: }
184:
185: /**
186: * Adds a path to source code to analyze.
187: * @return a source path
188: * @deprecated since 1.6.x.
189: */
190: public Path createSourcespath() {
191: if (sourcesPath == null) {
192: sourcesPath = new Path(getProject());
193: }
194: return sourcesPath.createPath();
195: }
196:
197: /**
198: * Gets the sourcepath.
199: * @return the sources path
200: * @deprecated since 1.6.x.
201: */
202: public Path getSourcespath() {
203: return sourcesPath;
204: }
205:
206: /**
207: * Adds a path to class code to analyze.
208: * @return a classes path
209: */
210: public Path createClassespath() {
211: if (classesPath == null) {
212: classesPath = new Path(getProject());
213: }
214: return classesPath.createPath();
215: }
216:
217: /**
218: * Gets the classespath.
219: * @return the classes path
220: */
221: public Path getClassespath() {
222: return classesPath;
223: }
224:
225: /**
226: * The directory to invoke the VM in. Ignored if no JVM is forked.
227: * @param dir the directory to invoke the JVM from.
228: * @see #setFork(boolean)
229: */
230: public void setDir(File dir) {
231: this .dir = dir;
232: }
233:
234: /**
235: * @return the dir attribute
236: */
237: public File getDir() {
238: return dir;
239: }
240:
241: /**
242: * Set the classpath to be used for this compilation.
243: * @param classpath a class path to be used
244: */
245: public void setClasspath(Path classpath) {
246: if (compileClasspath == null) {
247: compileClasspath = classpath;
248: } else {
249: compileClasspath.append(classpath);
250: }
251: }
252:
253: /**
254: * Gets the classpath to be used for this compilation.
255: * @return the class path used for compilation
256: */
257: public Path getClasspath() {
258: return compileClasspath;
259: }
260:
261: /**
262: * Adds a path to the classpath.
263: * @return a classpath
264: */
265: public Path createClasspath() {
266: if (compileClasspath == null) {
267: compileClasspath = new Path(getProject());
268: }
269: return compileClasspath.createPath();
270: }
271:
272: /**
273: * Create a new JVM argument. Ignored if no JVM is forked.
274: * @param commandline the commandline to create the argument on
275: * @return create a new JVM argument so that any argument can
276: * be passed to the JVM.
277: * @see #setFork(boolean)
278: */
279: public Commandline.Argument createJvmarg(CommandlineJava commandline) {
280: return commandline.createVmArgument();
281: }
282:
283: /**
284: * Adds a reference to a classpath defined elsewhere.
285: * @param r a classpath reference
286: */
287: public void setClasspathRef(Reference r) {
288: createClasspath().setRefid(r);
289: }
290:
291: /**
292: * add a name entry on the exclude list
293: * @return a pattern for the excludes
294: */
295: public PatternSet.NameEntry createExclude() {
296: return defaultPatterns.createExclude();
297: }
298:
299: /**
300: * @return the excludes patterns
301: */
302: public PatternSet getExcludes() {
303: return defaultPatterns;
304: }
305:
306: /**
307: * The format to write the output in, "xml" or "text".
308: *
309: * @param ea xml or text
310: */
311: public void setFormat(FormatAttribute ea) {
312: format = ea.getValue();
313: }
314:
315: /**
316: * A class for the enumerated attribute format,
317: * values are xml and text.
318: * @see EnumeratedAttribute
319: */
320: public static class FormatAttribute extends EnumeratedAttribute {
321: private String[] formats = new String[] { "xml", "text" };
322:
323: /**
324: * @return the enumerated values
325: */
326: public String[] getValues() {
327: return formats;
328: }
329: }
330:
331: /**
332: * No problems with this test.
333: */
334: private static final int SUCCESS = 0;
335: /**
336: * An error occurred.
337: */
338: private static final int ERRORS = 1;
339:
340: /**
341: * Search for the given resource and add the directory or archive
342: * that contains it to the classpath.
343: *
344: * <p>Doesn't work for archives in JDK 1.1 as the URL returned by
345: * getResource doesn't contain the name of the archive.</p>
346: *
347: * @param resource resource that one wants to lookup
348: * @since Ant 1.6
349: */
350: private void addClasspathEntry(String resource) {
351: /*
352: * pre Ant 1.6 this method used to call getClass().getResource
353: * while Ant 1.6 will call ClassLoader.getResource().
354: *
355: * The difference is that Class.getResource expects a leading
356: * slash for "absolute" resources and will strip it before
357: * delegating to ClassLoader.getResource - so we now have to
358: * emulate Class's behavior.
359: */
360: if (resource.startsWith("/")) {
361: resource = resource.substring(1);
362: } else {
363: resource = "org/apache/tools/ant/taskdefs/optional/jdepend/"
364: + resource;
365: }
366:
367: File f = LoaderUtils.getResourceSource(getClass()
368: .getClassLoader(), resource);
369: if (f != null) {
370: log("Found " + f.getAbsolutePath(), Project.MSG_DEBUG);
371: runtimeClasses.createPath().setLocation(f);
372: } else {
373: log("Couldn\'t find " + resource, Project.MSG_DEBUG);
374: }
375: }
376:
377: /**
378: * execute the task
379: *
380: * @exception BuildException if an error occurs
381: */
382: public void execute() throws BuildException {
383:
384: CommandlineJava commandline = new CommandlineJava();
385:
386: if ("text".equals(format)) {
387: commandline.setClassname("jdepend.textui.JDepend");
388: } else if ("xml".equals(format)) {
389: commandline.setClassname("jdepend.xmlui.JDepend");
390: }
391:
392: if (jvm != null) {
393: commandline.setVm(jvm);
394: }
395: if (getSourcespath() == null && getClassespath() == null) {
396: throw new BuildException(
397: "Missing classespath required argument");
398: } else if (getClassespath() == null) {
399: String msg = "sourcespath is deprecated in JDepend >= 2.5 "
400: + "- please convert to classespath";
401: log(msg);
402: }
403:
404: // execute the test and get the return code
405: int exitValue = JDependTask.ERRORS;
406: boolean wasKilled = false;
407: if (!getFork()) {
408: exitValue = executeInVM(commandline);
409: } else {
410: ExecuteWatchdog watchdog = createWatchdog();
411: exitValue = executeAsForked(commandline, watchdog);
412: // null watchdog means no timeout, you'd better not check with null
413: if (watchdog != null) {
414: wasKilled = watchdog.killedProcess();
415: }
416: }
417:
418: // if there is an error/failure and that it should halt, stop
419: // everything otherwise just log a statement
420: boolean errorOccurred = exitValue == JDependTask.ERRORS
421: || wasKilled;
422:
423: if (errorOccurred) {
424: String errorMessage = "JDepend FAILED"
425: + (wasKilled ? " - Timed out" : "");
426:
427: if (getHaltonerror()) {
428: throw new BuildException(errorMessage, getLocation());
429: } else {
430: log(errorMessage, Project.MSG_ERR);
431: }
432: }
433: }
434:
435: // this comment extract from JUnit Task may also apply here
436: // "in VM is not very nice since it could probably hang the
437: // whole build. IMHO this method should be avoided and it would be best
438: // to remove it in future versions. TBD. (SBa)"
439:
440: /**
441: * Execute inside VM.
442: *
443: * @param commandline the command line
444: * @return the return value of the mvm
445: * @exception BuildException if an error occurs
446: */
447: public int executeInVM(CommandlineJava commandline)
448: throws BuildException {
449: jdepend.textui.JDepend jdepend;
450:
451: if ("xml".equals(format)) {
452: jdepend = new jdepend.xmlui.JDepend();
453: } else {
454: jdepend = new jdepend.textui.JDepend();
455: }
456:
457: FileWriter fw = null;
458: if (getOutputFile() != null) {
459: try {
460: fw = new FileWriter(getOutputFile().getPath());
461: } catch (IOException e) {
462: String msg = "JDepend Failed when creating the output file: "
463: + e.getMessage();
464: log(msg);
465: throw new BuildException(msg);
466: }
467: jdepend.setWriter(new PrintWriter(fw));
468: log("Output to be stored in " + getOutputFile().getPath());
469: }
470:
471: try {
472: if (getClassespath() != null) {
473: // This is the new, better way - use classespath instead
474: // of sourcespath. The code is currently the same - you
475: // need class files in a directory to use this or jar files.
476: String[] cP = getClassespath().list();
477: for (int i = 0; i < cP.length; i++) {
478: File f = new File(cP[i]);
479: // not necessary as JDepend would fail, but why loose
480: // some time?
481: if (!f.exists()) {
482: String msg = "\""
483: + f.getPath()
484: + "\" does not represent a valid"
485: + " file or directory. JDepend would fail.";
486: log(msg);
487: throw new BuildException(msg);
488: }
489: try {
490: jdepend.addDirectory(f.getPath());
491: } catch (IOException e) {
492: String msg = "JDepend Failed when adding a class directory: "
493: + e.getMessage();
494: log(msg);
495: throw new BuildException(msg);
496: }
497: }
498:
499: } else if (getSourcespath() != null) {
500:
501: // This is the old way and is deprecated - classespath is
502: // the right way to do this and is above
503: String[] sP = getSourcespath().list();
504: for (int i = 0; i < sP.length; i++) {
505: File f = new File(sP[i]);
506:
507: // not necessary as JDepend would fail, but why loose
508: // some time?
509: if (!f.exists() || !f.isDirectory()) {
510: String msg = "\"" + f.getPath()
511: + "\" does not represent a valid"
512: + " directory. JDepend would fail.";
513: log(msg);
514: throw new BuildException(msg);
515: }
516: try {
517: jdepend.addDirectory(f.getPath());
518: } catch (IOException e) {
519: String msg = "JDepend Failed when adding a source directory: "
520: + e.getMessage();
521: log(msg);
522: throw new BuildException(msg);
523: }
524: }
525: }
526:
527: // This bit turns <exclude> child tags into patters to ignore
528: String[] patterns = defaultPatterns
529: .getExcludePatterns(getProject());
530: if (patterns != null && patterns.length > 0) {
531: if (setFilter != null) {
532: Vector v = new Vector();
533: for (int i = 0; i < patterns.length; i++) {
534: v.addElement(patterns[i]);
535: }
536: try {
537: Object o = packageFilterC
538: .newInstance(new Object[] { v });
539: setFilter.invoke(jdepend, new Object[] { o });
540: } catch (Throwable e) {
541: log(
542: "excludes will be ignored as JDepend doesn't like me: "
543: + e.getMessage(),
544: Project.MSG_WARN);
545: }
546: } else {
547: log(
548: "Sorry, your version of JDepend doesn't support excludes",
549: Project.MSG_WARN);
550: }
551: }
552:
553: jdepend.analyze();
554: } finally {
555: FileUtils.close(fw);
556: }
557: return SUCCESS;
558: }
559:
560: /**
561: * Execute the task by forking a new JVM. The command will block until
562: * it finishes. To know if the process was destroyed or not, use the
563: * <tt>killedProcess()</tt> method of the watchdog class.
564: * @param commandline the commandline for forked jvm
565: * @param watchdog the watchdog in charge of cancelling the test if it
566: * exceeds a certain amount of time. Can be <tt>null</tt>.
567: * @return the result of running the jdepend
568: * @throws BuildException in case of error
569: */
570: // JL: comment extracted from JUnitTask (and slightly modified)
571: public int executeAsForked(CommandlineJava commandline,
572: ExecuteWatchdog watchdog) throws BuildException {
573: runtimeClasses = new Path(getProject());
574: addClasspathEntry("/jdepend/textui/JDepend.class");
575:
576: // if not set, auto-create the ClassPath from the project
577: createClasspath();
578:
579: // not sure whether this test is needed but cost nothing to put.
580: // hope it will be reviewed by anybody competent
581: if (getClasspath().toString().length() > 0) {
582: createJvmarg(commandline).setValue("-classpath");
583: createJvmarg(commandline).setValue(
584: getClasspath().toString());
585: }
586:
587: if (includeRuntime) {
588: Vector v = Execute.getProcEnvironment();
589: Enumeration e = v.elements();
590: while (e.hasMoreElements()) {
591: String s = (String) e.nextElement();
592: if (s.startsWith("CLASSPATH=")) {
593: commandline.createClasspath(getProject())
594: .createPath().append(
595: new Path(getProject(), s
596: .substring("CLASSPATH="
597: .length())));
598: }
599: }
600: log(
601: "Implicitly adding " + runtimeClasses
602: + " to CLASSPATH", Project.MSG_VERBOSE);
603: commandline.createClasspath(getProject()).createPath()
604: .append(runtimeClasses);
605: }
606:
607: if (getOutputFile() != null) {
608: // having a space between the file and its path causes commandline
609: // to add quotes around the argument thus making JDepend not taking
610: // it into account. Thus we split it in two
611: commandline.createArgument().setValue("-file");
612: commandline.createArgument().setValue(outputFile.getPath());
613: // we have to find a cleaner way to put this output
614: }
615:
616: if (getSourcespath() != null) {
617: // This is deprecated - use classespath in the future
618: String[] sP = getSourcespath().list();
619: for (int i = 0; i < sP.length; i++) {
620: File f = new File(sP[i]);
621:
622: // not necessary as JDepend would fail, but why loose
623: // some time?
624: if (!f.exists() || !f.isDirectory()) {
625: throw new BuildException("\"" + f.getPath()
626: + "\" does not represent a valid"
627: + " directory. JDepend would" + " fail.");
628: }
629: commandline.createArgument().setValue(f.getPath());
630: }
631: }
632:
633: if (getClassespath() != null) {
634: // This is the new way - use classespath - code is the
635: // same for now
636: String[] cP = getClassespath().list();
637: for (int i = 0; i < cP.length; i++) {
638: File f = new File(cP[i]);
639: // not necessary as JDepend would fail, but why loose
640: // some time?
641: if (!f.exists()) {
642: throw new BuildException("\"" + f.getPath()
643: + "\" does not represent a valid"
644: + " file or directory. JDepend would"
645: + " fail.");
646: }
647: commandline.createArgument().setValue(f.getPath());
648: }
649: }
650:
651: Execute execute = new Execute(new LogStreamHandler(this ,
652: Project.MSG_INFO, Project.MSG_WARN), watchdog);
653: execute.setCommandline(commandline.getCommandline());
654: if (getDir() != null) {
655: execute.setWorkingDirectory(getDir());
656: execute.setAntRun(getProject());
657: }
658:
659: if (getOutputFile() != null) {
660: log("Output to be stored in " + getOutputFile().getPath());
661: }
662: log(commandline.describeCommand(), Project.MSG_VERBOSE);
663: try {
664: return execute.execute();
665: } catch (IOException e) {
666: throw new BuildException("Process fork failed.", e,
667: getLocation());
668: }
669: }
670:
671: /**
672: * @return <tt>null</tt> if there is a timeout value, otherwise the
673: * watchdog instance.
674: * @throws BuildException in case of error
675: */
676: protected ExecuteWatchdog createWatchdog() throws BuildException {
677: if (getTimeout() == null) {
678: return null;
679: }
680: return new ExecuteWatchdog(getTimeout().longValue());
681: }
682: }
|