package org.apache.maven.shared.incremental; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.MojoExecution; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.apache.maven.shared.utils.io.DirectoryScanResult; import org.apache.maven.shared.utils.io.DirectoryScanner; import org.apache.maven.shared.utils.io.FileUtils; import java.io.File; import java.io.IOException; import java.util.Set; /** * Various helper methods to support incremental builds */ public class IncrementalBuildHelper { /** * the root directory to store status information about Maven executions in. */ private static final String MAVEN_STATUS_ROOT = "maven-status"; public static final String CREATED_FILES_LST_FILENAME = "createdFiles.lst"; private static final String INPUT_FILES_LST_FILENAME = "inputFiles.lst"; private static final String[] EMPTY_ARRAY = new String[0]; /** * Needed for storing the status for the incremental build support. */ private MojoExecution mojoExecution; /** * Needed for storing the status for the incremental build support. */ private MavenProject mavenProject; /** * Used for detecting changes between the Mojo execution. * @see #getDirectoryScanner(); */ private DirectoryScanner directoryScanner; /** * Once the {@link #beforeRebuildExecution(org.apache.maven.shared.incremental.IncrementalBuildHelperRequest)} got * called, this will contain the list of files in the build directory. */ private String[] filesBeforeAction = new String[0]; public IncrementalBuildHelper( MojoExecution mojoExecution, MavenSession mavenSession ) { this( mojoExecution, getMavenProject( mavenSession ) ); } public IncrementalBuildHelper( MojoExecution mojoExecution, MavenProject mavenProject ) { if ( mavenProject == null ) { throw new IllegalArgumentException( "MavenProject must not be null!" ); } if ( mojoExecution == null ) { throw new IllegalArgumentException( "MojoExecution must not be null!" ); } this.mavenProject = mavenProject; this.mojoExecution = mojoExecution; } /** * small helper method to allow for the nullcheck in the ct invocation */ private static MavenProject getMavenProject( MavenSession mavenSession ) { if ( mavenSession == null ) { throw new IllegalArgumentException( "MavenSession must not be null!" ); } return mavenSession.getCurrentProject(); } /** * Get the existing DirectoryScanner used by this helper, * or create new a DirectoryScanner if none is yet set. * The DirectoryScanner is used for detecting changes in a directory */ public DirectoryScanner getDirectoryScanner() { if ( directoryScanner == null ) { directoryScanner = new DirectoryScanner(); } return directoryScanner; } /** * Set the DirectoryScanner which shall get used by this build helper. * @param directoryScanner */ public void setDirectoryScanner( DirectoryScanner directoryScanner ) { this.directoryScanner = directoryScanner; } /** * We use a specific status directory for each Mojo execution to store state * which is needed during the next build invocation run. * @return the directory for storing status information of the current Mojo execution. */ public File getMojoStatusDirectory() throws MojoExecutionException { if ( mojoExecution == null ) { throw new MojoExecutionException( "MojoExecution could not get resolved" ); } File buildOutputDirectory = new File( mavenProject.getBuild().getDirectory() ); //X TODO the executionId contains -cli and -mojoname //X we should remove those postfixes as it should not make //X any difference whether being run on the cli or via build String mojoStatusPath = MAVEN_STATUS_ROOT + File.separator + mojoExecution.getMojoDescriptor().getPluginDescriptor().getArtifactId() + File.separator + mojoExecution.getMojoDescriptor().getGoal() + File.separator + mojoExecution.getExecutionId(); File mojoStatusDir = new File( buildOutputDirectory, mojoStatusPath ); if ( !mojoStatusDir.exists() ) { mojoStatusDir.mkdirs(); } return mojoStatusDir; } /** * Detect whether the list of detected files has changed since the last build. * We simply load the list of files for the previous build from a status file * and compare it with the new list. Afterwards we store the new list in the status file. * * @param incrementalBuildHelperRequest * @return <code>true</code> if the set of inputFiles got changed since the last build. * @throws MojoExecutionException */ public boolean inputFileTreeChanged( IncrementalBuildHelperRequest incrementalBuildHelperRequest ) throws MojoExecutionException { File mojoConfigBase = getMojoStatusDirectory(); File mojoConfigFile = new File( mojoConfigBase, INPUT_FILES_LST_FILENAME ); String[] oldInputFiles = new String[0]; if ( mojoConfigFile.exists() ) { try { oldInputFiles = FileUtils.fileReadArray( mojoConfigFile ); } catch ( IOException e ) { throw new MojoExecutionException( "Error reading old mojo status " + mojoConfigFile, e ); } } String[] inputFileNames = new String[incrementalBuildHelperRequest.getInputFiles().size()]; int i = 0; for ( File inputFile : incrementalBuildHelperRequest.getInputFiles() ) { inputFileNames[i++] = inputFile.getAbsolutePath(); } DirectoryScanResult dsr = DirectoryScanner.diffFiles( oldInputFiles, inputFileNames ); try { FileUtils.fileWriteArray( mojoConfigFile, inputFileNames ); } catch ( IOException e ) { throw new MojoExecutionException( "Error while storing the mojo status", e ); } return ( dsr.getFilesAdded().length > 0 || dsr.getFilesRemoved().length > 0 ); } /** * Detect whether the list of detected files picked up by the DirectoryScanner * has changed since the last build. * We simply load the list of files for the previous build from a status file * and compare it with the result of the new DirectoryScanner#scan(). * Afterwards we store the new list in the status file. * * @param dirScanner * @return <code>true</code> if the set of inputFiles got changed since the last build. * @throws MojoExecutionException */ public boolean inputFileTreeChanged( DirectoryScanner dirScanner ) throws MojoExecutionException { File mojoConfigBase = getMojoStatusDirectory(); File mojoConfigFile = new File( mojoConfigBase, INPUT_FILES_LST_FILENAME ); String[] oldInputFiles = new String[0]; if ( mojoConfigFile.exists() ) { try { oldInputFiles = FileUtils.fileReadArray( mojoConfigFile ); } catch ( IOException e ) { throw new MojoExecutionException( "Error reading old mojo status " + mojoConfigFile, e ); } } dirScanner.scan(); try { // store away the list of input files FileUtils.fileWriteArray( mojoConfigFile, dirScanner.getIncludedFiles() ); } catch ( IOException e ) { throw new MojoExecutionException( "Error while storing new mojo status" + mojoConfigFile, e ); } DirectoryScanResult dsr = dirScanner.diffIncludedFiles( oldInputFiles ); return ( dsr.getFilesAdded().length > 0 || dsr.getFilesRemoved().length > 0 ); } /** * <p> * This method shall get invoked before the actual Mojo task gets triggered, e.g. the actual compile in * maven-compiler-plugin. * </p> * <p> * <b>Attention:</b> This method shall only get invoked if the plugin re-creates <b>all</b> the output. * </p> * <p> * It first picks up the list of files created in the previous build and delete them. This step is necessary to * prevent left-overs. After that we take a 'directory snapshot' (list of all files which exist in the * outputDirectory after the clean). * </p> * <p> * After the actual Mojo task got executed you should invoke the method * {@link #afterRebuildExecution(org.apache.maven.shared.incremental.IncrementalBuildHelperRequest)} to collect the * list of files which got changed by this task. * </p> * * @param incrementalBuildHelperRequest * @return all files which got created in the previous build and have been deleted now. * @throws MojoExecutionException */ public String[] beforeRebuildExecution( IncrementalBuildHelperRequest incrementalBuildHelperRequest ) throws MojoExecutionException { File mojoConfigBase = getMojoStatusDirectory(); File mojoConfigFile = new File( mojoConfigBase, CREATED_FILES_LST_FILENAME ); String[] oldFiles; try { oldFiles = FileUtils.fileReadArray( mojoConfigFile ); for ( String oldFileName : oldFiles ) { File oldFile = new File( incrementalBuildHelperRequest.getOutputDirectory(), oldFileName ); oldFile.delete(); } } catch ( IOException e ) { throw new MojoExecutionException( "Error reading old mojo status", e ); } // we remember all files which currently exist in the output directory DirectoryScanner diffScanner = getDirectoryScanner(); diffScanner.setBasedir( incrementalBuildHelperRequest.getOutputDirectory() ); if ( incrementalBuildHelperRequest.getOutputDirectory().exists() ) { diffScanner.scan(); filesBeforeAction = diffScanner.getIncludedFiles(); } return oldFiles; } /** * <p>This method collects and stores all information about files changed since the * call to {@link #beforeRebuildExecution(org.apache.maven.shared.incremental.IncrementalBuildHelperRequest)}.</p> * * <p><b>Attention:</b> This method shall only get invoked if the plugin re-creates <b>all</b> the output.</p> * * @param incrementalBuildHelperRequest will contains file sources to store if create files are not yet stored * * @throws MojoExecutionException */ public void afterRebuildExecution( IncrementalBuildHelperRequest incrementalBuildHelperRequest ) throws MojoExecutionException { DirectoryScanner diffScanner = getDirectoryScanner(); // now scan the same directory again and create a diff diffScanner.scan(); DirectoryScanResult scanResult = diffScanner.diffIncludedFiles( filesBeforeAction ); File mojoConfigBase = getMojoStatusDirectory(); File mojoConfigFile = new File( mojoConfigBase, CREATED_FILES_LST_FILENAME ); try { FileUtils.fileWriteArray( mojoConfigFile, scanResult.getFilesAdded() ); } catch ( IOException e ) { throw new MojoExecutionException( "Error while storing the mojo status", e ); } // in case of clean compile the file is not created so next compile won't see it // we mus create it here mojoConfigFile = new File( mojoConfigBase, INPUT_FILES_LST_FILENAME ); if ( !mojoConfigFile.exists() ) { try { FileUtils.fileWriteArray( mojoConfigFile, toArrayOfPath( incrementalBuildHelperRequest.getInputFiles() ) ); } catch ( IOException e ) { throw new MojoExecutionException( "Error while storing the mojo status", e ); } } } private String[] toArrayOfPath( Set<File> files ) { if ( files == null || files.isEmpty() ) { return EMPTY_ARRAY; } String[] paths = new String[files.size()]; int i = 0; for ( File file : files ) { paths[i] = file.getPath(); i++; } return paths; } }