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 
20 package org.apache.thrift.maven;
21 
22 import com.google.common.base.Joiner;
23 import com.google.common.collect.ImmutableSet;
24 import org.apache.maven.artifact.Artifact;
25 import org.apache.maven.artifact.repository.ArtifactRepository;
26 import org.apache.maven.plugin.AbstractMojo;
27 import org.apache.maven.plugin.MojoExecutionException;
28 import org.apache.maven.plugin.MojoFailureException;
29 import org.apache.maven.project.MavenProject;
30 import org.apache.maven.project.MavenProjectHelper;
31 import org.codehaus.plexus.util.cli.CommandLineException;
32 import org.codehaus.plexus.util.io.RawInputStreamFacade;
33 import java.io.File;
34 import java.io.FilenameFilter;
35 import java.io.IOException;
36 import java.security.MessageDigest;
37 import java.security.NoSuchAlgorithmException;
38 import java.util.List;
39 import java.util.Set;
40 import java.util.jar.JarEntry;
41 import java.util.jar.JarFile;
42 import static com.google.common.base.Preconditions.checkArgument;
43 import static com.google.common.base.Preconditions.checkNotNull;
44 import static com.google.common.base.Preconditions.checkState;
45 import static com.google.common.collect.Sets.newHashSet;
46 import static java.lang.String.format;
47 import static java.util.Arrays.asList;
48 import static java.util.Collections.list;
49 import static org.codehaus.plexus.util.FileUtils.cleanDirectory;
50 import static org.codehaus.plexus.util.FileUtils.copyStreamToFile;
51 import static org.codehaus.plexus.util.FileUtils.getFiles;
52 
53 /**
54  * Abstract Mojo implementation.
55  * <p/>
56  * This class is extended by {@link org.apache.thrift.maven.ThriftCompileMojo} and
57  * {@link org.apache.thrift.maven.ThriftTestCompileMojo} in order to override the specific configuration for
58  * compiling the main or test classes respectively.
59  */
60 abstract class AbstractThriftMojo extends AbstractMojo {
61 
62     private static final String THRIFT_FILE_SUFFIX = ".thrift";
63 
64     private static final String DEFAULT_INCLUDES = "**/*" + THRIFT_FILE_SUFFIX;
65 
66     /**
67      * The current Maven project.
68      *
69      * @parameter default-value="${project}"
70      * @readonly
71      * @required
72      */
73     protected MavenProject project;
74 
75     /**
76      * A helper used to add resources to the project.
77      *
78      * @component
79      * @required
80      */
81     protected MavenProjectHelper projectHelper;
82 
83     /**
84      * This is the path to the {@code thrift} executable. By default it will search the {@code $PATH}.
85      *
86      * @parameter default-value="thrift"
87      * @required
88      */
89     private String thriftExecutable;
90 
91     /**
92      * This string is passed to the {@code --gen} option of the {@code thrift} parameter. By default
93      * it will generate Java output. The main reason for this option is to be able to add options
94      * to the Java generator - if you generate something else, you're on your own.
95      *
96      * @parameter default-value="java"
97      */
98     private String generator;
99 
100     /**
101      * @parameter
102      */
103     private File[] additionalThriftPathElements = new File[]{};
104 
105     /**
106      * Since {@code thrift} cannot access jars, thrift files in dependencies are extracted to this location
107      * and deleted on exit. This directory is always cleaned during execution.
108      *
109      * @parameter default-value="${project.build.directory}/thrift-dependencies"
110      * @required
111      */
112     private File temporaryThriftFileDirectory;
113 
114     /**
115      * This is the path to the local maven {@code repository}.
116      *
117      * @parameter default-value="${localRepository}"
118      * @required
119      */
120     protected ArtifactRepository localRepository;
121 
122     /**
123      * Set this to {@code false} to disable hashing of dependent jar paths.
124      * <p/>
125      * This plugin expands jars on the classpath looking for embedded .thrift files.
126      * Normally these paths are hashed (MD5) to avoid issues with long file names on windows.
127      * However if this property is set to {@code false} longer paths will be used.
128      *
129      * @parameter default-value="true"
130      * @required
131      */
132     protected boolean hashDependentPaths;
133 
134     /**
135      * @parameter
136      */
137     private Set<String> includes = ImmutableSet.of(DEFAULT_INCLUDES);
138 
139     /**
140      * @parameter
141      */
142     private Set<String> excludes = ImmutableSet.of();
143 
144     /**
145      * @parameter
146      */
147     private long staleMillis = 0;
148 
149     /**
150      * @parameter
151      */
152     private boolean checkStaleness = false;
153 
154     /**
155      * Executes the mojo.
156      */
execute()157     public void execute() throws MojoExecutionException, MojoFailureException {
158         checkParameters();
159         final File thriftSourceRoot = getThriftSourceRoot();
160         if (thriftSourceRoot.exists()) {
161             try {
162                 ImmutableSet<File> thriftFiles = findThriftFilesInDirectory(thriftSourceRoot);
163                 final File outputDirectory = getOutputDirectory();
164                 ImmutableSet<File> outputFiles = findGeneratedFilesInDirectory(getOutputDirectory());
165 
166                 if (thriftFiles.isEmpty()) {
167                     getLog().info("No thrift files to compile.");
168                 } else if (checkStaleness && ((lastModified(thriftFiles) + staleMillis) < lastModified(outputFiles))) {
169                     getLog().info("Skipping compilation because target directory newer than sources.");
170                     attachFiles();
171                 } else {
172                     ImmutableSet<File> derivedThriftPathElements =
173                             makeThriftPathFromJars(temporaryThriftFileDirectory, getDependencyArtifactFiles());
174                     outputDirectory.mkdirs();
175 
176                     // Quick fix to fix issues with two mvn installs in a row (ie no clean)
177                     // cleanDirectory(outputDirectory);
178 
179                     Thrift thrift = new Thrift.Builder(thriftExecutable, outputDirectory)
180                             .setGenerator(generator)
181                             .addThriftPathElement(thriftSourceRoot)
182                             .addThriftPathElements(derivedThriftPathElements)
183                             .addThriftPathElements(asList(additionalThriftPathElements))
184                             .addThriftFiles(thriftFiles)
185                             .build();
186                     final int exitStatus = thrift.compile();
187                     if (exitStatus != 0) {
188                         getLog().error("thrift failed output: " + thrift.getOutput());
189                         getLog().error("thrift failed error: " + thrift.getError());
190                         throw new MojoFailureException(
191                                 "thrift did not exit cleanly. Review output for more information.");
192                     }
193                     attachFiles();
194                 }
195             } catch (IOException e) {
196                 throw new MojoExecutionException("An IO error occurred", e);
197             } catch (IllegalArgumentException e) {
198                 throw new MojoFailureException("thrift failed to execute because: " + e.getMessage(), e);
199             } catch (CommandLineException e) {
200                 throw new MojoExecutionException("An error occurred while invoking thrift.", e);
201             }
202         } else {
203             getLog().info(format("%s does not exist. Review the configuration or consider disabling the plugin.",
204                     thriftSourceRoot));
205         }
206     }
207 
findGeneratedFilesInDirectory(File directory)208     ImmutableSet<File> findGeneratedFilesInDirectory(File directory) throws IOException {
209         if (directory == null || !directory.isDirectory())
210             return ImmutableSet.of();
211 
212         List<File> javaFilesInDirectory = getFiles(directory, "**/*.java", null);
213         return ImmutableSet.copyOf(javaFilesInDirectory);
214     }
215 
lastModified(ImmutableSet<File> files)216     private long lastModified(ImmutableSet<File> files) {
217         long result = 0;
218         for (File file : files) {
219             if (file.lastModified() > result)
220                 result = file.lastModified();
221         }
222         return result;
223     }
224 
checkParameters()225     private void checkParameters() {
226         checkNotNull(project, "project");
227         checkNotNull(projectHelper, "projectHelper");
228         checkNotNull(thriftExecutable, "thriftExecutable");
229         checkNotNull(generator, "generator");
230         final File thriftSourceRoot = getThriftSourceRoot();
231         checkNotNull(thriftSourceRoot);
232         checkArgument(!thriftSourceRoot.isFile(), "thriftSourceRoot is a file, not a directory");
233         checkNotNull(temporaryThriftFileDirectory, "temporaryThriftFileDirectory");
234         checkState(!temporaryThriftFileDirectory.isFile(), "temporaryThriftFileDirectory is a file, not a directory");
235         final File outputDirectory = getOutputDirectory();
236         checkNotNull(outputDirectory);
237         checkState(!outputDirectory.isFile(), "the outputDirectory is a file, not a directory");
238     }
239 
getThriftSourceRoot()240     protected abstract File getThriftSourceRoot();
241 
getDependencyArtifacts()242     protected abstract List<Artifact> getDependencyArtifacts();
243 
getOutputDirectory()244     protected abstract File getOutputDirectory();
245 
attachFiles()246     protected abstract void attachFiles();
247 
248     /**
249      * Gets the {@link File} for each dependency artifact.
250      *
251      * @return A set of all dependency artifacts.
252      */
getDependencyArtifactFiles()253     private ImmutableSet<File> getDependencyArtifactFiles() {
254         Set<File> dependencyArtifactFiles = newHashSet();
255         for (Artifact artifact : getDependencyArtifacts()) {
256             dependencyArtifactFiles.add(artifact.getFile());
257         }
258         return ImmutableSet.copyOf(dependencyArtifactFiles);
259     }
260 
261     /**
262      * @throws IOException
263      */
makeThriftPathFromJars(File temporaryThriftFileDirectory, Iterable<File> classpathElementFiles)264     ImmutableSet<File> makeThriftPathFromJars(File temporaryThriftFileDirectory, Iterable<File> classpathElementFiles)
265             throws IOException, MojoExecutionException {
266         checkNotNull(classpathElementFiles, "classpathElementFiles");
267         // clean the temporary directory to ensure that stale files aren't used
268         if (temporaryThriftFileDirectory.exists()) {
269             cleanDirectory(temporaryThriftFileDirectory);
270         }
271 
272         Set<File> thriftDirectories = newHashSet();
273 
274         for (File classpathElementFile : classpathElementFiles) {
275             // for some reason under IAM, we receive poms as dependent files
276             // I am excluding .xml rather than including .jar as there may be other extensions in use (sar, har, zip)
277             if (classpathElementFile.isFile() && classpathElementFile.canRead() &&
278                     !classpathElementFile.getName().endsWith(".xml")) {
279 
280                 // create the jar file. the constructor validates.
281                 JarFile classpathJar;
282                 try {
283                     classpathJar = new JarFile(classpathElementFile);
284                 } catch (IOException e) {
285                     throw new IllegalArgumentException(format(
286                             "%s was not a readable artifact", classpathElementFile));
287                 }
288 
289                 /**
290                  * Copy each .thrift file found in the JAR into a temporary directory, preserving the
291                  * directory path it had relative to its containing JAR. Add the resulting root directory
292                  * (unique for each JAR processed) to the set of thrift include directories to use when
293                  * compiling.
294                  */
295                 for (JarEntry jarEntry : list(classpathJar.entries())) {
296                     final String jarEntryName = jarEntry.getName();
297                     if (jarEntry.getName().endsWith(THRIFT_FILE_SUFFIX)) {
298                         final String truncatedJarPath = truncatePath(classpathJar.getName());
299                         final File thriftRootDirectory = new File(temporaryThriftFileDirectory, truncatedJarPath);
300                         final File uncompressedCopy =
301                                 new File(thriftRootDirectory, jarEntryName);
302                         uncompressedCopy.getParentFile().mkdirs();
303                         copyStreamToFile(new RawInputStreamFacade(classpathJar
304                                 .getInputStream(jarEntry)), uncompressedCopy);
305                         thriftDirectories.add(thriftRootDirectory);
306                     }
307                 }
308 
309             } else if (classpathElementFile.isDirectory()) {
310                 File[] thriftFiles = classpathElementFile.listFiles(new FilenameFilter() {
311                     public boolean accept(File dir, String name) {
312                         return name.endsWith(THRIFT_FILE_SUFFIX);
313                     }
314                 });
315 
316                 if (thriftFiles.length > 0) {
317                     thriftDirectories.add(classpathElementFile);
318                 }
319             }
320         }
321 
322         return ImmutableSet.copyOf(thriftDirectories);
323     }
324 
findThriftFilesInDirectory(File directory)325     ImmutableSet<File> findThriftFilesInDirectory(File directory) throws IOException {
326         checkNotNull(directory);
327         checkArgument(directory.isDirectory(), "%s is not a directory", directory);
328         List<File> thriftFilesInDirectory = getFiles(directory,
329         		Joiner.on(",").join(includes),
330         		Joiner.on(",").join(excludes));
331         return ImmutableSet.copyOf(thriftFilesInDirectory);
332     }
333 
334     /**
335      * Truncates the path of jar files so that they are relative to the local repository.
336      *
337      * @param jarPath the full path of a jar file.
338      * @return the truncated path relative to the local repository or root of the drive.
339      */
truncatePath(final String jarPath)340     String truncatePath(final String jarPath) throws MojoExecutionException {
341 
342         if (hashDependentPaths) {
343             try {
344                 return toHexString(MessageDigest.getInstance("MD5").digest(jarPath.getBytes()));
345             } catch (NoSuchAlgorithmException e) {
346                 throw new MojoExecutionException("Failed to expand dependent jar", e);
347             }
348         }
349 
350         String repository = localRepository.getBasedir().replace('\\', '/');
351         if (!repository.endsWith("/")) {
352             repository += "/";
353         }
354 
355         String path = jarPath.replace('\\', '/');
356         int repositoryIndex = path.indexOf(repository);
357         if (repositoryIndex != -1) {
358             path = path.substring(repositoryIndex + repository.length());
359         }
360 
361         // By now the path should be good, but do a final check to fix windows machines.
362         int colonIndex = path.indexOf(':');
363         if (colonIndex != -1) {
364             // 2 = :\ in C:\
365             path = path.substring(colonIndex + 2);
366         }
367 
368         return path;
369     }
370 
371     private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
372 
toHexString(byte[] byteArray)373     public static String toHexString(byte[] byteArray) {
374         final StringBuilder hexString = new StringBuilder(2 * byteArray.length);
375         for (final byte b : byteArray) {
376             hexString.append(HEX_CHARS[(b & 0xF0) >> 4]).append(HEX_CHARS[b & 0x0F]);
377         }
378         return hexString.toString();
379     }
380 }
381