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