1. /*
  2. * Copyright 2001-2002,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.metamata;
  18. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.InputStreamReader;
  22. import java.io.OutputStream;
  23. import java.io.OutputStreamWriter;
  24. import java.text.DecimalFormat;
  25. import java.text.NumberFormat;
  26. import java.text.ParseException;
  27. import java.util.Date;
  28. import java.util.EmptyStackException;
  29. import java.util.Enumeration;
  30. import java.util.Stack;
  31. import java.util.Vector;
  32. import javax.xml.transform.OutputKeys;
  33. import javax.xml.transform.Transformer;
  34. import javax.xml.transform.TransformerFactory;
  35. import javax.xml.transform.sax.SAXTransformerFactory;
  36. import javax.xml.transform.sax.TransformerHandler;
  37. import javax.xml.transform.stream.StreamResult;
  38. import org.apache.tools.ant.BuildException;
  39. import org.apache.tools.ant.Project;
  40. import org.apache.tools.ant.Task;
  41. import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;
  42. import org.apache.tools.ant.util.DateUtils;
  43. import org.xml.sax.Attributes;
  44. import org.xml.sax.SAXException;
  45. import org.xml.sax.helpers.AttributesImpl;
  46. /**
  47. * A handy metrics handler. Most of this code was done only with the
  48. * screenshots on the documentation since the evaluation version as
  49. * of this writing does not allow to save metrics or to run it via
  50. * command line.
  51. * <p>
  52. * This class can be used to transform a text file or to process the
  53. * output stream directly.
  54. *
  55. */
  56. public class MMetricsStreamHandler implements ExecuteStreamHandler {
  57. /** CLASS construct, it should be named something like 'MyClass' */
  58. private static final String CLASS = "class";
  59. /** package construct, it should be look like 'com.mycompany.something' */
  60. private static final String PACKAGE = "package";
  61. /** FILE construct, it should look like something 'MyClass.java' or 'MyClass.class' */
  62. private static final String FILE = "file";
  63. /** METHOD construct, it should looke like something 'doSomething(...)' or 'doSomething()' */
  64. private static final String METHOD = "method";
  65. private static final String[] ATTRIBUTES = {
  66. "name", "vg", "loc", "dit", "noa", "nrm", "nlm", "wmc",
  67. "rfc", "dac", "fanout", "cbo", "lcom", "nocl"};
  68. /** reader for stdout */
  69. private InputStream metricsOutput;
  70. /**
  71. * this is where the XML output will go, should mostly be a file
  72. * the caller is responsible for flushing and closing this stream
  73. */
  74. private OutputStream xmlOutputStream;
  75. /** metrics handler */
  76. private TransformerHandler metricsHandler;
  77. /** the task */
  78. private Task task;
  79. /**
  80. * the stack where are stored the metrics element so that they we can
  81. * know if we have to close an element or not.
  82. */
  83. private Stack stack = new Stack();
  84. /** initialize this handler */
  85. MMetricsStreamHandler(Task task, OutputStream xmlOut) {
  86. this.task = task;
  87. this.xmlOutputStream = xmlOut;
  88. }
  89. /** Ignore. */
  90. public void setProcessInputStream(OutputStream p1) throws IOException {
  91. }
  92. /** Ignore. */
  93. public void setProcessErrorStream(InputStream p1) throws IOException {
  94. }
  95. /** Set the inputstream */
  96. public void setProcessOutputStream(InputStream is) throws IOException {
  97. metricsOutput = is;
  98. }
  99. public void start() throws IOException {
  100. // create the transformer handler that will be used to serialize
  101. // the output.
  102. TransformerFactory factory = TransformerFactory.newInstance();
  103. if (!factory.getFeature(SAXTransformerFactory.FEATURE)) {
  104. throw new IllegalStateException("Invalid Transformer factory feature");
  105. }
  106. try {
  107. metricsHandler = ((SAXTransformerFactory) factory).newTransformerHandler();
  108. metricsHandler.setResult(new StreamResult(new OutputStreamWriter(xmlOutputStream, "UTF-8")));
  109. Transformer transformer = metricsHandler.getTransformer();
  110. transformer.setOutputProperty(OutputKeys.INDENT, "yes");
  111. // start the document with a 'metrics' root
  112. final Date now = new Date();
  113. metricsHandler.startDocument();
  114. AttributesImpl attr = new AttributesImpl();
  115. attr.addAttribute("", "company", "company", "CDATA", "metamata");
  116. attr.addAttribute("", "snapshot_created", "snapshot_created", "CDATA",
  117. DateUtils.format(now, DateUtils.ISO8601_DATETIME_PATTERN));
  118. // attr.addAttribute("", "elapsed_time", "elapsed_time", "CDATA",
  119. // String.valueOf(now.getTime() - program_start.getTime()));
  120. attr.addAttribute("", "program_start", "program_start", "CDATA",
  121. DateUtils.format(new Date(), DateUtils.ISO8601_DATETIME_PATTERN));
  122. metricsHandler.startElement("", "metrics", "metrics", attr);
  123. // now parse the whole thing
  124. parseOutput();
  125. } catch (Exception e) {
  126. throw new BuildException(e);
  127. }
  128. }
  129. /**
  130. * Pretty dangerous business here.
  131. */
  132. public void stop() {
  133. try {
  134. // we need to pop everything and close elements that have not been
  135. // closed yet.
  136. while (stack.size() > 0) {
  137. ElementEntry elem = (ElementEntry) stack.pop();
  138. metricsHandler.endElement("", elem.getType(), elem.getType());
  139. }
  140. // close the root
  141. metricsHandler.endElement("", "metrics", "metrics");
  142. // document is finished for good
  143. metricsHandler.endDocument();
  144. } catch (SAXException e) {
  145. e.printStackTrace();
  146. throw new IllegalStateException(e.getMessage());
  147. }
  148. }
  149. /** read each line and process it */
  150. protected void parseOutput() throws IOException, SAXException {
  151. BufferedReader br = new BufferedReader(new InputStreamReader(metricsOutput));
  152. String line = null;
  153. while ((line = br.readLine()) != null) {
  154. processLine(line);
  155. }
  156. }
  157. /**
  158. * Process a metrics line. If the metrics is invalid and that this is not
  159. * the header line, it is display as info.
  160. * @param line the line to process, it is normally a line full of metrics.
  161. */
  162. protected void processLine(String line) throws SAXException {
  163. if (line.startsWith("Construct\tV(G)\tLOC\tDIT\tNOA\tNRM\tNLM\tWMC\tRFC\tDAC\tFANOUT\tCBO\tLCOM\tNOCL")) {
  164. return;
  165. }
  166. try {
  167. MetricsElement elem = MetricsElement.parse(line);
  168. startElement(elem);
  169. } catch (ParseException e) {
  170. //e.printStackTrace();
  171. // invalid lines are sent to the output as information, it might be anything,
  172. task.log(line, Project.MSG_INFO);
  173. }
  174. }
  175. /**
  176. * Start a new construct. Elements are popped until we are on the same
  177. * parent node, then the element type is guessed and pushed on the
  178. * stack.
  179. * @param elem the element to process.
  180. * @throws SAXException thrown if there is a problem when sending SAX events.
  181. */
  182. protected void startElement(MetricsElement elem) throws SAXException {
  183. // if there are elements in the stack we possibly need to close one or
  184. // more elements previous to this one until we got its parent
  185. int indent = elem.getIndent();
  186. if (stack.size() > 0) {
  187. ElementEntry previous = (ElementEntry) stack.peek();
  188. // close nodes until you got the parent.
  189. try {
  190. while (indent <= previous.getIndent() && stack.size() > 0) {
  191. stack.pop();
  192. metricsHandler.endElement("", previous.getType(), previous.getType());
  193. previous = (ElementEntry) stack.peek();
  194. }
  195. } catch (EmptyStackException ignored) {
  196. }
  197. }
  198. // ok, now start the new construct
  199. String type = getConstructType(elem);
  200. Attributes attrs = createAttributes(elem);
  201. metricsHandler.startElement("", type, type, attrs);
  202. // make sure we keep track of what we did, that's history
  203. stack.push(new ElementEntry(type, indent));
  204. }
  205. /**
  206. * return the construct type of the element. We can hardly recognize the
  207. * type of a metrics element, so we are kind of forced to do some black
  208. * magic based on the name and indentation to recognize the type.
  209. * @param elem the metrics element to guess for its type.
  210. * @return the type of the metrics element, either PACKAGE, FILE, CLASS or
  211. * METHOD.
  212. */
  213. protected String getConstructType(MetricsElement elem) {
  214. // ok no doubt, it's a file
  215. if (elem.isCompilationUnit()) {
  216. return FILE;
  217. }
  218. // same, we're sure it's a method
  219. if (elem.isMethod()) {
  220. return METHOD;
  221. }
  222. // if it's empty, and none of the above it should be a package
  223. if (stack.size() == 0) {
  224. return PACKAGE;
  225. }
  226. // ok, this is now black magic time, we will guess the type based on
  227. // the previous type and its indent...
  228. final ElementEntry previous = (ElementEntry) stack.peek();
  229. final String prevType = previous.getType();
  230. final int prevIndent = previous.getIndent();
  231. final int indent = elem.getIndent();
  232. // we're just under a file with a bigger indent so it's a class
  233. if (prevType.equals(FILE) && indent > prevIndent) {
  234. return CLASS;
  235. }
  236. // we're just under a class with a greater or equals indent, it's a class
  237. // (there might be several classes in a compilation unit and inner classes as well)
  238. if (prevType.equals(CLASS) && indent >= prevIndent) {
  239. return CLASS;
  240. }
  241. // we assume the other are package
  242. return PACKAGE;
  243. }
  244. /**
  245. * Create all attributes of a MetricsElement skipping those who have an
  246. * empty string
  247. */
  248. protected Attributes createAttributes(MetricsElement elem) {
  249. AttributesImpl impl = new AttributesImpl();
  250. int i = 0;
  251. String name = ATTRIBUTES[i++];
  252. impl.addAttribute("", name, name, "CDATA", elem.getName());
  253. Enumeration metrics = elem.getMetrics();
  254. for (; metrics.hasMoreElements(); i++) {
  255. String value = (String) metrics.nextElement();
  256. if (value.length() > 0) {
  257. name = ATTRIBUTES[i];
  258. impl.addAttribute("", name, name, "CDATA", value);
  259. }
  260. }
  261. return impl;
  262. }
  263. /**
  264. * helper class to keep track of elements via its type and indent
  265. * that's all we need to guess a type.
  266. */
  267. private static final class ElementEntry {
  268. private String type;
  269. private int indent;
  270. ElementEntry(String type, int indent) {
  271. this.type = type;
  272. this.indent = indent;
  273. }
  274. public String getType() {
  275. return type;
  276. }
  277. public int getIndent() {
  278. return indent;
  279. }
  280. }
  281. }
  282. class MetricsElement {
  283. private static final NumberFormat METAMATA_NF;
  284. private static final NumberFormat NEUTRAL_NF;
  285. static {
  286. METAMATA_NF = NumberFormat.getInstance();
  287. METAMATA_NF.setMaximumFractionDigits(1);
  288. NEUTRAL_NF = NumberFormat.getInstance();
  289. if (NEUTRAL_NF instanceof DecimalFormat) {
  290. ((DecimalFormat) NEUTRAL_NF).applyPattern("###0.###;-###0.###");
  291. }
  292. NEUTRAL_NF.setMaximumFractionDigits(1);
  293. }
  294. private int indent;
  295. private String construct;
  296. private Vector metrics;
  297. MetricsElement(int indent, String construct, Vector metrics) {
  298. this.indent = indent;
  299. this.construct = construct;
  300. this.metrics = metrics;
  301. }
  302. public int getIndent() {
  303. return indent;
  304. }
  305. public String getName() {
  306. return construct;
  307. }
  308. public Enumeration getMetrics() {
  309. return metrics.elements();
  310. }
  311. public boolean isCompilationUnit() {
  312. return (construct.endsWith(".java") || construct.endsWith(".class"));
  313. }
  314. public boolean isMethod() {
  315. return (construct.endsWith("(...)") || construct.endsWith("()"));
  316. }
  317. public static MetricsElement parse(String line) throws ParseException {
  318. final Vector metrics = new Vector();
  319. int pos;
  320. // i'm using indexOf since I need to know if there are empty strings
  321. // between tabs and I find it easier than with StringTokenizer
  322. while ((pos = line.indexOf('\t')) != -1) {
  323. String token = line.substring(0, pos);
  324. // only parse what coudl be a valid number. ie not constructs nor no value
  325. /*if (metrics.size() != 0 || token.length() != 0) {
  326. Number num = METAMATA_NF.parse(token); // parse with Metamata NF
  327. token = NEUTRAL_NF.format(num.doubleValue()); // and format with a neutral NF
  328. }*/
  329. metrics.addElement(token);
  330. line = line.substring(pos + 1);
  331. }
  332. metrics.addElement(line);
  333. // there should be exactly 14 tokens (1 name + 13 metrics), if not, there is a problem !
  334. if (metrics.size() != 14) {
  335. throw new ParseException("Could not parse the following line as "
  336. + "a metrics: -->" + line + "<--", -1);
  337. }
  338. // remove the first token it's made of the indentation string and the
  339. // construct name, we'll need all this to figure out what type of
  340. // construct it is since we lost all semantics :(
  341. // (#indent[/]*)(#construct.*)
  342. String name = (String) metrics.elementAt(0);
  343. metrics.removeElementAt(0);
  344. int indent = 0;
  345. pos = name.lastIndexOf('/');
  346. if (pos != -1) {
  347. name = name.substring(pos + 1);
  348. indent = pos + 1; // indentation is last position of token + 1
  349. }
  350. return new MetricsElement(indent, name, metrics);
  351. }
  352. }