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 }