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.nio.charset.Charset;
28  import java.util.Objects;
29  
30  import net.sourceforge.pmd.cpd.CPDConfiguration;
31  import net.sourceforge.pmd.cpd.CPDReportRenderer;
32  import net.sourceforge.pmd.cpd.CSVRenderer;
33  import net.sourceforge.pmd.cpd.CpdAnalysis;
34  import net.sourceforge.pmd.cpd.SimpleRenderer;
35  import net.sourceforge.pmd.cpd.XMLRenderer;
36  import net.sourceforge.pmd.lang.Language;
37  import org.apache.maven.plugin.MojoExecutionException;
38  import org.apache.maven.plugins.pmd.ExcludeDuplicationsFromFile;
39  import org.apache.maven.reporting.MavenReportException;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  /**
44   * Executes CPD with the configuration provided via {@link CpdRequest}.
45   */
46  public class CpdExecutor extends Executor {
47      private static final Logger LOG = LoggerFactory.getLogger(CpdExecutor.class);
48  
49      public static CpdResult execute(CpdRequest request) throws MavenReportException {
50          if (request.getJavaExecutable() != null) {
51              return fork(request);
52          }
53  
54          ClassLoader origLoader = Thread.currentThread().getContextClassLoader();
55          try {
56              Thread.currentThread().setContextClassLoader(CpdExecutor.class.getClassLoader());
57              CpdExecutor cpdExecutor = new CpdExecutor(request);
58              return cpdExecutor.run();
59          } finally {
60              Thread.currentThread().setContextClassLoader(origLoader);
61          }
62      }
63  
64      private static CpdResult fork(CpdRequest request) throws MavenReportException {
65          File basePmdDir = new File(request.getTargetDirectory(), "pmd");
66          basePmdDir.mkdirs();
67          File cpdRequestFile = new File(basePmdDir, "cpdrequest.bin");
68          try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(cpdRequestFile))) {
69              out.writeObject(request);
70          } catch (IOException e) {
71              throw new MavenReportException(e.getMessage(), e);
72          }
73  
74          String classpath = buildClasspath();
75          ProcessBuilder pb = new ProcessBuilder();
76          // note: using env variable instead of -cp cli arg to avoid length limitations under Windows
77          pb.environment().put("CLASSPATH", classpath);
78          pb.command().add(request.getJavaExecutable());
79          pb.command().add(CpdExecutor.class.getName());
80          pb.command().add(cpdRequestFile.getAbsolutePath());
81  
82          LOG.debug("Executing: CLASSPATH={}, command={}", classpath, pb.command());
83          try {
84              final Process p = pb.start();
85              // Note: can't use pb.inheritIO(), since System.out/System.err has been modified after process start
86              // and inheritIO would only inherit file handles, not the changed streams.
87              ProcessStreamHandler.start(p.getInputStream(), System.out);
88              ProcessStreamHandler.start(p.getErrorStream(), System.err);
89              int exit = p.waitFor();
90              LOG.debug("CpdExecutor exit code: {}", exit);
91              if (exit != 0) {
92                  throw new MavenReportException("CpdExecutor exited with exit code " + exit);
93              }
94              return new CpdResult(new File(request.getTargetDirectory(), "cpd.xml"), request.getOutputEncoding());
95          } catch (IOException e) {
96              throw new MavenReportException(e.getMessage(), e);
97          } catch (InterruptedException e) {
98              Thread.currentThread().interrupt();
99              throw new MavenReportException(e.getMessage(), e);
100         }
101     }
102 
103     /**
104      * Execute CPD analysis from CLI.
105      *
106      * <p>
107      * Single arg with the filename to the serialized {@link CpdRequest}.
108      *
109      * <p>
110      * Exit-code: 0 = success, 1 = failure in executing
111      *
112      * @param args
113      */
114     public static void main(String[] args) {
115         File requestFile = new File(args[0]);
116         try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(requestFile))) {
117             CpdRequest request = (CpdRequest) in.readObject();
118             CpdExecutor cpdExecutor = new CpdExecutor(request);
119             cpdExecutor.setupLogLevel(request.getLogLevel());
120             cpdExecutor.run();
121             System.exit(0);
122         } catch (IOException | ClassNotFoundException | MavenReportException e) {
123             LOG.error(e.getMessage(), e);
124         }
125         System.exit(1);
126     }
127 
128     private final CpdRequest request;
129 
130     /** Helper to exclude duplications from the result. */
131     private final ExcludeDuplicationsFromFile excludeDuplicationsFromFile = new ExcludeDuplicationsFromFile();
132 
133     public CpdExecutor(CpdRequest request) {
134         this.request = Objects.requireNonNull(request);
135     }
136 
137     private CpdResult run() throws MavenReportException {
138         try {
139             excludeDuplicationsFromFile.loadExcludeFromFailuresData(request.getExcludeFromFailureFile());
140         } catch (MojoExecutionException e) {
141             throw new MavenReportException("Error loading exclusions", e);
142         }
143 
144         CPDConfiguration cpdConfiguration = new CPDConfiguration();
145         cpdConfiguration.setMinimumTileSize(request.getMinimumTokens());
146         cpdConfiguration.setIgnoreAnnotations(request.isIgnoreAnnotations());
147         cpdConfiguration.setIgnoreLiterals(request.isIgnoreLiterals());
148         cpdConfiguration.setIgnoreIdentifiers(request.isIgnoreIdentifiers());
149         // we are not running CPD through CLI and deal with any errors during analysis on our own
150         cpdConfiguration.setSkipLexicalErrors(true);
151 
152         String languageId = request.getLanguage();
153         if ("javascript".equals(languageId)) {
154             languageId = "ecmascript";
155         } else if (languageId == null) {
156             languageId = "java"; // default
157         }
158         Language cpdLanguage = cpdConfiguration.getLanguageRegistry().getLanguageById(languageId);
159 
160         cpdConfiguration.setOnlyRecognizeLanguage(cpdLanguage);
161         cpdConfiguration.setSourceEncoding(Charset.forName(request.getSourceEncoding()));
162 
163         request.getFiles().forEach(f -> cpdConfiguration.addInputPath(f.toPath()));
164 
165         LOG.debug("Executing CPD...");
166         try (CpdAnalysis cpd = CpdAnalysis.create(cpdConfiguration)) {
167             CpdReportConsumer reportConsumer = new CpdReportConsumer(request, excludeDuplicationsFromFile);
168             cpd.performAnalysis(reportConsumer);
169         } catch (IOException e) {
170             throw new MavenReportException("Error while executing CPD", e);
171         }
172         LOG.debug("CPD finished.");
173 
174         // in constrast to pmd goal, we don't have a parameter for cpd like "skipPmdError" - if there
175         // are any errors during CPD analysis, the maven build fails.
176         int cpdErrors = cpdConfiguration.getReporter().numErrors();
177         if (cpdErrors == 1) {
178             throw new MavenReportException("There was 1 error while executing CPD");
179         } else if (cpdErrors > 1) {
180             throw new MavenReportException("There were " + cpdErrors + " errors while executing CPD");
181         }
182 
183         return new CpdResult(new File(request.getTargetDirectory(), "cpd.xml"), request.getOutputEncoding());
184     }
185 
186     /**
187      * Create and return the correct renderer for the output type.
188      *
189      * @return the renderer based on the configured output
190      * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type
191      */
192     public static CPDReportRenderer createRenderer(String format, String outputEncoding) throws MavenReportException {
193         CPDReportRenderer renderer = null;
194         if ("xml".equals(format)) {
195             renderer = new XMLRenderer(outputEncoding);
196         } else if ("csv".equals(format)) {
197             renderer = new CSVRenderer();
198         } else if ("txt".equals(format)) {
199             renderer = new SimpleRenderer();
200         } else if (!"".equals(format) && !"none".equals(format)) {
201             try {
202                 renderer = (CPDReportRenderer)
203                         Class.forName(format).getConstructor().newInstance();
204             } catch (Exception e) {
205                 throw new MavenReportException(
206                         "Can't find CPD custom format " + format + ": "
207                                 + e.getClass().getName(),
208                         e);
209             }
210         }
211 
212         return renderer;
213     }
214 }