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.surefire.api.util;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.UncheckedIOException;
24 import java.time.LocalDateTime;
25 import java.time.format.DateTimeFormatter;
26 import java.util.ArrayList;
27 import java.util.LinkedHashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.concurrent.atomic.AtomicInteger;
31
32 /**
33 * Management of temporary files in surefire with support for sub directories of the system's directory
34 * of temporary files.<br>
35 * The {@link File#createTempFile(String, String)} API creates rather meaningless file names and
36 * only in the system's temp directory.<br>
37 * This class creates temp files from a prefix, a unique date/timestamp, a short file id and suffix.
38 * It also helps you organize temporary files into sub-directories,
39 * and thus avoid bloating the temp directory root.<br>
40 * Apart from that, this class works in much the same way as {@link File#createTempFile(String, String)}
41 * and {@link File#deleteOnExit()}, and can be used as a drop-in replacement.
42 *
43 * @author Markus Spann
44 */
45 public final class TempFileManager {
46
47 private static final Map<File, TempFileManager> INSTANCES = new LinkedHashMap<>();
48 /** An id unique across all file managers used as part of the temporary file's base name. */
49 private static final AtomicInteger FILE_ID = new AtomicInteger(1);
50
51 private static final String SUFFIX_TMP = ".tmp";
52
53 private static Thread shutdownHook;
54
55 /** The temporary directory or sub-directory under which temporary files are created. */
56 private final File tempDir;
57 /** The fixed base name used between prefix and suffix of temporary files created by this class. */
58 private final String baseName;
59
60 /** List of successfully created temporary files. */
61 private final List<File> tempFiles;
62
63 /** Temporary files are delete on JVM exit if true. */
64 private boolean deleteOnExit;
65
66 private TempFileManager(File tempDir) {
67 this.tempDir = tempDir;
68 this.baseName = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").format(LocalDateTime.now());
69 this.tempFiles = new ArrayList<>();
70 }
71
72 private static File calcTempDir(String subDirName) {
73 String tempDirName = subDirName == null ? null : subDirName.trim().isEmpty() ? null : subDirName.trim();
74 File tempDirCandidate =
75 tempDirName == null ? new File(getJavaIoTmpDir()) : new File(getJavaIoTmpDir(), tempDirName);
76 return tempDirCandidate;
77 }
78
79 public static TempFileManager instance() {
80 return instance((File) null);
81 }
82
83 /**
84 * Creates an instance using a subdirectory of the system's temporary directory.
85 * @param subDirName name of subdirectory
86 * @return instance
87 */
88 public static TempFileManager instance(String subDirName) {
89 return instance(calcTempDir(subDirName));
90 }
91
92 public static synchronized TempFileManager instance(File tempDir) {
93
94 // on creation of the first instance, register a shutdown hook to delete temporary files of all managers
95 if (shutdownHook == null) {
96 ThreadGroup tg = Thread.currentThread().getThreadGroup();
97 while (tg.getParent() != null) {
98 tg = tg.getParent();
99 }
100 shutdownHook = new Thread(
101 tg, TempFileManager::shutdownAll, TempFileManager.class.getSimpleName() + "-ShutdownHook");
102 Runtime.getRuntime().addShutdownHook(shutdownHook);
103 }
104
105 return INSTANCES.computeIfAbsent(tempDir == null ? new File(getJavaIoTmpDir()) : tempDir, TempFileManager::new);
106 }
107
108 public synchronized void deleteAll() {
109 tempFiles.forEach(File::delete);
110 tempFiles.clear();
111 tempDir.delete();
112 }
113
114 static void shutdownAll() {
115 INSTANCES.values().stream().filter(TempFileManager::isDeleteOnExit).forEach(TempFileManager::deleteAll);
116 }
117
118 /**
119 * Returns the temporary directory or sub-directory under which temporary files are created.
120 *
121 * @return temporary directory
122 */
123 public File getTempDir() {
124 return tempDir;
125 }
126
127 /**
128 * Creates a new, uniquely-named temporary file in this object's {@link #tempDir}
129 * with user-defined prefix and suffix (both may be null or empty and won't be trimmed).
130 * <p>
131 * This method behaves similarly to {@link java.io.File#createTempFile(String, String)} and
132 * may be used as a drop-in replacement.<br>
133 * This method is {@code synchronized} to help ensure uniqueness of temporary files.
134 *
135 * @param prefix optional file name prefix
136 * @param suffix optional file name suffix
137 * @return file object
138 */
139 public synchronized File createTempFile(String prefix, String suffix) {
140 // use only the file name from the supplied prefix
141 prefix = prefix == null ? "" : (new File(prefix)).getName();
142 suffix = suffix == null ? "" : suffix;
143
144 String name = String.join("-", prefix, baseName + "_" + FILE_ID.getAndIncrement()) + suffix;
145 File tempFile = new File(tempDir, name);
146 if (!name.equals(tempFile.getName())) {
147 throw new UncheckedIOException(new IOException("Unable to create temporary file " + tempFile));
148 }
149
150 if (tempFile.exists() || tempFiles.contains(tempFile)) {
151 // temporary file already exists? Try another name
152 return createTempFile(prefix, suffix);
153 } else {
154 // create temporary directory
155 if (!tempDir.exists()) {
156 if (!tempDir.mkdirs()) {
157 throw new UncheckedIOException(
158 new IOException("Unable to create temporary directory " + tempDir.getAbsolutePath()));
159 }
160 }
161
162 try {
163 tempFile.createNewFile();
164 } catch (IOException ex) {
165 throw new UncheckedIOException("Unable to create temporary file " + tempFile.getAbsolutePath(), ex);
166 }
167
168 tempFiles.add(tempFile);
169 return tempFile;
170 }
171 }
172
173 public File createTempFile(String prefix) {
174 return createTempFile(prefix, SUFFIX_TMP);
175 }
176
177 public boolean isDeleteOnExit() {
178 return deleteOnExit;
179 }
180
181 /**
182 * Instructs this file manager to delete its temporary files during JVM shutdown.<br>
183 * This status can be turned on and off unlike {@link File#deleteOnExit()}.
184 *
185 * @param deleteOnExit delete the file in a shutdown hook on JVM exit
186 */
187 public void setDeleteOnExit(boolean deleteOnExit) {
188 this.deleteOnExit = deleteOnExit;
189 }
190
191 @Override
192 public String toString() {
193 return String.format(
194 "%s[tempDir=%s, deleteOnExit=%s, baseName=%s, tempFiles=%d]",
195 getClass().getSimpleName(), tempDir, deleteOnExit, baseName, tempFiles.size());
196 }
197
198 /**
199 * Returns the value of system property {@code java.io.tmpdir}, the system's temp directory.
200 *
201 * @return path to system temp directory
202 */
203 public static String getJavaIoTmpDir() {
204 String tmpDir = System.getProperty("java.io.tmpdir");
205 if (!tmpDir.endsWith(File.separator)) {
206 tmpDir += File.separatorChar;
207 }
208 return tmpDir;
209 }
210 }