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.eclipse.aether.util;
20  
21  import java.io.Closeable;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.nio.ByteBuffer;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.StandardCopyOption;
29  import java.util.concurrent.ThreadLocalRandom;
30  import java.util.concurrent.atomic.AtomicBoolean;
31  
32  import static java.util.Objects.requireNonNull;
33  
34  /**
35   * A utility class to write files.
36   *
37   * @since 1.9.0
38   */
39  public final class FileUtils {
40      /**
41       * Logic borrowed from Commons-Lang3: we really need only this, to decide do we NIO2 file ops or not.
42       * For some reason non-NIO2 works better on Windows.
43       */
44      private static final boolean IS_WINDOWS =
45              System.getProperty("os.name", "unknown").startsWith("Windows");
46  
47      /**
48       * Escape hatch if atomic move is not desired on system we run on.
49       *
50       * @since 2.0.12
51       */
52      private static final boolean ATOMIC_MOVE =
53              Boolean.parseBoolean(System.getProperty(FileUtils.class.getName() + "ATOMIC_MOVE", "true"));
54  
55      private FileUtils() {
56          // hide constructor
57      }
58      /**
59       * A temporary file, that is removed when closed.
60       */
61      public interface TempFile extends Closeable {
62          /**
63           * Returns the path of the created temp file.
64           */
65          Path getPath();
66      }
67  
68      /**
69       * A collocated temporary file, that resides next to a "target" file, and is removed when closed.
70       */
71      public interface CollocatedTempFile extends TempFile {
72          /**
73           * Upon close, atomically moves temp file to target file it is collocated with overwriting target (if exists).
74           * Invocation of this method merely signals that caller ultimately wants temp file to replace the target
75           * file, but when this method returns, the move operation did not yet happen, it will happen when this
76           * instance is closed.
77           * <p>
78           * Invoking this method <em>without writing to temp file</em> {@link #getPath()} (thus, not creating a temp
79           * file to be moved) is considered a bug, a mistake of the caller. Caller of this method should ensure
80           * that this method is invoked ONLY when the temp file is created and moving it to its final place is
81           * required.
82           */
83          void move() throws IOException;
84      }
85  
86      /**
87       * Creates a {@link TempFile} instance and backing temporary file on file system. It will be located in the default
88       * temporary-file directory. Returned instance should be handled in try-with-resource construct and created
89       * temp file is removed (if exists) when returned instance is closed.
90       * <p>
91       * This method uses {@link Files#createTempFile(String, String, java.nio.file.attribute.FileAttribute[])} to create
92       * the temporary file on file system.
93       */
94      public static TempFile newTempFile() throws IOException {
95          Path tempFile = Files.createTempFile("resolver", "tmp");
96          return new TempFile() {
97              @Override
98              public Path getPath() {
99                  return tempFile;
100             }
101 
102             @Override
103             public void close() throws IOException {
104                 Files.deleteIfExists(tempFile);
105             }
106         };
107     }
108 
109     /**
110      * Creates a {@link CollocatedTempFile} instance for given file without backing file. The path will be located in
111      * same directory where given file is, and will reuse its name for generated (randomized) name. Returned instance
112      * should be handled in try-with-resource and created temp path is removed (if exists) when returned instance is
113      * closed. The {@link CollocatedTempFile#move()} makes possible to atomically replace passed in file with the
114      * processed content written into a file backing the {@link CollocatedTempFile} instance.
115      * <p>
116      * The {@code file} nor it's parent directories have to exist. The parent directories are created if needed.
117      * <p>
118      * This method uses {@link Path#resolve(String)} to create the temporary file path in passed in file parent
119      * directory, but it does NOT create backing file on file system.
120      */
121     public static CollocatedTempFile newTempFile(Path file) throws IOException {
122         Path parent = requireNonNull(file.getParent(), "file must have parent");
123         Files.createDirectories(parent);
124         Path tempFile = parent.resolve(file.getFileName() + "."
125                 + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
126         return new CollocatedTempFile() {
127             private final AtomicBoolean wantsMove = new AtomicBoolean(false);
128             private final StandardCopyOption copyOption =
129                     FileUtils.ATOMIC_MOVE ? StandardCopyOption.ATOMIC_MOVE : StandardCopyOption.REPLACE_EXISTING;
130 
131             @Override
132             public Path getPath() {
133                 return tempFile;
134             }
135 
136             @Override
137             public void move() {
138                 wantsMove.set(true);
139             }
140 
141             @Override
142             public void close() throws IOException {
143                 if (wantsMove.get()) {
144                     if (IS_WINDOWS) {
145                         copy(tempFile, file);
146                     } else {
147                         Files.move(tempFile, file, copyOption);
148                     }
149                 }
150                 Files.deleteIfExists(tempFile);
151             }
152         };
153     }
154 
155     /**
156      * On Windows we use pre-NIO2 way to copy files, as for some reason it works. Beat me why.
157      */
158     private static void copy(Path source, Path target) throws IOException {
159         ByteBuffer buffer = ByteBuffer.allocate(1024 * 32);
160         byte[] array = buffer.array();
161         try (InputStream is = Files.newInputStream(source);
162                 OutputStream os = Files.newOutputStream(target)) {
163             while (true) {
164                 int bytes = is.read(array);
165                 if (bytes < 0) {
166                     break;
167                 }
168                 os.write(array, 0, bytes);
169             }
170         }
171     }
172 
173     /**
174      * A file writer, that accepts a {@link Path} to write some content to. Note: the file denoted by path may exist,
175      * hence implementation have to ensure it is able to achieve its goal ("replace existing" option or equivalent
176      * should be used).
177      */
178     @FunctionalInterface
179     public interface FileWriter {
180         void write(Path path) throws IOException;
181     }
182 
183     /**
184      * Writes file without backup.
185      *
186      * @param target that is the target file (must be file, the path must have parent)
187      * @param writer the writer that will accept a {@link Path} to write content to
188      * @throws IOException if at any step IO problem occurs
189      */
190     public static void writeFile(Path target, FileWriter writer) throws IOException {
191         writeFile(target, writer, false);
192     }
193 
194     /**
195      * Writes file with backup copy (appends ".bak" extension).
196      *
197      * @param target that is the target file (must be file, the path must have parent)
198      * @param writer the writer that will accept a {@link Path} to write content to
199      * @throws IOException if at any step IO problem occurs
200      */
201     public static void writeFileWithBackup(Path target, FileWriter writer) throws IOException {
202         writeFile(target, writer, true);
203     }
204 
205     /**
206      * Utility method to write out file to disk in "atomic" manner, with optional backups (".bak") if needed. This
207      * ensures that no other thread or process will be able to read not fully written files. Finally, this method
208      * may create the needed parent directories, if the passed in target parents does not exist.
209      *
210      * @param target   that is the target file (must be an existing or non-existing file, the path must have parent)
211      * @param writer   the writer that will accept a {@link Path} to write content to
212      * @param doBackup if {@code true}, and target file is about to be overwritten, a ".bak" file with old contents will
213      *                 be created/overwritten
214      * @throws IOException if at any step IO problem occurs
215      */
216     private static void writeFile(Path target, FileWriter writer, boolean doBackup) throws IOException {
217         requireNonNull(target, "target is null");
218         requireNonNull(writer, "writer is null");
219         Path parent = requireNonNull(target.getParent(), "target must have parent");
220 
221         try (CollocatedTempFile tempFile = newTempFile(target)) {
222             writer.write(tempFile.getPath());
223             if (doBackup && Files.isRegularFile(target)) {
224                 Files.copy(target, parent.resolve(target.getFileName() + ".bak"), StandardCopyOption.REPLACE_EXISTING);
225             }
226             tempFile.move();
227         }
228     }
229 }