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