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.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 }