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 javax.lang.model.SourceVersion;
22  
23  import java.io.Closeable;
24  import java.io.IOException;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.util.ArrayList;
28  import java.util.LinkedHashMap;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Set;
32  
33  import org.apache.maven.api.PathType;
34  
35  /**
36   * Source files for a specific Java release. Instances of {@code SourcesForRelease} are created from
37   * a list of {@link SourceFile} after the sources have been filtered according include and exclude filters.
38   *
39   * @author Martin Desruisseaux
40   */
41  final class SourcesForRelease implements Closeable {
42      /**
43       * The release for this set of sources, or {@code null} if the user did not specified a release.
44       *
45       * @see #getReleaseString()
46       * @see SourceDirectory#release
47       */
48      final SourceVersion release;
49  
50      /**
51       * All source files. This is the union of all {@link SourceFile#file} for this {@linkplain #release}.
52       *
53       * @see SourceFile#file
54       */
55      final List<Path> files;
56  
57      /**
58       * All source directories that are part of this compilation unit, grouped by module names.
59       * The keys in the map are the module names, with the empty string standing for no module.
60       * Values are the union of all {@link SourceDirectory#root} for this {@linkplain #release}.
61       *
62       * @see SourceDirectory#root
63       */
64      final Map<String, Set<Path>> roots;
65  
66      /**
67       * The directories that contains a {@code module-info.java} file. If the set of source files
68       * is for a Java release different than the base release, or if it is for the test sources,
69       * then a non-empty map means that some modules overwrite {@code module-info.class}.
70       */
71      private final Map<SourceDirectory, ModuleInfoOverwrite> moduleInfos;
72  
73      /**
74       * Last directory added to the {@link #roots} map. This is a small optimization for reducing
75       * the number of accesses to the map. In most cases, only one element will be written there.
76       */
77      private SourceDirectory lastDirectoryAdded;
78  
79      /**
80       * Snapshot of {@link ToolExecutor#dependencies}.
81       * This information is saved in case a {@code target/javac.args} debug file needs to be written.
82       */
83      Map<PathType, List<Path>> dependencySnapshot;
84  
85      /**
86       * The output directory for the release. This is either the base output directory or a sub-directory
87       * in {@code META-INF/versions/}. This field is not used by this class, but made available for making
88       * easier to write the {@code target/javac.args} debug file.
89       */
90      Path outputForRelease;
91  
92      /**
93       * Creates an initially empty instance for the given Java release.
94       *
95       * @param release the release for this set of sources, or {@code null} if the user did not specified a release
96       */
97      SourcesForRelease(SourceVersion release) {
98          this.release = release;
99          files = new ArrayList<>();
100         roots = new LinkedHashMap<>();
101         moduleInfos = new LinkedHashMap<>();
102     }
103 
104     /**
105      * Returns the release as a string suitable for the {@code --release} compiler option.
106      *
107      * @return the release number as a string, or {@code null} if none
108      */
109     String getReleaseString() {
110         if (release == null) {
111             return null;
112         }
113         var version = release.name();
114         return version.substring(version.lastIndexOf('_') + 1);
115     }
116 
117     /**
118      * Adds the given source file to this collection of source files.
119      * The value of {@code source.directory.release}, if not null, must be equal to {@link #release}.
120      *
121      * @param source the source file to add.
122      */
123     void add(SourceFile source) {
124         var directory = source.directory;
125         if (lastDirectoryAdded != directory) {
126             lastDirectoryAdded = directory;
127             String moduleName = directory.moduleName;
128             if (moduleName == null || moduleName.isBlank()) {
129                 moduleName = "";
130             }
131             roots.get(moduleName).add(directory.root);
132             directory.getModuleInfo().ifPresent((path) -> moduleInfos.put(directory, null));
133         }
134         files.add(source.file);
135     }
136 
137     /**
138      * If there is any {@code module-info.class} in the main classes that are overwritten by this set of sources,
139      * temporarily replace the main files by the test files. The {@link #close()} method must be invoked after
140      * this method for resetting the original state.
141      *
142      * <p>This method is invoked when the test files overwrite the {@code module-info.class} from the main files.
143      * This method should not be invoked during the compilation of main classes, as its behavior may be not well
144      * defined.</p>
145      */
146     void substituteModuleInfos(final Path mainOutputDirectory, final Path testOutputDirectory) throws IOException {
147         for (Map.Entry<SourceDirectory, ModuleInfoOverwrite> entry : moduleInfos.entrySet()) {
148             Path main = mainOutputDirectory;
149             Path test = testOutputDirectory;
150             SourceDirectory directory = entry.getKey();
151             String moduleName = directory.moduleName;
152             if (moduleName != null) {
153                 main = main.resolve(moduleName);
154                 if (!Files.isDirectory(main)) {
155                     main = mainOutputDirectory;
156                 }
157                 test = test.resolve(moduleName);
158                 if (!Files.isDirectory(test)) {
159                     test = testOutputDirectory;
160                 }
161             }
162             Path source = directory.getModuleInfo().orElseThrow(); // Should never be absent for entries in the map.
163             entry.setValue(ModuleInfoOverwrite.create(source, main, test));
164         }
165     }
166 
167     /**
168      * Restores the hidden {@code module-info.class} files to their original names.
169      */
170     @Override
171     public void close() throws IOException {
172         IOException error = null;
173         for (Map.Entry<SourceDirectory, ModuleInfoOverwrite> entry : moduleInfos.entrySet()) {
174             ModuleInfoOverwrite mo = entry.getValue();
175             if (mo != null) {
176                 entry.setValue(null);
177                 try {
178                     mo.restore();
179                 } catch (IOException e) {
180                     if (error == null) {
181                         error = e;
182                     } else {
183                         error.addSuppressed(e);
184                     }
185                 }
186             }
187         }
188         if (error != null) {
189             throw error;
190         }
191     }
192 
193     /**
194      * {@return a string representation for debugging purposes}
195      */
196     @Override
197     public String toString() {
198         var sb = new StringBuilder(getClass().getSimpleName()).append('[');
199         if (release != null) {
200             sb.append(release).append(": ");
201         }
202         return sb.append(files.size()).append(" files]").toString();
203     }
204 }