001: ////////////////////////////////////////////////////////////////////////////////
002: // checkstyle: Checks Java source code for adherence to a set of rules.
003: // Copyright (C) 2001-2007 Oliver Burn
004: //
005: // This library is free software; you can redistribute it and/or
006: // modify it under the terms of the GNU Lesser General Public
007: // License as published by the Free Software Foundation; either
008: // version 2.1 of the License, or (at your option) any later version.
009: //
010: // This library is distributed in the hope that it will be useful,
011: // but WITHOUT ANY WARRANTY; without even the implied warranty of
012: // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013: // Lesser General Public License for more details.
014: //
015: // You should have received a copy of the GNU Lesser General Public
016: // License along with this library; if not, write to the Free Software
017: // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018: ////////////////////////////////////////////////////////////////////////////////
019: package com.puppycrawl.tools.checkstyle;
020:
021: import java.io.BufferedInputStream;
022: import java.io.FileInputStream;
023: import java.io.FileNotFoundException;
024: import java.io.IOException;
025: import java.io.InputStream;
026: import java.net.MalformedURLException;
027: import java.net.URL;
028: import java.util.ArrayList;
029: import java.util.HashMap;
030: import java.util.Iterator;
031: import java.util.List;
032: import java.util.Map;
033: import java.util.Stack;
034: import javax.xml.parsers.ParserConfigurationException;
035:
036: import com.puppycrawl.tools.checkstyle.api.AbstractLoader;
037: import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
038: import com.puppycrawl.tools.checkstyle.api.Configuration;
039: import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
040:
041: import org.xml.sax.Attributes;
042: import org.xml.sax.InputSource;
043: import org.xml.sax.SAXException;
044: import org.xml.sax.SAXParseException;
045:
046: /**
047: * Loads a configuration from a standard configuration XML file.
048: *
049: * @author Oliver Burn
050: * @version 1.0
051: */
052: public final class ConfigurationLoader {
053: /** the public ID for version 1_0 of the configuration dtd */
054: private static final String DTD_PUBLIC_ID_1_0 = "-//Puppy Crawl//DTD Check Configuration 1.0//EN";
055:
056: /** the resource for version 1_0 of the configuration dtd */
057: private static final String DTD_RESOURCE_NAME_1_0 = "com/puppycrawl/tools/checkstyle/configuration_1_0.dtd";
058:
059: /** the public ID for version 1_1 of the configuration dtd */
060: private static final String DTD_PUBLIC_ID_1_1 = "-//Puppy Crawl//DTD Check Configuration 1.1//EN";
061:
062: /** the resource for version 1_1 of the configuration dtd */
063: private static final String DTD_RESOURCE_NAME_1_1 = "com/puppycrawl/tools/checkstyle/configuration_1_1.dtd";
064:
065: /** the public ID for version 1_2 of the configuration dtd */
066: private static final String DTD_PUBLIC_ID_1_2 = "-//Puppy Crawl//DTD Check Configuration 1.2//EN";
067:
068: /** the resource for version 1_1 of the configuration dtd */
069: private static final String DTD_RESOURCE_NAME_1_2 = "com/puppycrawl/tools/checkstyle/configuration_1_2.dtd";
070:
071: /** constant to specify two kilobyte of data */
072: private static final int TWO_KB = 2048;
073:
074: /**
075: * Implements the SAX document handler interfaces, so they do not
076: * appear in the public API of the ConfigurationLoader.
077: */
078: private final class InternalLoader extends AbstractLoader {
079: /** module elements */
080: private static final String MODULE = "module";
081: /** name attribute */
082: private static final String NAME = "name";
083: /** property element */
084: private static final String PROPERTY = "property";
085: /** value attribute */
086: private static final String VALUE = "value";
087: /** default attribute */
088: private static final String DEFAULT = "default";
089: /** name of the severity property */
090: private static final String SEVERITY = "severity";
091:
092: /**
093: * Creates a new InternalLoader.
094: * @throws SAXException if an error occurs
095: * @throws ParserConfigurationException if an error occurs
096: */
097: private InternalLoader() throws SAXException,
098: ParserConfigurationException {
099: // super(DTD_PUBLIC_ID_1_1, DTD_RESOURCE_NAME_1_1);
100: super (createIdToResourceNameMap());
101: }
102:
103: /** {@inheritDoc} **/
104: public void startElement(String aNamespaceURI,
105: String aLocalName, String aQName, Attributes aAtts)
106: throws SAXException {
107: // TODO: debug logging for support puposes
108: if (aQName.equals(MODULE)) {
109: //create configuration
110: final String name = aAtts.getValue(NAME);
111: final DefaultConfiguration conf = new DefaultConfiguration(
112: name);
113:
114: if (mConfiguration == null) {
115: mConfiguration = conf;
116: }
117:
118: //add configuration to it's parent
119: if (!mConfigStack.isEmpty()) {
120: final DefaultConfiguration top = (DefaultConfiguration) mConfigStack
121: .peek();
122: top.addChild(conf);
123: }
124:
125: mConfigStack.push(conf);
126: } else if (aQName.equals(PROPERTY)) {
127: //extract name and value
128: final String name = aAtts.getValue(NAME);
129: final String value;
130: try {
131: value = replaceProperties(aAtts.getValue(VALUE),
132: mOverridePropsResolver, aAtts
133: .getValue(DEFAULT));
134: } catch (final CheckstyleException ex) {
135: throw new SAXException(ex.getMessage());
136: }
137:
138: //add to attributes of configuration
139: final DefaultConfiguration top = (DefaultConfiguration) mConfigStack
140: .peek();
141: top.addAttribute(name, value);
142: }
143: }
144:
145: /** {@inheritDoc} **/
146: public void endElement(String aNamespaceURI, String aLocalName,
147: String aQName) throws SAXException {
148: if (aQName.equals(MODULE)) {
149:
150: final Configuration recentModule = (Configuration) mConfigStack
151: .pop();
152:
153: // remove modules with severity ignore if these modules should
154: // be omitted
155: SeverityLevel level = null;
156: try {
157: final String severity = recentModule
158: .getAttribute(SEVERITY);
159: level = SeverityLevel.getInstance(severity);
160: } catch (final CheckstyleException e) {
161: //severity not set -> ignore
162: ;
163: }
164:
165: // omit this module if these should be omitted and the module
166: // has the severity 'ignore'
167: final boolean omitModule = mOmitIgnoredModules
168: && SeverityLevel.IGNORE.equals(level);
169:
170: if (omitModule && !mConfigStack.isEmpty()) {
171: final DefaultConfiguration parentModule = (DefaultConfiguration) mConfigStack
172: .peek();
173: parentModule.removeChild(recentModule);
174: }
175: }
176: }
177:
178: }
179:
180: /** the SAX document handler */
181: private InternalLoader mSaxHandler;
182:
183: /** property resolver **/
184: private final PropertyResolver mOverridePropsResolver;
185: /** the loaded configurations **/
186: private final Stack mConfigStack = new Stack();
187: /** the Configuration that is being built */
188: private Configuration mConfiguration;
189:
190: /** flags if modules with the severity 'ignore' should be omitted. */
191: private boolean mOmitIgnoredModules;
192:
193: /**
194: * Creates mapping between local resources and dtd ids.
195: * @return map between local resources and dtd ids.
196: */
197: private static Map createIdToResourceNameMap() {
198: final Map map = new HashMap();
199: map.put(DTD_PUBLIC_ID_1_0, DTD_RESOURCE_NAME_1_0);
200: map.put(DTD_PUBLIC_ID_1_1, DTD_RESOURCE_NAME_1_1);
201: map.put(DTD_PUBLIC_ID_1_2, DTD_RESOURCE_NAME_1_2);
202: return map;
203: }
204:
205: /**
206: * Creates a new <code>ConfigurationLoader</code> instance.
207: * @param aOverrideProps resolver for overriding properties
208: * @param aOmitIgnoredModules <code>true</code> if ignored modules should be
209: * omitted
210: * @throws ParserConfigurationException if an error occurs
211: * @throws SAXException if an error occurs
212: */
213: private ConfigurationLoader(final PropertyResolver aOverrideProps,
214: final boolean aOmitIgnoredModules)
215: throws ParserConfigurationException, SAXException {
216: mSaxHandler = new InternalLoader();
217: mOverridePropsResolver = aOverrideProps;
218: mOmitIgnoredModules = aOmitIgnoredModules;
219: }
220:
221: /**
222: * Parses the specified stream loading the configuration information.
223: * The stream is NOT closed after parsing, it is the responsibility of
224: * the caller to close the stream.
225: *
226: * @param aStream the stream that contains the configuration data
227: * @throws IOException if an error occurs
228: * @throws SAXException if an error occurs
229: */
230: private void parseInputStream(InputStream aStream)
231: throws IOException, SAXException {
232: final InputStream configStream = new BufferedInputStream(
233: aStream, TWO_KB);
234: final InputSource inputSource = new InputSource(configStream);
235: mSaxHandler.parseInputSource(inputSource);
236: }
237:
238: /**
239: * Returns the module configurations in a specified file.
240: * @param aConfig location of config file, can be either a URL or a filename
241: * @param aOverridePropsResolver overriding properties
242: * @return the check configurations
243: * @throws CheckstyleException if an error occurs
244: */
245: public static Configuration loadConfiguration(String aConfig,
246: PropertyResolver aOverridePropsResolver)
247: throws CheckstyleException {
248: return loadConfiguration(aConfig, aOverridePropsResolver, false);
249: }
250:
251: /**
252: * Returns the module configurations in a specified file.
253: *
254: * @param aConfig location of config file, can be either a URL or a filename
255: * @param aOverridePropsResolver overriding properties
256: * @param aOmitIgnoredModules <code>true</code> if modules with severity
257: * 'ignore' should be omitted, <code>false</code> otherwise
258: * @return the check configurations
259: * @throws CheckstyleException if an error occurs
260: */
261: public static Configuration loadConfiguration(String aConfig,
262: PropertyResolver aOverridePropsResolver,
263: boolean aOmitIgnoredModules) throws CheckstyleException {
264: InputStream bufferedStream = null;
265: try {
266: // figure out if this is a File or a URL
267: InputStream configStream;
268: try {
269: final URL url = new URL(aConfig);
270: configStream = url.openStream();
271: } catch (final MalformedURLException ex) {
272: configStream = new FileInputStream(aConfig);
273: }
274: bufferedStream = new BufferedInputStream(configStream);
275:
276: return loadConfiguration(bufferedStream,
277: aOverridePropsResolver, aOmitIgnoredModules);
278: } catch (final FileNotFoundException e) {
279: throw new CheckstyleException("unable to find " + aConfig,
280: e);
281: } catch (final IOException e) {
282: throw new CheckstyleException("unable to read " + aConfig,
283: e);
284: } catch (final CheckstyleException e) {
285: //wrap again to add file name info
286: throw new CheckstyleException("unable to read " + aConfig
287: + " - " + e.getMessage(), e);
288: } finally {
289: if (bufferedStream != null) {
290: try {
291: bufferedStream.close();
292: } catch (final IOException e) {
293: // cannot throw another exception.
294: ;
295: }
296: }
297: }
298: }
299:
300: /**
301: * Returns the module configurations from a specified input stream.
302: * Note that clients are required to close the given stream by themselves
303: *
304: * @param aConfigStream the input stream to the Checkstyle configuration
305: * @param aOverridePropsResolver overriding properties
306: * @param aOmitIgnoredModules <code>true</code> if modules with severity
307: * 'ignore' should be omitted, <code>false</code> otherwise
308: * @return the check configurations
309: * @throws CheckstyleException if an error occurs
310: */
311: public static Configuration loadConfiguration(
312: InputStream aConfigStream,
313: PropertyResolver aOverridePropsResolver,
314: boolean aOmitIgnoredModules) throws CheckstyleException {
315: try {
316: final ConfigurationLoader loader = new ConfigurationLoader(
317: aOverridePropsResolver, aOmitIgnoredModules);
318: loader.parseInputStream(aConfigStream);
319: return loader.getConfiguration();
320: } catch (final ParserConfigurationException e) {
321: throw new CheckstyleException(
322: "unable to parse configuration stream", e);
323: } catch (final SAXParseException e) {
324: throw new CheckstyleException(
325: "unable to parse configuration stream" + " - "
326: + e.getMessage() + ":" + e.getLineNumber()
327: + ":" + e.getColumnNumber(), e);
328: } catch (final SAXException e) {
329: throw new CheckstyleException(
330: "unable to parse configuration stream" + " - "
331: + e.getMessage(), e);
332: } catch (final IOException e) {
333: throw new CheckstyleException("unable to read from stream",
334: e);
335: }
336: }
337:
338: /**
339: * Returns the configuration in the last file parsed.
340: * @return Configuration object
341: */
342: private Configuration getConfiguration() {
343: return mConfiguration;
344: }
345:
346: /**
347: * Replaces <code>${xxx}</code> style constructions in the given value
348: * with the string value of the corresponding data types.
349: *
350: * The method is package visible to facilitate testing.
351: *
352: * @param aValue The string to be scanned for property references.
353: * May be <code>null</code>, in which case this
354: * method returns immediately with no effect.
355: * @param aProps Mapping (String to String) of property names to their
356: * values. Must not be <code>null</code>.
357: * @param aDefaultValue default to use if one of the properties in aValue
358: * cannot be resolved from aProps.
359: *
360: * @throws CheckstyleException if the string contains an opening
361: * <code>${</code> without a closing
362: * <code>}</code>
363: * @return the original string with the properties replaced, or
364: * <code>null</code> if the original string is <code>null</code>.
365: *
366: * Code copied from ant -
367: * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java
368: */
369: // Package visible for testing purposes
370: static String replaceProperties(String aValue,
371: PropertyResolver aProps, String aDefaultValue)
372: throws CheckstyleException {
373: if (aValue == null) {
374: return null;
375: }
376:
377: final List fragments = new ArrayList();
378: final List propertyRefs = new ArrayList();
379: parsePropertyString(aValue, fragments, propertyRefs);
380:
381: final StringBuffer sb = new StringBuffer();
382: final Iterator i = fragments.iterator();
383: final Iterator j = propertyRefs.iterator();
384: while (i.hasNext()) {
385: String fragment = (String) i.next();
386: if (fragment == null) {
387: final String propertyName = (String) j.next();
388: fragment = aProps.resolve(propertyName);
389: if (fragment == null) {
390: if (aDefaultValue != null) {
391: return aDefaultValue;
392: }
393: throw new CheckstyleException("Property ${"
394: + propertyName + "} has not been set");
395: }
396: }
397: sb.append(fragment);
398: }
399:
400: return sb.toString();
401: }
402:
403: /**
404: * Parses a string containing <code>${xxx}</code> style property
405: * references into two lists. The first list is a collection
406: * of text fragments, while the other is a set of string property names.
407: * <code>null</code> entries in the first list indicate a property
408: * reference from the second list.
409: *
410: * @param aValue Text to parse. Must not be <code>null</code>.
411: * @param aFragments List to add text fragments to.
412: * Must not be <code>null</code>.
413: * @param aPropertyRefs List to add property names to.
414: * Must not be <code>null</code>.
415: *
416: * @throws CheckstyleException if the string contains an opening
417: * <code>${</code> without a closing
418: * <code>}</code>
419: * Code copied from ant -
420: * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java
421: */
422: private static void parsePropertyString(String aValue,
423: List aFragments, List aPropertyRefs)
424: throws CheckstyleException {
425: int prev = 0;
426: int pos;
427: //search for the next instance of $ from the 'prev' position
428: while ((pos = aValue.indexOf("$", prev)) >= 0) {
429:
430: //if there was any text before this, add it as a fragment
431: //TODO, this check could be modified to go if pos>prev;
432: //seems like this current version could stick empty strings
433: //into the list
434: if (pos > 0) {
435: aFragments.add(aValue.substring(prev, pos));
436: }
437: //if we are at the end of the string, we tack on a $
438: //then move past it
439: if (pos == (aValue.length() - 1)) {
440: aFragments.add("$");
441: prev = pos + 1;
442: } else if (aValue.charAt(pos + 1) != '{') {
443: //peek ahead to see if the next char is a property or not
444: //not a property: insert the char as a literal
445: /*
446: fragments.addElement(value.substring(pos + 1, pos + 2));
447: prev = pos + 2;
448: */
449: if (aValue.charAt(pos + 1) == '$') {
450: //backwards compatibility two $ map to one mode
451: aFragments.add("$");
452: prev = pos + 2;
453: } else {
454: //new behaviour: $X maps to $X for all values of X!='$'
455: aFragments.add(aValue.substring(pos, pos + 2));
456: prev = pos + 2;
457: }
458:
459: } else {
460: //property found, extract its name or bail on a typo
461: final int endName = aValue.indexOf('}', pos);
462: if (endName < 0) {
463: throw new CheckstyleException(
464: "Syntax error in property: " + aValue);
465: }
466: final String propertyName = aValue.substring(pos + 2,
467: endName);
468: aFragments.add(null);
469: aPropertyRefs.add(propertyName);
470: prev = endName + 1;
471: }
472: }
473: //no more $ signs found
474: //if there is any tail to the file, append it
475: if (prev < aValue.length()) {
476: aFragments.add(aValue.substring(prev));
477: }
478: }
479: }
|