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.plugin.compiler;
20  
21  import java.io.IOException;
22  import java.nio.file.Files;
23  import java.nio.file.Path;
24  import java.util.Collections;
25  import java.util.List;
26  import java.util.Set;
27  import java.util.stream.Collectors;
28  import java.util.stream.Stream;
29  
30  import org.apache.maven.api.plugin.MojoException;
31  
32  /**
33   * Various helper methods to support incremental builds
34   */
35  public class IncrementalBuildHelper {
36      /**
37       * the root directory to store status information about Maven executions in.
38       */
39      private static final String MAVEN_STATUS_ROOT = "maven-status";
40  
41      public static final String CREATED_FILES_LST_FILENAME = "createdFiles.lst";
42      private static final String INPUT_FILES_LST_FILENAME = "inputFiles.lst";
43  
44      /**
45       * Needed for storing the status for the incremental build support.
46       */
47      private final String mojoStatusPath;
48  
49      private final Set<Path> sources;
50  
51      private final Path directory;
52  
53      private final Path outputDirectory;
54  
55      /**
56       * Once the {@link #beforeRebuildExecution()} got
57       * called, this will contain the list of files in the build directory.
58       */
59      private List<Path> filesBeforeAction = Collections.emptyList();
60  
61      public IncrementalBuildHelper(String mojoStatusPath, Set<Path> sources, Path directory, Path outputDirectory) {
62          if (mojoStatusPath == null) {
63              throw new IllegalArgumentException("MojoExecution must not be null!");
64          }
65  
66          this.mojoStatusPath = mojoStatusPath;
67          this.sources = sources;
68          this.directory = directory;
69          this.outputDirectory = outputDirectory;
70      }
71  
72      /**
73       * We use a specific status directory for each Mojo execution to store state
74       * which is needed during the next build invocation run.
75       * @return the directory for storing status information of the current Mojo execution.
76       */
77      public Path getMojoStatusDirectory() throws MojoException {
78          // X TODO the executionId contains -cli and -mojoname
79          // X we should remove those postfixes as it should not make
80          // X any difference whether being run on the cli or via build
81          Path mojoStatusDir = directory.resolve(mojoStatusPath);
82  
83          try {
84              Files.createDirectories(mojoStatusDir);
85          } catch (IOException e) {
86              throw new MojoException("Unable to create directory: " + mojoStatusDir, e);
87          }
88  
89          return mojoStatusDir;
90      }
91  
92      /**
93       * Detect whether the list of detected files has changed since the last build.
94       * We simply load the list of files for the previous build from a status file
95       * and compare it with the new list. Afterwards we store the new list in the status file.
96       *
97       * @return <code>true</code> if the set of inputFiles got changed since the last build.
98       */
99      public boolean inputFileTreeChanged(List<String> added, List<String> removed) {
100         Path mojoConfigBase = getMojoStatusDirectory();
101         Path mojoConfigFile = mojoConfigBase.resolve(INPUT_FILES_LST_FILENAME);
102 
103         List<String> oldInputFiles = Collections.emptyList();
104 
105         if (Files.exists(mojoConfigFile)) {
106             try {
107                 oldInputFiles = Files.readAllLines(mojoConfigFile);
108             } catch (IOException e) {
109                 throw new MojoException("Error reading old mojo status " + mojoConfigFile, e);
110             }
111         }
112 
113         List<String> newFiles =
114                 sources.stream().map(Path::toAbsolutePath).map(Path::toString).collect(Collectors.toList());
115 
116         List<String> previousFiles = oldInputFiles;
117         newFiles.stream().filter(s -> !previousFiles.contains(s)).forEach(added::add);
118         previousFiles.stream().filter(s -> !newFiles.contains(s)).forEach(removed::add);
119         try {
120             Files.write(mojoConfigFile, added);
121         } catch (IOException e) {
122             throw new MojoException("Error while storing the mojo status", e);
123         }
124 
125         return added.size() + removed.size() > 0;
126     }
127 
128     /**
129      * <p>
130      * This method shall get invoked before the actual Mojo task gets triggered, e.g. the actual compile in
131      * maven-compiler-plugin.
132      * </p>
133      * <p>
134      * <b>Attention:</b> This method shall only get invoked if the plugin re-creates <b>all</b> the output.
135      * </p>
136      * <p>
137      * It first picks up the list of files created in the previous build and delete them. This step is necessary to
138      * prevent left-overs. After that we take a 'directory snapshot' (list of all files which exist in the
139      * outputDirectory after the clean).
140      * </p>
141      * <p>
142      * After the actual Mojo task got executed you should invoke the method
143      * {@link #afterRebuildExecution()} to collect the
144      * list of files which got changed by this task.
145      * </p>
146      */
147     public void beforeRebuildExecution() {
148         Path mojoConfigBase = getMojoStatusDirectory();
149         Path mojoConfigFile = mojoConfigBase.resolve(CREATED_FILES_LST_FILENAME);
150 
151         try {
152             if (Files.exists(mojoConfigFile)) {
153                 for (String oldFileName : Files.readAllLines(mojoConfigFile)) {
154                     Path oldFile = outputDirectory.resolve(oldFileName);
155                     Files.deleteIfExists(oldFile);
156                 }
157             }
158 
159             // we remember all files which currently exist in the output directory
160             if (Files.exists(outputDirectory)) {
161                 try (Stream<Path> walk = Files.walk(outputDirectory)) {
162                     filesBeforeAction = walk.filter(Files::isRegularFile).collect(Collectors.toList());
163                 }
164             }
165         } catch (IOException e) {
166             throw new MojoException("Error reading old mojo status", e);
167         }
168     }
169 
170     /**
171      * <p>This method collects and stores all information about files changed since the
172      * call to {@link #beforeRebuildExecution()}.</p>
173      *
174      * <p><b>Attention:</b> This method shall only get invoked if the plugin re-creates <b>all</b> the output.</p>
175      */
176     public void afterRebuildExecution() {
177         Path mojoConfigBase = getMojoStatusDirectory();
178         Path mojoConfigFile = mojoConfigBase.resolve(CREATED_FILES_LST_FILENAME);
179 
180         try {
181             try (Stream<Path> walk = Files.walk(outputDirectory)) {
182                 List<String> added = walk.filter(Files::isRegularFile)
183                         .filter(p -> !filesBeforeAction.contains(p))
184                         .map(Path::toString)
185                         .collect(Collectors.toList());
186 
187                 Files.write(mojoConfigFile, added);
188             }
189         } catch (IOException e) {
190             throw new MojoException("Error while storing the mojo status", e);
191         }
192 
193         // in case of clean compile the file is not created so next compile won't see it
194         // we mus create it here
195         mojoConfigFile = mojoConfigBase.resolve(INPUT_FILES_LST_FILENAME);
196         if (!Files.exists(mojoConfigFile)) {
197             try {
198                 Files.write(mojoConfigFile, sources.stream().map(Path::toString).collect(Collectors.toList()));
199             } catch (IOException e) {
200                 throw new MojoException("Error while storing the mojo status", e);
201             }
202         }
203     }
204 }