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.Collection;
29 import java.util.EnumMap;
30 import java.util.LinkedHashMap;
31 import java.util.LinkedHashSet;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35
36 /**
37 * Source files for a specific Java release. Instances of {@code SourcesForRelease} are created from
38 * a list of {@link SourceFile} after the sources have been filtered according include and exclude filters.
39 *
40 * @author Martin Desruisseaux
41 */
42 final class SourcesForRelease implements Closeable {
43 /**
44 * The release for this set of sources. For this class, the
45 * {@link SourceVersion#RELEASE_0} value means "no version".
46 */
47 final SourceVersion release;
48
49 /**
50 * All source files.
51 */
52 final List<Path> files;
53
54 /**
55 * The root directories for each module. Keys are module names.
56 * The empty string stands for no module.
57 */
58 final Map<String, Set<Path>> roots;
59
60 /**
61 * The directories that contains a {@code module-info.java} file. If the set of source files
62 * is for a Java release different than the base release, or if it is for the test sources,
63 * then a non-empty map means that some modules overwrite {@code module-info.class}.
64 */
65 private final Map<SourceDirectory, ModuleInfoOverwrite> moduleInfos;
66
67 /**
68 * Last directory added to the {@link #roots} map. This is a small optimization for reducing
69 * the number of accesses to the map. In most cases, only one element will be written there.
70 */
71 private SourceDirectory lastDirectoryAdded;
72
73 /**
74 * Creates an initially empty instance for the given Java release.
75 *
76 * @param release the release for this set of sources, or {@link SourceVersion#RELEASE_0} for no version.
77 */
78 private SourcesForRelease(SourceVersion release) {
79 this.release = release;
80 roots = new LinkedHashMap<>();
81 files = new ArrayList<>(256);
82 moduleInfos = new LinkedHashMap<>();
83 }
84
85 /**
86 * Adds the given source file to this collection of source files.
87 * The value of {@code source.directory.release} must be {@link #release}.
88 *
89 * @param source the source file to add.
90 */
91 private void add(SourceFile source) {
92 var directory = source.directory;
93 if (lastDirectoryAdded != directory) {
94 lastDirectoryAdded = directory;
95 String moduleName = directory.moduleName;
96 if (moduleName == null) {
97 moduleName = "";
98 }
99 roots.computeIfAbsent(moduleName, (key) -> new LinkedHashSet<>()).add(directory.root);
100 directory.getModuleInfo().ifPresent((path) -> moduleInfos.put(directory, null));
101 }
102 files.add(source.file);
103 }
104
105 /**
106 * Groups all sources files first by Java release versions, then by module names.
107 * The elements in the returned collection are sorted in the order of {@link SourceVersion}
108 * enumeration values. It should match the increasing order of Java releases.
109 *
110 * @param sources the sources to group.
111 * @return the given sources grouped by Java release versions and module names.
112 */
113 public static Collection<SourcesForRelease> groupByReleaseAndModule(List<SourceFile> sources) {
114 var result = new EnumMap<SourceVersion, SourcesForRelease>(SourceVersion.class);
115 for (SourceFile source : sources) {
116 SourceVersion release = source.directory.release;
117 if (release == null) {
118 release = SourceVersion.RELEASE_0; // No release sub-directory for the compiled classes.
119 }
120 result.computeIfAbsent(release, SourcesForRelease::new).add(source);
121 }
122 // TODO: add empty set for all modules present in a release but not in the next release.
123 return result.values();
124 }
125
126 /**
127 * If there is any {@code module-info.class} in the main classes that are overwritten by this set of sources,
128 * temporarily replace the main files by the test files. The {@link #close()} method must be invoked after
129 * this method for resetting the original state.
130 *
131 * <p>This method is invoked when the test files overwrite the {@code module-info.class} from the main files.
132 * This method should not be invoked during the compilation of main classes, as its behavior may be not well
133 * defined.</p>
134 */
135 void substituteModuleInfos(final Path mainOutputDirectory, final Path testOutputDirectory) throws IOException {
136 for (Map.Entry<SourceDirectory, ModuleInfoOverwrite> entry : moduleInfos.entrySet()) {
137 Path main = mainOutputDirectory;
138 Path test = testOutputDirectory;
139 SourceDirectory directory = entry.getKey();
140 String moduleName = directory.moduleName;
141 if (moduleName != null) {
142 main = main.resolve(moduleName);
143 if (!Files.isDirectory(main)) {
144 main = mainOutputDirectory;
145 }
146 test = test.resolve(moduleName);
147 if (!Files.isDirectory(test)) {
148 test = testOutputDirectory;
149 }
150 }
151 Path source = directory.getModuleInfo().orElseThrow(); // Should never be absent for entries in the map.
152 entry.setValue(ModuleInfoOverwrite.create(source, main, test));
153 }
154 }
155
156 /**
157 * Restores the hidden {@code module-info.class} files to their original names.
158 */
159 @Override
160 public void close() throws IOException {
161 IOException error = null;
162 for (Map.Entry<SourceDirectory, ModuleInfoOverwrite> entry : moduleInfos.entrySet()) {
163 ModuleInfoOverwrite mo = entry.getValue();
164 if (mo != null) {
165 entry.setValue(null);
166 try {
167 mo.restore();
168 } catch (IOException e) {
169 if (error == null) {
170 error = e;
171 } else {
172 error.addSuppressed(e);
173 }
174 }
175 }
176 }
177 if (error != null) {
178 throw error;
179 }
180 }
181 }