View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.pmd.exec;
20  
21  import java.io.File;
22  import java.io.FileInputStream;
23  import java.io.FileOutputStream;
24  import java.io.IOException;
25  import java.io.ObjectInputStream;
26  import java.io.ObjectOutputStream;
27  import java.io.OutputStreamWriter;
28  import java.io.Writer;
29  import java.util.ArrayList;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Objects;
33  
34  import net.sourceforge.pmd.cpd.CPD;
35  import net.sourceforge.pmd.cpd.CPDConfiguration;
36  import net.sourceforge.pmd.cpd.CSVRenderer;
37  import net.sourceforge.pmd.cpd.EcmascriptLanguage;
38  import net.sourceforge.pmd.cpd.JSPLanguage;
39  import net.sourceforge.pmd.cpd.JavaLanguage;
40  import net.sourceforge.pmd.cpd.Language;
41  import net.sourceforge.pmd.cpd.LanguageFactory;
42  import net.sourceforge.pmd.cpd.Match;
43  import net.sourceforge.pmd.cpd.SimpleRenderer;
44  import net.sourceforge.pmd.cpd.XMLRenderer;
45  import net.sourceforge.pmd.cpd.renderer.CPDRenderer;
46  import org.apache.maven.plugin.MojoExecutionException;
47  import org.apache.maven.plugins.pmd.ExcludeDuplicationsFromFile;
48  import org.apache.maven.reporting.MavenReportException;
49  import org.codehaus.plexus.util.FileUtils;
50  import org.slf4j.Logger;
51  import org.slf4j.LoggerFactory;
52  
53  /**
54   * Executes CPD with the configuration provided via {@link CpdRequest}.
55   */
56  public class CpdExecutor extends Executor {
57      private static final Logger LOG = LoggerFactory.getLogger(CpdExecutor.class);
58  
59      public static CpdResult execute(CpdRequest request) throws MavenReportException {
60          if (request.getJavaExecutable() != null) {
61              return fork(request);
62          }
63  
64          ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
65          try {
66              Thread.currentThread().setContextClassLoader(CpdExecutor.class.getClassLoader());
67              CpdExecutor cpdExecutor = new CpdExecutor(request);
68              return cpdExecutor.run();
69          } finally {
70              Thread.currentThread().setContextClassLoader(origLoader);
71          }
72      }
73  
74      private static CpdResult fork(CpdRequest request) throws MavenReportException {
75          File basePmdDir = new File(request.getTargetDirectory(), "pmd");
76          basePmdDir.mkdirs();
77          File cpdRequestFile = new File(basePmdDir, "cpdrequest.bin");
78          try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(cpdRequestFile))) {
79              out.writeObject(request);
80          } catch (IOException e) {
81              throw new MavenReportException(e.getMessage(), e);
82          }
83  
84          String classpath = buildClasspath();
85          ProcessBuilder pb = new ProcessBuilder();
86          // note: using env variable instead of -cp cli arg to avoid length limitations under Windows
87          pb.environment().put("CLASSPATH", classpath);
88          pb.command().add(request.getJavaExecutable());
89          pb.command().add(CpdExecutor.class.getName());
90          pb.command().add(cpdRequestFile.getAbsolutePath());
91  
92          LOG.debug("Executing: CLASSPATH={}, command={}", classpath, pb.command());
93          try {
94              final Process p = pb.start();
95              // Note: can't use pb.inheritIO(), since System.out/System.err has been modified after process start
96              // and inheritIO would only inherit file handles, not the changed streams.
97              ProcessStreamHandler.start(p.getInputStream(), System.out);
98              ProcessStreamHandler.start(p.getErrorStream(), System.err);
99              int exit = p.waitFor();
100             LOG.debug("CpdExecutor exit code: {}", exit);
101             if (exit != 0) {
102                 throw new MavenReportException("CpdExecutor exited with exit code " + exit);
103             }
104             return new CpdResult(new File(request.getTargetDirectory(), "cpd.xml"), request.getOutputEncoding());
105         } catch (IOException e) {
106             throw new MavenReportException(e.getMessage(), e);
107         } catch (InterruptedException e) {
108             Thread.currentThread().interrupt();
109             throw new MavenReportException(e.getMessage(), e);
110         }
111     }
112 
113     /**
114      * Execute CPD analysis from CLI.
115      *
116      * <p>
117      * Single arg with the filename to the serialized {@link CpdRequest}.
118      *
119      * <p>
120      * Exit-code: 0 = success, 1 = failure in executing
121      *
122      * @param args
123      */
124     public static void main(String[] args) {
125         File requestFile = new File(args[0]);
126         try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(requestFile))) {
127             CpdRequest request = (CpdRequest) in.readObject();
128             CpdExecutor cpdExecutor = new CpdExecutor(request);
129             cpdExecutor.setupLogLevel(request.getLogLevel());
130             cpdExecutor.run();
131             System.exit(0);
132         } catch (IOException | ClassNotFoundException | MavenReportException e) {
133             LOG.error(e.getMessage(), e);
134         }
135         System.exit(1);
136     }
137 
138     private final CpdRequest request;
139 
140     /** Helper to exclude duplications from the result. */
141     private final ExcludeDuplicationsFromFile excludeDuplicationsFromFile = new ExcludeDuplicationsFromFile();
142 
143     public CpdExecutor(CpdRequest request) {
144         this.request = Objects.requireNonNull(request);
145     }
146 
147     private CpdResult run() throws MavenReportException {
148         setupPmdLogging(request.isShowPmdLog(), request.getLogLevel());
149 
150         try {
151             excludeDuplicationsFromFile.loadExcludeFromFailuresData(request.getExcludeFromFailureFile());
152         } catch (MojoExecutionException e) {
153             throw new MavenReportException("Error loading exclusions", e);
154         }
155 
156         CPDConfiguration cpdConfiguration = new CPDConfiguration();
157         cpdConfiguration.setMinimumTileSize(request.getMinimumTokens());
158 
159         Language cpdLanguage;
160         if ("java".equals(request.getLanguage()) || null == request.getLanguage()) {
161             cpdLanguage = new JavaLanguage(request.getLanguageProperties());
162         } else if ("javascript".equals(request.getLanguage())) {
163             cpdLanguage = new EcmascriptLanguage();
164         } else if ("jsp".equals(request.getLanguage())) {
165             cpdLanguage = new JSPLanguage();
166         } else {
167             cpdLanguage = LanguageFactory.createLanguage(request.getLanguage(), request.getLanguageProperties());
168         }
169 
170         cpdConfiguration.setLanguage(cpdLanguage);
171         cpdConfiguration.setSourceEncoding(request.getSourceEncoding());
172 
173         CPD cpd = new CPD(cpdConfiguration);
174         try {
175             cpd.add(request.getFiles());
176         } catch (IOException e) {
177             throw new MavenReportException(e.getMessage(), e);
178         }
179 
180         LOG.debug("Executing CPD...");
181         cpd.go();
182         LOG.debug("CPD finished.");
183 
184         // always create XML format. we need to output it even if the file list is empty or we have no duplications
185         // so the "check" goals can check for violations
186         writeXmlReport(cpd);
187 
188         // html format is handled by maven site report, xml format has already been rendered
189         String format = request.getFormat();
190         if (!"html".equals(format) && !"xml".equals(format)) {
191             writeFormattedReport(cpd);
192         }
193 
194         return new CpdResult(new File(request.getTargetDirectory(), "cpd.xml"), request.getOutputEncoding());
195     }
196 
197     private void writeXmlReport(CPD cpd) throws MavenReportException {
198         File targetFile = writeReport(cpd, new XMLRenderer(request.getOutputEncoding()), "xml");
199         if (request.isIncludeXmlInSite()) {
200             File siteDir = new File(request.getReportOutputDirectory());
201             siteDir.mkdirs();
202             try {
203                 FileUtils.copyFile(targetFile, new File(siteDir, "cpd.xml"));
204             } catch (IOException e) {
205                 throw new MavenReportException(e.getMessage(), e);
206             }
207         }
208     }
209 
210     private File writeReport(CPD cpd, CPDRenderer r, String extension) throws MavenReportException {
211         if (r == null) {
212             return null;
213         }
214 
215         File targetDir = new File(request.getTargetDirectory());
216         targetDir.mkdirs();
217         File targetFile = new File(targetDir, "cpd." + extension);
218         try (Writer writer = new OutputStreamWriter(new FileOutputStream(targetFile), request.getOutputEncoding())) {
219             r.render(filterMatches(cpd.getMatches()), writer);
220             writer.flush();
221         } catch (IOException ioe) {
222             throw new MavenReportException(ioe.getMessage(), ioe);
223         }
224         return targetFile;
225     }
226 
227     private void writeFormattedReport(CPD cpd) throws MavenReportException {
228         CPDRenderer r = createRenderer(request.getFormat(), request.getOutputEncoding());
229         writeReport(cpd, r, request.getFormat());
230     }
231 
232     /**
233      * Create and return the correct renderer for the output type.
234      *
235      * @return the renderer based on the configured output
236      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
237      */
238     public static CPDRenderer createRenderer(String format, String outputEncoding) throws MavenReportException {
239         CPDRenderer renderer = null;
240         if ("xml".equals(format)) {
241             renderer = new XMLRenderer(outputEncoding);
242         } else if ("csv".equals(format)) {
243             renderer = new CSVRenderer();
244         } else if ("txt".equals(format)) {
245             renderer = new SimpleRenderer();
246         } else if (!"".equals(format) && !"none".equals(format)) {
247             try {
248                 renderer = (CPDRenderer) Class.forName(format).getConstructor().newInstance();
249             } catch (Exception e) {
250                 throw new MavenReportException(
251                         "Can't find CPD custom format " + format + ": "
252                                 + e.getClass().getName(),
253                         e);
254             }
255         }
256 
257         return renderer;
258     }
259 
260     private Iterator<Match> filterMatches(Iterator<Match> matches) {
261         LOG.debug("Filtering duplications. Using " + excludeDuplicationsFromFile.countExclusions()
262                 + " configured exclusions.");
263 
264         List<Match> filteredMatches = new ArrayList<>();
265         int excludedDuplications = 0;
266         while (matches.hasNext()) {
267             Match match = matches.next();
268             if (excludeDuplicationsFromFile.isExcludedFromFailure(match)) {
269                 excludedDuplications++;
270             } else {
271                 filteredMatches.add(match);
272             }
273         }
274 
275         LOG.debug("Excluded " + excludedDuplications + " duplications.");
276         return filteredMatches.iterator();
277     }
278 }