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.File;
022:
023: import java.util.ArrayList;
024: import java.util.Iterator;
025: import java.util.Locale;
026: import java.util.Stack;
027: import java.util.StringTokenizer;
028:
029: import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
030: import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
031: import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
032: import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
033: import com.puppycrawl.tools.checkstyle.api.Context;
034: import com.puppycrawl.tools.checkstyle.api.FilterSet;
035: import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
036: import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
037: import com.puppycrawl.tools.checkstyle.api.Configuration;
038: import com.puppycrawl.tools.checkstyle.api.FileSetCheck;
039: import com.puppycrawl.tools.checkstyle.api.Filter;
040: import com.puppycrawl.tools.checkstyle.api.AuditListener;
041: import com.puppycrawl.tools.checkstyle.api.Utils;
042: import com.puppycrawl.tools.checkstyle.api.AuditEvent;
043:
044: /**
045: * This class provides the functionality to check a set of files.
046: * @author Oliver Burn
047: * @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a>
048: * @author lkuehne
049: */
050: public class Checker extends AutomaticBean implements MessageDispatcher {
051: /** maintains error count */
052: private final SeverityLevelCounter mCounter = new SeverityLevelCounter(
053: SeverityLevel.ERROR);
054:
055: /** vector of listeners */
056: private final ArrayList mListeners = new ArrayList();
057:
058: /** vector of fileset checks */
059: private final ArrayList mFileSetChecks = new ArrayList();
060:
061: /** class loader to resolve classes with. **/
062: private ClassLoader mLoader = Thread.currentThread()
063: .getContextClassLoader();
064:
065: /** the basedir to strip off in filenames */
066: private String mBasedir;
067:
068: /** locale country to report messages **/
069: private String mLocaleCountry = Locale.getDefault().getCountry();
070: /** locale language to report messages **/
071: private String mLocaleLanguage = Locale.getDefault().getLanguage();
072:
073: /** The factory for instantiating submodules */
074: private ModuleFactory mModuleFactory;
075:
076: /** the context of all child components */
077: private Context mChildContext;
078:
079: /** The audit event filters */
080: private final FilterSet mFilters = new FilterSet();
081:
082: /**
083: * The severity level of any violations found by submodules.
084: * The value of this property is passed to submodules via
085: * contextualize().
086: *
087: * Note: Since the Checker is merely a container for modules
088: * it does not make sense to implement logging functionality
089: * here. Consequently Checker does not extend AbstractViolationReporter,
090: * leading to a bit of duplicated code for severity level setting.
091: */
092: private SeverityLevel mSeverityLevel = SeverityLevel.ERROR;
093:
094: /**
095: * Creates a new <code>Checker</code> instance.
096: * The instance needs to be contextualized and configured.
097: *
098: * @throws CheckstyleException if an error occurs
099: */
100: public Checker() throws CheckstyleException {
101: addListener(mCounter);
102: }
103:
104: /** {@inheritDoc} */
105: public void finishLocalSetup() throws CheckstyleException {
106: final Locale locale = new Locale(mLocaleLanguage,
107: mLocaleCountry);
108: LocalizedMessage.setLocale(locale);
109:
110: if (mModuleFactory == null) {
111: mModuleFactory = PackageNamesLoader
112: .loadModuleFactory(Thread.currentThread()
113: .getContextClassLoader());
114: }
115:
116: final DefaultContext context = new DefaultContext();
117: context.add("classLoader", mLoader);
118: context.add("moduleFactory", mModuleFactory);
119: context.add("severity", mSeverityLevel.getName());
120: context.add("basedir", mBasedir);
121: mChildContext = context;
122: }
123:
124: /**
125: * Instantiates, configures and registers a child AbstractFilter
126: * or FileSetCheck
127: * that is specified in the provided configuration.
128: * @param aChildConf {@inheritDoc}
129: * @throws CheckstyleException {@inheritDoc}
130: * @see com.puppycrawl.tools.checkstyle.api.AutomaticBean
131: */
132: protected void setupChild(Configuration aChildConf)
133: throws CheckstyleException {
134: final String name = aChildConf.getName();
135: try {
136: final Object child = mModuleFactory.createModule(name);
137: if (child instanceof AutomaticBean) {
138: final AutomaticBean bean = (AutomaticBean) child;
139: bean.contextualize(mChildContext);
140: bean.configure(aChildConf);
141: }
142: if (child instanceof FileSetCheck) {
143: final FileSetCheck fsc = (FileSetCheck) child;
144: addFileSetCheck(fsc);
145: } else if (child instanceof Filter) {
146: final Filter filter = (Filter) child;
147: addFilter(filter);
148: } else if (child instanceof AuditListener) {
149: final AuditListener listener = (AuditListener) child;
150: addListener(listener);
151: } else {
152: throw new CheckstyleException(name
153: + " is not allowed as a child in Checker");
154: }
155: } catch (final Exception ex) {
156: // TODO i18n
157: throw new CheckstyleException("cannot initialize module "
158: + name + " - " + ex.getMessage(), ex);
159: }
160: }
161:
162: /**
163: * Adds a FileSetCheck to the list of FileSetChecks
164: * that is executed in process().
165: * @param aFileSetCheck the additional FileSetCheck
166: */
167: public void addFileSetCheck(FileSetCheck aFileSetCheck) {
168: aFileSetCheck.setMessageDispatcher(this );
169: mFileSetChecks.add(aFileSetCheck);
170: }
171:
172: /**
173: * Adds a filter to the end of the audit event filter chain.
174: * @param aFilter the additional filter
175: */
176: public void addFilter(Filter aFilter) {
177: mFilters.addFilter(aFilter);
178: }
179:
180: /**
181: * Removes filter.
182: * @param aFilter filter to remove.
183: */
184: public void removeFilter(Filter aFilter) {
185: mFilters.removeFilter(aFilter);
186: }
187:
188: /** Cleans up the object. **/
189: public void destroy() {
190: mListeners.clear();
191: mFilters.clear();
192: }
193:
194: /**
195: * Add the listener that will be used to receive events from the audit.
196: * @param aListener the nosy thing
197: */
198: public void addListener(AuditListener aListener) {
199: mListeners.add(aListener);
200: }
201:
202: /**
203: * Removes a given listener.
204: * @param aListener a listener to remove
205: */
206: public void removeListener(AuditListener aListener) {
207: mListeners.remove(aListener);
208: }
209:
210: /**
211: * Processes a set of files with all FileSetChecks.
212: * Once this is done, it is highly recommended to call for
213: * the destroy method to close and remove the listeners.
214: * @param aFiles the list of files to be audited.
215: * @return the total number of errors found
216: * @see #destroy()
217: */
218: public int process(File[] aFiles) {
219: fireAuditStarted();
220: for (int i = 0; i < mFileSetChecks.size(); i++) {
221: final FileSetCheck fileSetCheck = (FileSetCheck) mFileSetChecks
222: .get(i);
223: fileSetCheck.process(aFiles);
224: fileSetCheck.destroy();
225: }
226: final int errorCount = mCounter.getCount();
227: fireAuditFinished();
228: return errorCount;
229: }
230:
231: /**
232: * Create a stripped down version of a filename.
233: * @param aFileName the original filename
234: * @return the filename where an initial prefix of basedir is stripped
235: */
236: private String getStrippedFileName(final String aFileName) {
237: return Utils.getStrippedFileName(mBasedir, aFileName);
238: }
239:
240: /** @param aBasedir the base directory to strip off in filenames */
241: public void setBasedir(String aBasedir) {
242: // we use getAbsolutePath() instead of getCanonicalPath()
243: // because normalize() removes all . and .. so path
244: // will be canonical by default.
245: mBasedir = normalize(aBasedir);
246: }
247:
248: /**
249: * "normalize" the given absolute path.
250: *
251: * <p>This includes:
252: * <ul>
253: * <li>Uppercase the drive letter if there is one.</li>
254: * <li>Remove redundant slashes after the drive spec.</li>
255: * <li>resolve all ./, .\, ../ and ..\ sequences.</li>
256: * <li>DOS style paths that start with a drive letter will have
257: * \ as the separator.</li>
258: * </ul>
259: *
260: * @param aPath a path for "normalizing"
261: * @return "normalized" file name
262: * @throws java.lang.NullPointerException if the file path is
263: * equal to null.
264: */
265: public String normalize(String aPath) {
266: final String osName = System.getProperty("os.name")
267: .toLowerCase(Locale.US);
268: final boolean onNetWare = (osName.indexOf("netware") > -1);
269:
270: final String orig = aPath;
271:
272: aPath = aPath.replace('/', File.separatorChar).replace('\\',
273: File.separatorChar);
274:
275: // make sure we are dealing with an absolute path
276: final int colon = aPath.indexOf(":");
277:
278: if (!onNetWare) {
279: if (!aPath.startsWith(File.separator)
280: && !((aPath.length() >= 2)
281: && Character.isLetter(aPath.charAt(0)) && (colon == 1))) {
282: final String msg = aPath + " is not an absolute path";
283: throw new IllegalArgumentException(msg);
284: }
285: } else {
286: if (!aPath.startsWith(File.separator) && (colon == -1)) {
287: final String msg = aPath + " is not an absolute path";
288: throw new IllegalArgumentException(msg);
289: }
290: }
291:
292: boolean dosWithDrive = false;
293: String root = null;
294: // Eliminate consecutive slashes after the drive spec
295: if ((!onNetWare && (aPath.length() >= 2)
296: && Character.isLetter(aPath.charAt(0)) && (aPath
297: .charAt(1) == ':'))
298: || (onNetWare && (colon > -1))) {
299:
300: dosWithDrive = true;
301:
302: final char[] ca = aPath.replace('/', '\\').toCharArray();
303: final StringBuffer sbRoot = new StringBuffer();
304: for (int i = 0; i < colon; i++) {
305: sbRoot.append(Character.toUpperCase(ca[i]));
306: }
307: sbRoot.append(':');
308: if (colon + 1 < aPath.length()) {
309: sbRoot.append(File.separatorChar);
310: }
311: root = sbRoot.toString();
312:
313: // Eliminate consecutive slashes after the drive spec
314: final StringBuffer sbPath = new StringBuffer();
315: for (int i = colon + 1; i < ca.length; i++) {
316: if ((ca[i] != '\\')
317: || ((ca[i] == '\\') && (ca[i - 1] != '\\'))) {
318: sbPath.append(ca[i]);
319: }
320: }
321: aPath = sbPath.toString().replace('\\', File.separatorChar);
322:
323: } else {
324: if (aPath.length() == 1) {
325: root = File.separator;
326: aPath = "";
327: } else if (aPath.charAt(1) == File.separatorChar) {
328: // UNC drive
329: root = File.separator + File.separator;
330: aPath = aPath.substring(2);
331: } else {
332: root = File.separator;
333: aPath = aPath.substring(1);
334: }
335: }
336:
337: final Stack s = new Stack();
338: s.push(root);
339: final StringTokenizer tok = new StringTokenizer(aPath,
340: File.separator);
341: while (tok.hasMoreTokens()) {
342: final String this Token = tok.nextToken();
343: if (".".equals(this Token)) {
344: continue;
345: } else if ("..".equals(this Token)) {
346: if (s.size() < 2) {
347: throw new IllegalArgumentException(
348: "Cannot resolve path " + orig);
349: }
350: s.pop();
351: } else { // plain component
352: s.push(this Token);
353: }
354: }
355:
356: final StringBuffer sb = new StringBuffer();
357: for (int i = 0; i < s.size(); i++) {
358: if (i > 1) {
359: // not before the filesystem root and not after it, since root
360: // already contains one
361: sb.append(File.separatorChar);
362: }
363: sb.append(s.elementAt(i));
364: }
365:
366: aPath = sb.toString();
367: if (dosWithDrive) {
368: aPath = aPath.replace('/', '\\');
369: }
370: return aPath;
371: }
372:
373: /** @return the base directory property used in unit-test. */
374: public final String getBasedir() {
375: return mBasedir;
376: }
377:
378: /** notify all listeners about the audit start */
379: protected void fireAuditStarted() {
380: final AuditEvent evt = new AuditEvent(this );
381: final Iterator it = mListeners.iterator();
382: while (it.hasNext()) {
383: final AuditListener listener = (AuditListener) it.next();
384: listener.auditStarted(evt);
385: }
386: }
387:
388: /** notify all listeners about the audit end */
389: protected void fireAuditFinished() {
390: final AuditEvent evt = new AuditEvent(this );
391: final Iterator it = mListeners.iterator();
392: while (it.hasNext()) {
393: final AuditListener listener = (AuditListener) it.next();
394: listener.auditFinished(evt);
395: }
396: }
397:
398: /**
399: * Notify all listeners about the beginning of a file audit.
400: *
401: * @param aFileName
402: * the file to be audited
403: */
404: public void fireFileStarted(String aFileName) {
405: final String stripped = getStrippedFileName(aFileName);
406: final AuditEvent evt = new AuditEvent(this , stripped);
407: final Iterator it = mListeners.iterator();
408: while (it.hasNext()) {
409: final AuditListener listener = (AuditListener) it.next();
410: listener.fileStarted(evt);
411: }
412: }
413:
414: /**
415: * Notify all listeners about the end of a file audit.
416: *
417: * @param aFileName
418: * the audited file
419: */
420: public void fireFileFinished(String aFileName) {
421: final String stripped = getStrippedFileName(aFileName);
422: final AuditEvent evt = new AuditEvent(this , stripped);
423: final Iterator it = mListeners.iterator();
424: while (it.hasNext()) {
425: final AuditListener listener = (AuditListener) it.next();
426: listener.fileFinished(evt);
427: }
428: }
429:
430: /**
431: * notify all listeners about the errors in a file.
432: *
433: * @param aFileName
434: * the audited file
435: * @param aErrors
436: * the audit errors from the file
437: */
438: public void fireErrors(String aFileName, LocalizedMessage[] aErrors) {
439: final String stripped = getStrippedFileName(aFileName);
440: for (int i = 0; i < aErrors.length; i++) {
441: final AuditEvent evt = new AuditEvent(this , stripped,
442: aErrors[i]);
443: if (mFilters.accept(evt)) {
444: final Iterator it = mListeners.iterator();
445: while (it.hasNext()) {
446: final AuditListener listener = (AuditListener) it
447: .next();
448: listener.addError(evt);
449: }
450: }
451: }
452: }
453:
454: /**
455: * Sets the factory for creating submodules.
456: *
457: * @param aModuleFactory the factory for creating FileSetChecks
458: */
459: public void setModuleFactory(ModuleFactory aModuleFactory) {
460: mModuleFactory = aModuleFactory;
461: }
462:
463: /** @param aLocaleCountry the country to report messages **/
464: public void setLocaleCountry(String aLocaleCountry) {
465: mLocaleCountry = aLocaleCountry;
466: }
467:
468: /** @param aLocaleLanguage the language to report messages **/
469: public void setLocaleLanguage(String aLocaleLanguage) {
470: mLocaleLanguage = aLocaleLanguage;
471: }
472:
473: /**
474: * Sets the severity level. The string should be one of the names
475: * defined in the <code>SeverityLevel</code> class.
476: *
477: * @param aSeverity The new severity level
478: * @see SeverityLevel
479: */
480: public final void setSeverity(String aSeverity) {
481: mSeverityLevel = SeverityLevel.getInstance(aSeverity);
482: }
483:
484: /**
485: * Sets the classloader that is used to contextualize filesetchecks.
486: * Some Check implementations will use that classloader to improve the
487: * quality of their reports, e.g. to load a class and then analyze it via
488: * reflection.
489: * @param aLoader the new classloader
490: */
491: public final void setClassloader(ClassLoader aLoader) {
492: mLoader = aLoader;
493: }
494: }
|