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.spi.io;
20  
21  import java.io.BufferedInputStream;
22  import java.io.BufferedOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.nio.ByteBuffer;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.AccessDeniedException;
29  import java.nio.file.FileSystemException;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.nio.file.StandardCopyOption;
33  import java.nio.file.attribute.FileTime;
34  import java.util.concurrent.ThreadLocalRandom;
35  import java.util.concurrent.atomic.AtomicBoolean;
36  
37  import static java.util.Objects.requireNonNull;
38  
39  /**
40   * Utility class serving as base of {@link PathProcessor} implementations. This class can be extended or replaced
41   * (as component) when needed. Also, this class is published in Resolver implementation for path processor interface.
42   *
43   * @since 2.0.13
44   */
45  public class PathProcessorSupport implements PathProcessor {
46      /**
47       * Logic borrowed from Commons-Lang3: we really need only this, to decide do we NIO2 file ops or not.
48       * For some reason non-NIO2 works better on Windows.
49       */
50      protected static final boolean IS_WINDOWS =
51              System.getProperty("os.name", "unknown").startsWith("Windows");
52  
53      /**
54       * Escape hatch if atomic move is not desired on system we run on.
55       */
56      protected static final boolean ATOMIC_MOVE =
57              Boolean.parseBoolean(System.getProperty(PathProcessor.class.getName() + "ATOMIC_MOVE", "true"));
58  
59      @Override
60      public boolean setLastModified(Path path, long value) throws IOException {
61          try {
62              Files.setLastModifiedTime(path, FileTime.fromMillis(value));
63              return true;
64          } catch (FileSystemException e) {
65              // MRESOLVER-536: Java uses generic FileSystemException for some weird cases,
66              // but some subclasses like AccessDeniedEx should be re-thrown
67              if (e instanceof AccessDeniedException) {
68                  throw e;
69              }
70              return false;
71          }
72      }
73  
74      @Override
75      public void write(Path target, String data) throws IOException {
76          writeFile(target, p -> Files.write(p, data.getBytes(StandardCharsets.UTF_8)), false);
77      }
78  
79      @Override
80      public void write(Path target, InputStream source) throws IOException {
81          writeFile(target, p -> Files.copy(source, p, StandardCopyOption.REPLACE_EXISTING), false);
82      }
83  
84      @Override
85      public void writeWithBackup(Path target, String data) throws IOException {
86          writeFile(target, p -> Files.write(p, data.getBytes(StandardCharsets.UTF_8)), true);
87      }
88  
89      @Override
90      public void writeWithBackup(Path target, InputStream source) throws IOException {
91          writeFile(target, p -> Files.copy(source, p, StandardCopyOption.REPLACE_EXISTING), true);
92      }
93  
94      /**
95       * A file writer, that accepts a {@link Path} to write some content to. Note: the file denoted by path may exist,
96       * hence implementation have to ensure it is able to achieve its goal ("replace existing" option or equivalent
97       * should be used).
98       */
99      @FunctionalInterface
100     public interface FileWriter {
101         void write(Path path) throws IOException;
102     }
103 
104     /**
105      * Utility method to write out file to disk in "atomic" manner, with optional backups (".bak") if needed. This
106      * ensures that no other thread or process will be able to read not fully written files. Finally, this method
107      * may create the needed parent directories, if the passed in target parents does not exist.
108      *
109      * @param target   that is the target file (must be an existing or non-existing file, the path must have parent)
110      * @param writer   the writer that will accept a {@link Path} to write content to
111      * @param doBackup if {@code true}, and target file is about to be overwritten, a ".bak" file with old contents will
112      *                 be created/overwritten
113      * @throws IOException if at any step IO problem occurs
114      */
115     public void writeFile(Path target, FileWriter writer, boolean doBackup) throws IOException {
116         requireNonNull(target, "target is null");
117         requireNonNull(writer, "writer is null");
118         Path parent = requireNonNull(target.getParent(), "target must have parent");
119 
120         try (CollocatedTempFile tempFile = newTempFile(target)) {
121             writer.write(tempFile.getPath());
122             if (doBackup && Files.isRegularFile(target)) {
123                 Files.copy(target, parent.resolve(target.getFileName() + ".bak"), StandardCopyOption.REPLACE_EXISTING);
124             }
125             tempFile.move();
126         }
127     }
128 
129     @Override
130     public long copy(Path source, Path target, ProgressListener listener) throws IOException {
131         try (InputStream in = new BufferedInputStream(Files.newInputStream(source));
132                 CollocatedTempFile tempTarget = newTempFile(target);
133                 OutputStream out = new BufferedOutputStream(Files.newOutputStream(tempTarget.getPath()))) {
134             long result = copy(out, in, listener);
135             tempTarget.move();
136             return result;
137         }
138     }
139 
140     private long copy(OutputStream os, InputStream is, ProgressListener listener) throws IOException {
141         long total = 0L;
142         byte[] buffer = new byte[1024 * 32];
143         while (true) {
144             int bytes = is.read(buffer);
145             if (bytes < 0) {
146                 break;
147             }
148 
149             os.write(buffer, 0, bytes);
150 
151             total += bytes;
152 
153             if (listener != null && bytes > 0) {
154                 try {
155                     listener.progressed(ByteBuffer.wrap(buffer, 0, bytes));
156                 } catch (Exception e) {
157                     // too bad
158                 }
159             }
160         }
161 
162         return total;
163     }
164 
165     @Override
166     public void move(Path source, Path target) throws IOException {
167         final StandardCopyOption[] copyOption = ATOMIC_MOVE
168                 ? new StandardCopyOption[] {
169                     StandardCopyOption.ATOMIC_MOVE,
170                     StandardCopyOption.REPLACE_EXISTING,
171                     StandardCopyOption.COPY_ATTRIBUTES
172                 }
173                 : new StandardCopyOption[] {StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES};
174         if (IS_WINDOWS) {
175             classicCopy(source, target);
176         } else {
177             Files.move(source, target, copyOption);
178         }
179         Files.deleteIfExists(source);
180     }
181 
182     // Temp files
183 
184     @Override
185     public TempFile newTempFile() throws IOException {
186         Path tempFile = Files.createTempFile("resolver", "tmp");
187         return new TempFile() {
188             @Override
189             public Path getPath() {
190                 return tempFile;
191             }
192 
193             @Override
194             public void close() throws IOException {
195                 Files.deleteIfExists(tempFile);
196             }
197         };
198     }
199 
200     @Override
201     public CollocatedTempFile newTempFile(Path file) throws IOException {
202         Path parent = requireNonNull(file.getParent(), "file must have parent");
203         Files.createDirectories(parent);
204         Path tempFile = parent.resolve(file.getFileName() + "."
205                 + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
206         return new CollocatedTempFile() {
207             private final AtomicBoolean wantsMove = new AtomicBoolean(false);
208             private final StandardCopyOption[] copyOption = ATOMIC_MOVE
209                     ? new StandardCopyOption[] {StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING}
210                     : new StandardCopyOption[] {StandardCopyOption.REPLACE_EXISTING};
211 
212             @Override
213             public Path getPath() {
214                 return tempFile;
215             }
216 
217             @Override
218             public void move() {
219                 wantsMove.set(true);
220             }
221 
222             @Override
223             public void close() throws IOException {
224                 if (wantsMove.get()) {
225                     if (IS_WINDOWS) {
226                         classicCopy(tempFile, file);
227                     } else {
228                         Files.move(tempFile, file, copyOption);
229                     }
230                 }
231                 Files.deleteIfExists(tempFile);
232             }
233         };
234     }
235 
236     /**
237      * On Windows we use pre-NIO2 way to copy files, as for some reason it works. Beat me why.
238      */
239     protected void classicCopy(Path source, Path target) throws IOException {
240         ByteBuffer buffer = ByteBuffer.allocate(1024 * 32);
241         byte[] array = buffer.array();
242         try (InputStream is = Files.newInputStream(source);
243                 OutputStream os = Files.newOutputStream(target)) {
244             while (true) {
245                 int bytes = is.read(array);
246                 if (bytes < 0) {
247                     break;
248                 }
249                 os.write(array, 0, bytes);
250             }
251         }
252     }
253 }