Source code: com/further/jaudit/SystemConfiguration.java
1 /*
2 * SystemConfiguration.java
3 * Copyright (c) 2001, Kristopher Wehner
4 * Created on September 16, 2001, 12:59 AM
5 */
6
7 package com.further.jaudit;
8
9 import java.io.IOException;
10 import java.io.File;
11 import java.util.Map;
12 import java.util.HashMap;
13 import java.util.Iterator;
14 import java.util.List;
15 import java.util.LinkedList;
16 import java.util.Set;
17 import java.util.HashSet;
18
19 import javax.xml.parsers.SAXParser;
20 import javax.xml.parsers.SAXParserFactory;
21 import javax.xml.parsers.ParserConfigurationException;
22 import org.xml.sax.helpers.DefaultHandler;
23 import org.xml.sax.SAXParseException;
24 import org.xml.sax.Attributes;
25 import org.xml.sax.SAXException;
26 import org.apache.log4j.Category;
27 import org.apache.log4j.BasicConfigurator;
28
29 /**
30 * The system configuration holds the static configuration for the audit
31 * system, including what metrics are collected and what their names are,
32 * and the base directory of the source tree that is to be audited. The
33 * configuration itself is done using XML and parsed using xerces SAX2
34 * parser. DTD and an example configuration are in jaudit/resource/xml.
35 *
36 * @author Kris Wehner <kris@further.com>
37 * @version $Id: SystemConfiguration.java,v 1.1.1.1 2001/10/11 16:42:08 krisw Exp $
38 * @since 1.0
39 */
40 public class SystemConfiguration {
41 /**
42 * log4j audit channel
43 */
44 private static Category category =
45 Category.getInstance(SystemConfiguration.class.getName());
46
47 /**
48 * The root source directory. Every directory under this directory
49 * is assumed to contain source, and all files in this directory are
50 * candidates for audit if they have the correct file extension.
51 */
52 private static String rootSourceDirectory;
53
54 /**
55 * The set of ignored directories when doing a directory listing.
56 * This is used, among other things, to ignore the CVS directories
57 * that many projects have
58 */
59 private static Set ignoredDirectories = new HashSet();
60 /**
61 * Should the output source be formatted using a pretty printer,
62 * or should it come out as text/plain
63 */
64 private static boolean formatSource = false;
65
66
67 /**
68 * The number of milliseconds between source audits. An audit older than
69 * this value is flagged as "expired", and the file is considered to be
70 * equivilent to unaudited
71 */
72 private static long auditFrequency;
73 /**
74 * The user specified foreground color
75 */
76 private static String foregroundColor;
77 /**
78 * The user specified background color
79 */
80 private static String backgroundColor;
81 /**
82 * The user specified keyword color
83 */
84 private static String keywordColor;
85 /**
86 * The user specified comment color
87 */
88 private static String commentColor;
89 /**
90 * The color for a file that has passed audit
91 */
92 private static String passedAuditColor;
93 /**
94 * The color for a file that has failed audit
95 */
96 private static String failedAuditColor;
97 /**
98 * The color for a file that needs to be audited, either due to
99 * age or complete lack of all audits
100 */
101 private static String unauditedColor;
102 /**
103 * The color for links in the html page
104 */
105 private static String linkColor;
106 /**
107 * The color for viewed links in the html output
108 */
109 private static String vlinkColor;
110
111 /**
112 * The map of active MetricCreator objects in the system. These are
113 * built during configuration parsing, then used in the factory
114 * methods to create the metric sets used in the system. The map
115 * keys are the metric abbreviations and the values are the MetricCreators.
116 */
117 private static Map metricCreatorMap = new HashMap();
118
119 /**
120 * The metric creator is responsible for receiving raw unparsed
121 * string values from the SAX parser, then assembling them to
122 * produce concrete metric instances.
123 *
124 */
125 private static class MetricCreator {
126 /**
127 * The value for the metric name
128 */
129 private String metricName;
130 /**
131 * The value for the metric abbreviation
132 */
133 private String abbreviation;
134 /**
135 * The metric type. Allowable values are boolean and fixed-range
136 */
137 private String metricType;
138 /**
139 * The metric default value
140 */
141 private String defaultValue;
142 /**
143 * The required metric value to ensure that a metric "passes" for a
144 * source file
145 */
146 private String requiredValue;
147 /**
148 * The low end of the acceptable range for fixed-range
149 */
150 private String lowerLimit;
151 /**
152 * The high end of the acceptable range for fixed-range
153 */
154 private String upperLimit;
155 /**
156 * The description for the given metric
157 */
158 private String description;
159 /**
160 * The list of possible values, if the metric is an enumeration
161 */
162 private LinkedList possibleValues = new LinkedList();
163
164 /**
165 * Set the name of the metric
166 *
167 * @param name The non-null metric name, as contained in the <name> tag
168 */
169 public void setMetricName(String name) {
170 metricName = name;
171 }
172
173 /**
174 * Set the metric abbreviation. This is the primary id used for referencing
175 * the metric, and, as such, must be unique across all the metric instances.
176 *
177 * @param abbrev The non-null metric abbreviation, from the <abbreviation> tag
178 */
179 public void setMetricAbbreviation(String abbrev) {
180 abbreviation = abbrev;
181 }
182
183 /**
184 * Get the metric abbreviation, used for registering the metric creator
185 * in the creator map.
186 *
187 * @return The abbreviation for the metric.
188 */
189 public String getMetricAbbreviation() {
190 return abbreviation;
191 }
192
193 /**
194 * Set the metric type, one of "boolean" or "fixed-range". This determines
195 * the concrete metric subclass that will be produced.
196 *
197 * @param type The metric type
198 */
199 public void setMetricType(String type) {
200 metricType = type;
201 }
202
203 /**
204 * Set the description for the metric, which is the longer bit of
205 * text that explains the purpose of the metric.
206 *
207 * @param description The description for the metric
208 */
209 public void setDescription(String description) {
210 this.description = description;
211 }
212
213 /**
214 * Set the default value for the metric. This must be consistent
215 * with the metric type, and is used when a new metric is created.
216 *
217 * @param defaultValue The default value for the metric as a string
218 */
219 public void setDefaultValue(String defaultValue) {
220 this.defaultValue = defaultValue;
221 }
222
223 /**
224 * Set the required value for the metric. For the rules governing
225 * the format of this value, see {@link #setDefaultValue(String)}
226 *
227 * @param requiredValue The required value for the metric as a string
228 */
229 public void setRequiredValue(String requiredValue) {
230 this.requiredValue = requiredValue;
231 }
232
233 /**
234 * Set the lower limit for a fixed-range metric.
235 *
236 * @param lower The lower limit for a fixed range metric
237 */
238 public void setLowerLimit(String lower) {
239 lowerLimit = lower;
240 }
241
242 /**
243 * Set the upper limit for a fixed-range metric
244 *
245 * @param upper The upper limit for a fixed range metric
246 */
247 public void setUpperLimit(String upper) {
248 upperLimit = upper;
249 }
250
251 /**
252 * Add a value for an enumerated type metric
253 *
254 * @param value The possible value for the metric to add
255 */
256 public void addValue(String value) {
257 possibleValues.addLast(value);
258 }
259
260 /**
261 * Utility method to convert a string value into a boolean.
262 * This performs the requisite validation to ensure that
263 * the value is consistent with the metric.
264 *
265 * @param value The string value to convert to boolean
266 * @return The boolean value represented by the string
267 * @throws ConfigurationExpection If the string is not a valid boolean
268 */
269 private boolean booleanValue(String value)
270 throws ConfigurationException {
271 if ("true".equalsIgnoreCase(value) ||
272 "false".equalsIgnoreCase(value))
273 return Boolean.valueOf(value).booleanValue();
274 else
275 throw new ConfigurationException("Invalid value for boolean: " + value);
276 }
277
278 /**
279 * Utility method to convert a string value into a double.
280 * This performs the requisite validation to ensure that the
281 * value is consistent with a valid double
282 *
283 * @param value The string value to convert to double
284 * @return The double value represented by the string
285 * @throws ConfigurationException If the string value is not a valid double
286 */
287 private double doubleValue(String value)
288 throws ConfigurationException {
289 try {
290 return Double.valueOf(value).doubleValue();
291 } catch (NumberFormatException nfe) {
292 throw new ConfigurationException(nfe.toString());
293 }
294 }
295
296 /**
297 * Validate that the configuration is consistent with the
298 * specified metric type. If this method returns true, then
299 * the metric creator will be able to succesfully create
300 * metrics with createMetric()
301 *
302 * @throws ConfigurationException If the configuration is not
303 * consistent with metric type
304 */
305 public void validate()
306 throws ConfigurationException {
307 if ("fixed-range".equals(metricType)) {
308 if ((defaultValue == null) ||
309 (lowerLimit == null) ||
310 (requiredValue == null) ||
311 (upperLimit == null))
312 throw new ConfigurationException("Missing required tag value");
313 doubleValue(defaultValue);
314 doubleValue(lowerLimit);
315 doubleValue(upperLimit);
316 doubleValue(requiredValue);
317 }
318 else if ("boolean".equals(metricType)) {
319 if (defaultValue == null)
320 throw new ConfigurationException("Default value cannot be null");
321 if (requiredValue == null)
322 throw new ConfigurationException("Required value cannot be null");
323 booleanValue(defaultValue);
324 booleanValue(requiredValue);
325 } else if ("enumeration".equals(metricType)) {
326 if (defaultValue == null)
327 throw new ConfigurationException("Default value cannot be null");
328 if (requiredValue == null)
329 throw new ConfigurationException("Required value cannot be null");
330 if (!possibleValues.contains(defaultValue))
331 throw new ConfigurationException("Default value not in values list");
332 if (!possibleValues.contains(requiredValue))
333 throw new ConfigurationException("Required value not in values list");
334 } else
335 throw new ConfigurationException("Unknown metric type: " + metricType);
336 }
337
338
339
340 /**
341 * Factory method to produce concrete metric instances for
342 * the given metric type string. This attempts to initialize
343 * the metric, and if it is unable, a runtime error is raised
344 * Clients are expected to validate the configuration values with
345 * validate() before ever calling createMetric().
346 *
347 * @return An initialized metric instance
348 * @throws Error If the data is inconsistent to allow the metric
349 * to be created
350 */
351 public SourceMetric createMetric() {
352 try {
353 // FIXME: I'd really like to be rid of this if block. This
354 // is an admission of a chink in the design.
355 if ("fixed-range".equals(metricType)) {
356 double lower = doubleValue(lowerLimit);
357 double upper = doubleValue(upperLimit);
358 double defaultDouble =
359 doubleValue(defaultValue);
360 double required =
361 doubleValue(requiredValue);
362 FixedRangeMetric metric =
363 new FixedRangeMetric(metricName, abbreviation, description,
364 lower,upper,required);
365 metric.setMetricValue(defaultDouble);
366 return metric;
367 }
368 if ("boolean".equals(metricType)) {
369 boolean defaultBoolean =
370 booleanValue(defaultValue);
371 boolean required =
372 booleanValue(requiredValue);
373 BooleanMetric metric =
374 new BooleanMetric(metricName, abbreviation, description,required);
375 metric.setMetricValue(defaultBoolean);
376 return metric;
377 }
378 if ("enumeration".equals(metricType)) {
379 EnumeratedTypeMetric metric =
380 new EnumeratedTypeMetric(metricName, abbreviation,
381 description, possibleValues, requiredValue);
382 metric.setMetricValue(defaultValue);
383 return metric;
384 }
385 throw new Error("Invalid metric type: " + metricType);
386 } catch (ConfigurationException ce) {
387 throw new Error(ce.toString());
388 }
389 }
390
391 }
392
393 /**
394 * Configuration file parser handler. This is the SAX2 parser
395 * handler for parsing the XML configuration files for the system.
396 * It makes callbacks onto private methods on SystemConfiguration
397 * to set the configuration data.
398 *
399 * @author kris wehner <kris@further.com>
400 */
401 private static class ConfigurationHandler extends DefaultHandler {
402 /**
403 * The elementContents string contains the last parsed character
404 * data from the previous tag that was processed. This is used
405 * to set specific data values when an endElement event is received.
406 */
407 private String elementContents;
408
409 /**
410 * The current metric creator. This is used to register the configuration
411 * values as they are parsed, and then registered in the metric creator
412 * map when a </metric> is encountered.
413 */
414 private MetricCreator metricCreator;
415
416 /**
417 * The start element handler for the parser.
418 */
419 public void startElement(String uri, String localName,
420 String qName, Attributes attributes) {
421
422 if ("config".equals(qName)) {
423 // Start of the configuration, extract the source root
424 rootSourceDirectory =
425 attributes.getValue("srcroot");
426 category.debug("Root source directory is " + rootSourceDirectory);
427 }
428 if ("metric".equals(qName)) {
429 // Start metric, create a new metric creator and register
430 // the metric type
431 metricCreator = new MetricCreator();
432 metricCreator.setMetricType(attributes.getValue("type"));
433 category.debug("New metric, type is " + attributes.getValue("type"));
434 }
435 if ("source".equals(qName)) {
436 // This is the source formatting flag, and has to be either
437 // "true" or "false"
438 formatSource = Boolean.valueOf(attributes.getValue("format")).booleanValue();
439 category.debug("Format source is " + formatSource);
440 }
441 }
442
443 public void characters(char[] ch, int start, int length) {
444 char[] chars = new char[length];
445 int j = 0;
446 for (int i = start; i < length; i++) {
447 chars[j++] = ch[i];
448 }
449 elementContents = new String(chars);
450 category.debug("Element contents = " + elementContents);
451 }
452
453 public void endElement(String uri, String localName, String qName) {
454 if ("metric".equals(qName)) {
455 metricCreatorMap.put(metricCreator.getMetricAbbreviation(),metricCreator);
456 category.debug("Register metric creator for " + metricCreator.getMetricAbbreviation());
457 }
458 if ("name".equals(qName)) {
459 metricCreator.setMetricName(elementContents);
460 category.debug("Metric name is " + elementContents);
461 }
462 if ("abbreviation".equals(qName)) {
463 metricCreator.setMetricAbbreviation(elementContents);
464 category.debug("Metric abbreviation is " + elementContents);
465 }
466 if ("description".equals(qName)) {
467 metricCreator.setDescription(elementContents);
468 category.debug("Metric description is " + elementContents);
469 }
470 if ("default".equals(qName)) {
471 metricCreator.setDefaultValue(elementContents);
472 category.debug("Default value is " + elementContents);
473 }
474 if ("required-value".equals(qName)) {
475 metricCreator.setRequiredValue(elementContents);
476 category.debug("Required value is " + elementContents);
477 }
478 if ("value".equals(qName)) {
479 metricCreator.addValue(elementContents);
480 category.debug("Possible value is " + elementContents);
481 }
482 if ("lowerlimit".equals(qName)) {
483 metricCreator.setLowerLimit(elementContents);
484 category.debug("Lower limit is " + elementContents);
485 }
486 if ("upperlimit".equals(qName)) {
487 metricCreator.setUpperLimit(elementContents);
488 category.debug("Upper limit is " + elementContents);
489 }
490 if ("audit-frequency".equals(qName)) {
491 try {
492 auditFrequency = Long.valueOf(elementContents).longValue();
493 } catch (NumberFormatException nfe) {
494 auditFrequency = 0;
495 }
496 category.debug("Audit frequency is " + auditFrequency);
497 }
498 if ("srcext".equals(qName)) {
499 SourceFilenameFilter.registerSourceExtension(elementContents);
500 category.debug("Register source extension: " + elementContents);
501 }
502 if ("ignore-directory".equals(qName)) {
503 addIgnoredDirectory(elementContents);
504 category.debug("Register ignored directory " + elementContents);
505 }
506 if ("foreground-color".equals(qName)) {
507 foregroundColor = elementContents;
508 category.debug("Foreground color is set to " + foregroundColor);
509 }
510 if ("background-color".equals(qName)) {
511 backgroundColor = elementContents;
512 category.debug("Background color is set to " + backgroundColor);
513 }
514 if ("link-color".equals(qName)) {
515 linkColor = elementContents;
516 category.debug("Link color is set to " + linkColor);
517 }
518 if ("vlink-color".equals(qName)) {
519 vlinkColor = elementContents;
520 category.debug("VLink color is set to " + vlinkColor);
521 }
522 if ("keyword-color".equals(qName)) {
523 keywordColor = elementContents;
524 category.debug("Keyword color is set to " + keywordColor);
525 }
526 if ("comment-color".equals(qName)) {
527 commentColor = elementContents;
528 category.debug("Comment color is set to " + commentColor);
529 }
530 if ("passed-color".equals(qName)) {
531 passedAuditColor = elementContents;
532 category.debug("Passing audit color is set to " + passedAuditColor);
533 }
534 if ("failed-color".equals(qName)) {
535 failedAuditColor = elementContents;
536 category.debug("Failing audit color is set to " + failedAuditColor);
537 }
538 if ("unaudited-color".equals(qName)) {
539 unauditedColor = elementContents;
540 category.debug("Unaudited color is set to " + unauditedColor);
541 }
542 }
543
544 public void fatalError(SAXParseException ex) {
545 System.out.println("Error parsing configuration: line " + ex.getLineNumber() + " col " + ex.getColumnNumber() + ": " + ex.toString());
546 }
547
548 public void error(SAXParseException ex) {
549 System.out.println("Error parsing configuration: line " + ex.getLineNumber() + " col " + ex.getColumnNumber() + ": " + ex.toString());
550 }
551 }
552
553 /**
554 * Set the given directory name as an ignored directory, so it will not
555 * show up in source listings.
556 *
557 * @param directory The name of the directory to ignore
558 */
559 private static void addIgnoredDirectory(String directory) {
560 ignoredDirectories.add(directory);
561 }
562
563 /**
564 * Is the given directory an ignored directory? If it is ignored, it
565 * should not show up in directory listings.
566 *
567 * @param directory The directory to check for being in the ignore list
568 * @return true if the directory is ignored, false otherwise
569 */
570 public static boolean isIgnoredDirectory(String directory) {
571 return ignoredDirectories.contains(directory);
572 }
573
574 /**
575 * Validate the entire set of metric creators and the root source directory.
576 * This is done at bootstrap
577 * to ensure configuration integrity, and causes the system to not start
578 * up if it fails. Once run, we can be sure that the metric creators are
579 * logically consistent and can be used in the factory methods.
580 */
581 private static void validateConfiguration()
582 throws ConfigurationException {
583 for (Iterator i = metricCreatorMap.values().iterator(); i.hasNext(); ) {
584 MetricCreator creator =
585 (MetricCreator)i.next();
586 creator.validate();
587 }
588 File rootDirectory = new File(rootSourceDirectory);
589 if (!rootDirectory.exists())
590 throw new ConfigurationException("Root source directory does not exist");
591 }
592
593 /**
594 * Retreive the audit frequency for the source files in milliseconds. If the
595 * source file audit is older than this value, the file should be reaudited.
596 *
597 * @return The audit frequency for the file, in milliseconds
598 */
599 public static long getAuditFrequency() {
600 return auditFrequency;
601 }
602
603 /**
604 * Should the displayed source code be formatted using a pretty printer,
605 * or should it be displayed entirely unformatted as text/plain?
606 *
607 * @return true if the source should be displayed formatted and colored
608 */
609 public static boolean getFormatSource() {
610 return formatSource;
611 }
612
613 /**
614 * Retreive the root directory for the source code to collect metrics for.
615 * This is the base of the source directory, and must exist.
616 *
617 * @return The string name of the root source directory
618 */
619 public static String getRootSourceDirectory() {
620 return rootSourceDirectory;
621 }
622
623 /**
624 * Get the foreground color that the user selected, in the format
625 * XXXXXX, which is the hex RGB of the color without the leading #
626 *
627 * @return The user configured foreground color
628 */
629 public static String getForegroundColor() {
630 return foregroundColor;
631 }
632
633 /**
634 * Get the background color that the user selected.
635 *
636 * @return The user configured background color
637 * @see #getForegroundColor
638 */
639 public static String getBackgroundColor() {
640 return backgroundColor;
641 }
642
643 /**
644 * Get the link color that the user selected.
645 *
646 * @return The user configured link color
647 * @see #getForegroundColor
648 */
649 public static String getLinkColor() {
650 return linkColor;
651 }
652
653 /**
654 * Get the vlink color that the user selected.
655 *
656 * @return The user configured vlink color
657 * @see #getForegroundColor
658 */
659 public static String getVlinkColor() {
660 return vlinkColor;
661 }
662
663 /**
664 * Get the keyword color that the user selected.
665 *
666 * @return The user configured keyword color
667 * @see #getForegroundColor
668 */
669 public static String getKeywordColor() {
670 return keywordColor;
671 }
672
673 /**
674 * Get the comment color that the user selected.
675 *
676 * @return The user configured comment color
677 * @see #getForegroundColor
678 */
679 public static String getCommentColor() {
680 return commentColor;
681 }
682
683 /**
684 * Get the passing audit color the user selected
685 *
686 * @return The user configured passing audit color
687 * @see #getForegroundColor
688 */
689 public static String getPassedAuditColor() {
690 return passedAuditColor;
691 }
692
693 /**
694 * Get the failing audit color the user selected
695 *
696 * @return The user configured failing audit color
697 * @see #getForegroundColor
698 */
699 public static String getFailedAuditColor() {
700 return failedAuditColor;
701 }
702
703 /**
704 * Get the unaudited color the user selected
705 *
706 * @return The user configured unaudited color
707 * @see #getForegroundColor
708 */
709 public static String getUnauditedColor() {
710 return unauditedColor;
711 }
712
713 /**
714 * Create a new set of metrics, initializing defaults and ranges. This
715 * set is determined by the system configuration, and will be used
716 * for all source audits in the system.
717 *
718 * @return A list of metrics, initialized and ordered in the same order
719 * as they were specified in the configuration
720 */
721 public static List createMetrics() {
722 List metricList = new LinkedList();
723 for (Iterator i = metricCreatorMap.keySet().iterator(); i.hasNext(); ) {
724 String key = (String)i.next();
725 MetricCreator creator = (MetricCreator)metricCreatorMap.get(key);
726 metricList.add(creator.createMetric());
727 }
728 return metricList;
729 }
730
731 /**
732 * Create a metric for a given metric abbreviation. This is the simple
733 * factory method used for individual metric creation.
734 *
735 * @param id The abbreviation for the metric to create
736 * @throws IllegalArgumentException If the metric for the given abbreviation
737 * is not found.
738 */
739 public static SourceMetric createMetric(String id) {
740 if (!metricCreatorMap.containsKey(id))
741 throw new IllegalArgumentException("Metric not found for " + id);
742 MetricCreator creator =
743 (MetricCreator)metricCreatorMap.get(id);
744 return creator.createMetric();
745 }
746
747
748 /**
749 * Load the configuration file at the given path. This loads all
750 * the system configuration data, including the metric types to
751 * load, and all of the source directory paths.
752 *
753 * @param configurationFile The configuration file to load and parse
754 * @throws ConfigurationException If the configuration file cannot be parsed
755 */
756 public static void parseConfiguration(String configurationFile)
757 throws ConfigurationException {
758 try {
759 // This is pure xerces boilerplate to initialize the XML parser
760 SAXParserFactory parserFactory = SAXParserFactory.newInstance();
761 SAXParser saxParser = parserFactory.newSAXParser();
762 // Perform the parsing, which leaves the system with a complete
763 // configuration (source extensions, a root directory and
764 // a set of possibly inconsistent metric creators)
765 saxParser.parse(configurationFile,new ConfigurationHandler());
766 // Ensure the metric creators are logically consistent so they
767 // can be used in the factory methods. If this passes, we
768 // have a valid system.
769 validateConfiguration();
770 } catch (IOException ex) {
771 if (category.isDebugEnabled())
772 ex.printStackTrace();
773 throw new ConfigurationException(ex.getMessage());
774 } catch (ParserConfigurationException ex) {
775 if (category.isDebugEnabled())
776 ex.printStackTrace();
777 throw new ConfigurationException(ex.getMessage());
778 } catch (SAXException ex) {
779 if (category.isDebugEnabled())
780 ex.printStackTrace();
781 throw new ConfigurationException(ex.getMessage());
782 }
783 }
784
785 /**
786 * SystemConfiguration objects are not creatable. All public functionality
787 * of the SystemConfiguration is accessed through static functions.
788 */
789 private SystemConfiguration() {
790 }
791
792 }