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 }