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.nio.charset.Charset;
30  import java.util.Objects;
31  import java.util.function.Predicate;
32  
33  import net.sourceforge.pmd.cpd.CPDConfiguration;
34  import net.sourceforge.pmd.cpd.CPDReport;
35  import net.sourceforge.pmd.cpd.CPDReportRenderer;
36  import net.sourceforge.pmd.cpd.CSVRenderer;
37  import net.sourceforge.pmd.cpd.CpdAnalysis;
38  import net.sourceforge.pmd.cpd.Match;
39  import net.sourceforge.pmd.cpd.SimpleRenderer;
40  import net.sourceforge.pmd.cpd.XMLRenderer;
41  import net.sourceforge.pmd.lang.Language;
42  import org.apache.maven.plugin.MojoExecutionException;
43  import org.apache.maven.plugins.pmd.ExcludeDuplicationsFromFile;
44  import org.apache.maven.reporting.MavenReportException;
45  import org.codehaus.plexus.util.FileUtils;
46  import org.slf4j.Logger;
47  import org.slf4j.LoggerFactory;
48  
49  /**
50   * Executes CPD with the configuration provided via {@link CpdRequest}.
51   */
52  public class CpdExecutor extends Executor {
53      private static final Logger LOG = LoggerFactory.getLogger(CpdExecutor.class);
54  
55      public static CpdResult execute(CpdRequest request) throws MavenReportException {
56          if (request.getJavaExecutable() != null) {
57              return fork(request);
58          }
59  
60          ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
61          try {
62              Thread.currentThread().setContextClassLoader(CpdExecutor.class.getClassLoader());
63              CpdExecutor cpdExecutor = new CpdExecutor(request);
64              return cpdExecutor.run();
65          } finally {
66              Thread.currentThread().setContextClassLoader(origLoader);
67          }
68      }
69  
70      private static CpdResult fork(CpdRequest request) throws MavenReportException {
71          File basePmdDir = new File(request.getTargetDirectory(), "pmd");
72          basePmdDir.mkdirs();
73          File cpdRequestFile = new File(basePmdDir, "cpdrequest.bin");
74          try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(cpdRequestFile))) {
75              out.writeObject(request);
76          } catch (IOException e) {
77              throw new MavenReportException(e.getMessage(), e);
78          }
79  
80          String classpath = buildClasspath();
81          ProcessBuilder pb = new ProcessBuilder();
82          // note: using env variable instead of -cp cli arg to avoid length limitations under Windows
83          pb.environment().put("CLASSPATH", classpath);
84          pb.command().add(request.getJavaExecutable());
85          pb.command().add(CpdExecutor.class.getName());
86          pb.command().add(cpdRequestFile.getAbsolutePath());
87  
88          LOG.debug("Executing: CLASSPATH={}, command={}", classpath, pb.command());
89          try {
90              final Process p = pb.start();
91              // Note: can't use pb.inheritIO(), since System.out/System.err has been modified after process start
92              // and inheritIO would only inherit file handles, not the changed streams.
93              ProcessStreamHandler.start(p.getInputStream(), System.out);
94              ProcessStreamHandler.start(p.getErrorStream(), System.err);
95              int exit = p.waitFor();
96              LOG.debug("CpdExecutor exit code: {}", exit);
97              if (exit != 0) {
98                  throw new MavenReportException("CpdExecutor exited with exit code " + exit);
99              }
100             return new CpdResult(new File(request.getTargetDirectory(), "cpd.xml"), request.getOutputEncoding());
101         } catch (IOException e) {
102             throw new MavenReportException(e.getMessage(), e);
103         } catch (InterruptedException e) {
104             Thread.currentThread().interrupt();
105             throw new MavenReportException(e.getMessage(), e);
106         }
107     }
108 
109     /**
110      * Execute CPD analysis from CLI.
111      *
112      * <p>
113      * Single arg with the filename to the serialized {@link CpdRequest}.
114      *
115      * <p>
116      * Exit-code: 0 = success, 1 = failure in executing
117      *
118      * @param args
119      */
120     public static void main(String[] args) {
121         File requestFile = new File(args[0]);
122         try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(requestFile))) {
123             CpdRequest request = (CpdRequest) in.readObject();
124             CpdExecutor cpdExecutor = new CpdExecutor(request);
125             cpdExecutor.setupLogLevel(request.getLogLevel());
126             cpdExecutor.run();
127             System.exit(0);
128         } catch (IOException | ClassNotFoundException | MavenReportException e) {
129             LOG.error(e.getMessage(), e);
130         }
131         System.exit(1);
132     }
133 
134     private final CpdRequest request;
135 
136     /** Helper to exclude duplications from the result. */
137     private final ExcludeDuplicationsFromFile excludeDuplicationsFromFile = new ExcludeDuplicationsFromFile();
138 
139     public CpdExecutor(CpdRequest request) {
140         this.request = Objects.requireNonNull(request);
141     }
142 
143     private CpdResult run() throws MavenReportException {
144         try {
145             excludeDuplicationsFromFile.loadExcludeFromFailuresData(request.getExcludeFromFailureFile());
146         } catch (MojoExecutionException e) {
147             throw new MavenReportException("Error loading exclusions", e);
148         }
149 
150         CPDConfiguration cpdConfiguration = new CPDConfiguration();
151         cpdConfiguration.setMinimumTileSize(request.getMinimumTokens());
152         cpdConfiguration.setIgnoreAnnotations(request.isIgnoreAnnotations());
153         cpdConfiguration.setIgnoreLiterals(request.isIgnoreLiterals());
154         cpdConfiguration.setIgnoreIdentifiers(request.isIgnoreIdentifiers());
155 
156         String languageId = request.getLanguage();
157         if ("javascript".equals(languageId)) {
158             languageId = "ecmascript";
159         } else if (languageId == null) {
160             languageId = "java"; // default
161         }
162         Language cpdLanguage = cpdConfiguration.getLanguageRegistry().getLanguageById(languageId);
163 
164         cpdConfiguration.setOnlyRecognizeLanguage(cpdLanguage);
165         cpdConfiguration.setSourceEncoding(Charset.forName(request.getSourceEncoding()));
166 
167         request.getFiles().forEach(f -> cpdConfiguration.addInputPath(f.toPath()));
168 
169         LOG.debug("Executing CPD...");
170 
171         // always create XML format. we need to output it even if the file list is empty or we have no duplications
172         // so the "check" goals can check for violations
173         try (CpdAnalysis cpd = CpdAnalysis.create(cpdConfiguration)) {
174             cpd.performAnalysis(report -> {
175                 try {
176                     writeXmlReport(report);
177 
178                     // html format is handled by maven site report, xml format has already been rendered
179                     String format = request.getFormat();
180                     if (!"html".equals(format) && !"xml".equals(format)) {
181                         writeFormattedReport(report);
182                     }
183                 } catch (MavenReportException e) {
184                     LOG.error("Error while writing CPD report", e);
185                 }
186             });
187         } catch (IOException e) {
188             LOG.error("Error while executing CPD", e);
189         }
190         LOG.debug("CPD finished.");
191 
192         return new CpdResult(new File(request.getTargetDirectory(), "cpd.xml"), request.getOutputEncoding());
193     }
194 
195     private void writeXmlReport(CPDReport cpd) throws MavenReportException {
196         File targetFile = writeReport(cpd, new XMLRenderer(request.getOutputEncoding()), "xml");
197         if (request.isIncludeXmlInSite()) {
198             File siteDir = new File(request.getReportOutputDirectory());
199             siteDir.mkdirs();
200             try {
201                 FileUtils.copyFile(targetFile, new File(siteDir, "cpd.xml"));
202             } catch (IOException e) {
203                 throw new MavenReportException(e.getMessage(), e);
204             }
205         }
206     }
207 
208     private File writeReport(CPDReport cpd, CPDReportRenderer r, String extension) throws MavenReportException {
209         if (r == null) {
210             return null;
211         }
212 
213         File targetDir = new File(request.getTargetDirectory());
214         targetDir.mkdirs();
215         File targetFile = new File(targetDir, "cpd." + extension);
216         try (Writer writer = new OutputStreamWriter(new FileOutputStream(targetFile), request.getOutputEncoding())) {
217             r.render(cpd.filterMatches(filterMatches()), writer);
218             writer.flush();
219         } catch (IOException ioe) {
220             throw new MavenReportException(ioe.getMessage(), ioe);
221         }
222         return targetFile;
223     }
224 
225     private void writeFormattedReport(CPDReport cpd) throws MavenReportException {
226         CPDReportRenderer r = createRenderer(request.getFormat(), request.getOutputEncoding());
227         writeReport(cpd, r, request.getFormat());
228     }
229 
230     /**
231      * Create and return the correct renderer for the output type.
232      *
233      * @return the renderer based on the configured output
234      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
235      */
236     public static CPDReportRenderer createRenderer(String format, String outputEncoding) throws MavenReportException {
237         CPDReportRenderer renderer = null;
238         if ("xml".equals(format)) {
239             renderer = new XMLRenderer(outputEncoding);
240         } else if ("csv".equals(format)) {
241             renderer = new CSVRenderer();
242         } else if ("txt".equals(format)) {
243             renderer = new SimpleRenderer();
244         } else if (!"".equals(format) && !"none".equals(format)) {
245             try {
246                 renderer = (CPDReportRenderer)
247                         Class.forName(format).getConstructor().newInstance();
248             } catch (Exception e) {
249                 throw new MavenReportException(
250                         "Can't find CPD custom format " + format + ": "
251                                 + e.getClass().getName(),
252                         e);
253             }
254         }
255 
256         return renderer;
257     }
258 
259     private Predicate<Match> filterMatches() {
260         return (Match match) -> {
261             LOG.debug("Filtering duplications. Using " + excludeDuplicationsFromFile.countExclusions()
262                     + " configured exclusions.");
263 
264             if (excludeDuplicationsFromFile.isExcludedFromFailure(match)) {
265                 LOG.debug("Excluded " + match + " duplications.");
266                 return false;
267             } else {
268                 return true;
269             }
270         };
271     }
272 }