001    /*
002     * Sonar, open source software quality management tool.
003     * Copyright (C) 2009 SonarSource SA
004     * mailto:contact AT sonarsource DOT com
005     *
006     * Sonar is free software; you can redistribute it and/or
007     * modify it under the terms of the GNU Lesser General Public
008     * License as published by the Free Software Foundation; either
009     * version 3 of the License, or (at your option) any later version.
010     *
011     * Sonar is distributed in the hope that it will be useful,
012     * but WITHOUT ANY WARRANTY; without even the implied warranty of
013     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014     * Lesser General Public License for more details.
015     *
016     * You should have received a copy of the GNU Lesser General Public
017     * License along with Sonar; if not, write to the Free Software
018     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
019     */
020    package org.sonar.squid.ast;
021    
022    import java.io.ByteArrayInputStream;
023    import java.io.File;
024    import java.io.IOException;
025    import java.io.InputStream;
026    import java.io.OutputStream;
027    import java.nio.charset.Charset;
028    import java.util.ArrayList;
029    import java.util.Collection;
030    import java.util.List;
031    import java.util.Stack;
032    
033    import org.apache.commons.io.IOUtils;
034    import org.slf4j.Logger;
035    import org.slf4j.LoggerFactory;
036    import org.sonar.squid.Squid;
037    import org.sonar.squid.api.AnalysisException;
038    import org.sonar.squid.api.CodeScanner;
039    import org.sonar.squid.api.CodeVisitor;
040    import org.sonar.squid.api.SourceCode;
041    import org.sonar.squid.api.SquidConfiguration;
042    import org.sonar.squid.ast.visitor.AccessorVisitor;
043    import org.sonar.squid.ast.visitor.AstVisitor;
044    import org.sonar.squid.ast.visitor.BlankLinesVisitor;
045    import org.sonar.squid.ast.visitor.BranchVisitor;
046    import org.sonar.squid.ast.visitor.ClassVisitor;
047    import org.sonar.squid.ast.visitor.CommentVisitor;
048    import org.sonar.squid.ast.visitor.ComplexityVisitor;
049    import org.sonar.squid.ast.visitor.EndAtLineVisitor;
050    import org.sonar.squid.ast.visitor.FileVisitor;
051    import org.sonar.squid.ast.visitor.LinesVisitor;
052    import org.sonar.squid.ast.visitor.MethodVisitor;
053    import org.sonar.squid.ast.visitor.LinesOfCodeVisitor;
054    import org.sonar.squid.ast.visitor.PackageVisitor;
055    import org.sonar.squid.ast.visitor.PublicApiVisitor;
056    import org.sonar.squid.ast.visitor.StatementVisitor;
057    
058    import com.puppycrawl.tools.checkstyle.Checker;
059    import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
060    import com.puppycrawl.tools.checkstyle.DefaultLogger;
061    import com.puppycrawl.tools.checkstyle.PropertiesExpander;
062    import com.puppycrawl.tools.checkstyle.api.AuditEvent;
063    import com.puppycrawl.tools.checkstyle.api.AuditListener;
064    import com.puppycrawl.tools.checkstyle.api.Configuration;
065    
066    /**
067     * Squid uses Checkstyle to get an out-of-the-box java parser with AST generation and visitor pattern support.
068     */
069    public class JavaAstScanner implements CodeScanner {
070    
071      private SquidConfiguration conf;
072      private List<AstVisitor>   visitors = new ArrayList<AstVisitor>();
073    
074      public JavaAstScanner(SquidConfiguration conf) {
075        this.conf = conf;
076      }
077    
078      /**
079       * Create and execute the Checkstyle engine.
080       * 
081       * @param files
082       *          collection of files to analyse. This list shouldn't contain and directory.
083       * @param charset
084       *          the default charset to use to read files
085       * @param confFileName
086       *          the Checkstyle configuration file name to load.
087       */
088      private void launchCheckstyleEngine(Collection<File> files, Charset charset) throws AnalysisException {
089        ErrorsListener listener = new ErrorsListener();
090        Checker c = createChecker(charset, listener);
091        File[] processedFiles = new File[files.size()];
092        files.toArray(processedFiles);
093        c.process(processedFiles);
094        c.destroy();
095        if (!listener.getErrors().isEmpty()) {
096          throw listener.getErrors().get(0);
097        }
098      }
099    
100      /**
101       * Creates the Checkstyle Checker object.
102       * 
103       * @return a nice new fresh Checkstyle Checker
104       */
105      private Checker createChecker(Charset charset, ErrorsListener errorsListener) {
106        try {
107          InputStream checkstyleConfig = JavaAstScanner.class.getClassLoader().getResourceAsStream("checkstyle-configuration.xml");
108          String readenConfig = IOUtils.toString(checkstyleConfig);
109          readenConfig = readenConfig.replace("${charset}", charset.toString());
110          checkstyleConfig = new ByteArrayInputStream(readenConfig.getBytes());
111          Configuration config = ConfigurationLoader.loadConfiguration(checkstyleConfig, new PropertiesExpander(System.getProperties()), false);
112          Checker c = new Checker();
113          c.configure(config);
114          final Logger logger = LoggerFactory.getLogger(Squid.class);
115          StreamLogger infoLogger = new StreamLogger() {
116    
117            @Override
118            public void log(String log) {
119              logger.info(log);
120            }
121          };
122          StreamLogger errorLogger = new StreamLogger() {
123    
124            @Override
125            public void log(String log) {
126              logger.error(log);
127            }
128          };
129          c.addListener(new DefaultLogger(infoLogger, true, errorLogger, true));
130          c.addListener(errorsListener);
131          return c;
132        } catch (final Exception e) {
133          throw new RuntimeException(
134              "Unable to create Checkstyle Checker object with 'checkstyle-configuration.xml' as Checkstyle configuration file name", e);
135        }
136      }
137    
138      private static class ErrorsListener implements AuditListener {
139    
140        private List<AnalysisException> errors = new ArrayList<AnalysisException>();
141    
142        public void addError(AuditEvent evt) {
143          // some projects can have file parsing errors (tapestry for example)
144          // currently do not throw an error.
145          // see
146          // http://sourceforge.net/tracker/?func=detail&atid=397078&aid=1667137&group_id=29721
147          if (evt.getMessage().contains("expecting EOF, found"))
148            return;
149          errors.add(new AnalysisException(evt.getMessage()));
150        }
151    
152        public void addException(AuditEvent evt, Throwable throwable) {
153          errors.add(new AnalysisException(evt.getMessage(), throwable));
154        }
155    
156        public void auditFinished(AuditEvent evt) {
157        }
158    
159        public void auditStarted(AuditEvent evt) {
160        }
161    
162        public void fileFinished(AuditEvent evt) {
163        }
164    
165        public void fileStarted(AuditEvent evt) {
166        }
167    
168        public List<AnalysisException> getErrors() {
169          return errors;
170        }
171      }
172      private abstract static class StreamLogger extends OutputStream {
173    
174        private StringBuilder builder = new StringBuilder(256);
175    
176        @Override
177        public void write(int byteToWrite) throws IOException {
178          char character = (char) byteToWrite;
179          if (character == '\n') {
180            logAndResetBuffer();
181          } else {
182            builder.append(character);
183          }
184        }
185    
186        private void logAndResetBuffer() {
187          log(builder.toString().trim());
188          builder.setLength(0);
189        }
190    
191        public abstract void log(String log);
192    
193        @Override
194        public void close() throws IOException {
195          if (builder.length() > 0) {
196            logAndResetBuffer();
197          }
198          super.close();
199        }
200      }
201    
202      public void scanCode(SourceCode project, Collection<File> filesToAnalyse) {
203        Stack<SourceCode> resourcesStack = new Stack<SourceCode>();
204        resourcesStack.add(project);
205        for (AstVisitor visitor : visitors) {
206          visitor.setResourcesStack(resourcesStack);
207        }
208        CheckstyleSquidBridge.setASTVisitors(visitors);
209        CheckstyleSquidBridge.setSquidConfiguration(conf);
210        launchCheckstyleEngine(filesToAnalyse, conf.getCharset());
211      }
212    
213      public void accept(CodeVisitor visitor) {
214        visitors.add((AstVisitor) visitor);
215      }
216    
217      public Collection<Class<? extends CodeVisitor>> getVisitors() {
218        List<Class<? extends CodeVisitor>> visitorClasses = new ArrayList<Class<? extends CodeVisitor>>();
219        visitorClasses.add(PackageVisitor.class);
220        visitorClasses.add(FileVisitor.class);
221        visitorClasses.add(ClassVisitor.class);
222        visitorClasses.add(MethodVisitor.class);
223        visitorClasses.add(EndAtLineVisitor.class);
224        visitorClasses.add(LinesVisitor.class);
225        visitorClasses.add(BlankLinesVisitor.class);
226        visitorClasses.add(CommentVisitor.class);
227        visitorClasses.add(PublicApiVisitor.class);
228        visitorClasses.add(BranchVisitor.class);
229        visitorClasses.add(StatementVisitor.class);
230        visitorClasses.add(ComplexityVisitor.class);
231        visitorClasses.add(LinesOfCodeVisitor.class);
232        if (conf.isAnalysePropertyAccessors()) {
233          visitorClasses.add(AccessorVisitor.class);
234        }
235        return visitorClasses;
236      }
237    }