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.internal.impl;
20  
21  import javax.inject.Named;
22  import javax.inject.Singleton;
23  
24  import java.io.ByteArrayOutputStream;
25  import java.io.File;
26  import java.io.IOException;
27  import java.io.UncheckedIOException;
28  import java.nio.ByteBuffer;
29  import java.nio.channels.Channels;
30  import java.nio.channels.FileChannel;
31  import java.nio.channels.FileLock;
32  import java.nio.channels.OverlappingFileLockException;
33  import java.nio.file.Files;
34  import java.nio.file.NoSuchFileException;
35  import java.nio.file.Path;
36  import java.nio.file.StandardOpenOption;
37  import java.util.Map;
38  import java.util.Properties;
39  
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  /**
44   * Manages access to a properties file.
45   * <p>
46   * Note: the file locking in this component (that predates {@link org.eclipse.aether.SyncContext}) is present only
47   * to back off two parallel implementations that coexist in Maven (this class and {@code maven-compat} one), as in
48   * certain cases the two implementations may collide on properties files. This locking must remain in place for as long
49   * as {@code maven-compat} code exists.
50   *
51   * <em>IMPORTANT:</em> This class is kept fully in sync with the master branch one (w/ simple change to convert File
52   * to Path instances).
53   */
54  @Singleton
55  @Named
56  public final class DefaultTrackingFileManager implements TrackingFileManager {
57      private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTrackingFileManager.class);
58  
59      @Override
60      public Properties read(File file) {
61          Path path = file.toPath();
62          if (Files.isReadable(path)) {
63              synchronized (mutex(path)) {
64                  try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
65                          FileLock unused = fileLock(fileChannel, true)) {
66                      Properties props = new Properties();
67                      props.load(Channels.newInputStream(fileChannel));
68                      return props;
69                  } catch (NoSuchFileException e) {
70                      LOGGER.debug("No such file to read {}: {}", path, e.getMessage());
71                  } catch (IOException e) {
72                      LOGGER.warn("Failed to read tracking file '{}'", path, e);
73                      throw new UncheckedIOException(e);
74                  }
75              }
76          }
77          return null;
78      }
79  
80      @Override
81      public Properties update(File file, Map<String, String> updates) {
82          Path path = file.toPath();
83          try {
84              Files.createDirectories(path.getParent());
85          } catch (IOException e) {
86              LOGGER.warn("Failed to create tracking file parent '{}'", path, e);
87              throw new UncheckedIOException(e);
88          }
89          synchronized (mutex(path)) {
90              try (FileChannel fileChannel = FileChannel.open(
91                              path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
92                      FileLock unused = fileLock(fileChannel, false)) {
93                  Properties props = new Properties();
94                  if (fileChannel.size() > 0) {
95                      props.load(Channels.newInputStream(fileChannel));
96                  }
97  
98                  for (Map.Entry<String, String> update : updates.entrySet()) {
99                      if (update.getValue() == null) {
100                         props.remove(update.getKey());
101                     } else {
102                         props.setProperty(update.getKey(), update.getValue());
103                     }
104                 }
105 
106                 LOGGER.debug("Writing tracking file '{}'", path);
107                 ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2);
108                 props.store(
109                         stream,
110                         "NOTE: This is a Maven Resolver internal implementation file"
111                                 + ", its format can be changed without prior notice.");
112                 fileChannel.position(0);
113                 int written = fileChannel.write(ByteBuffer.wrap(stream.toByteArray()));
114                 fileChannel.truncate(written);
115                 return props;
116             } catch (IOException e) {
117                 LOGGER.warn("Failed to write tracking file '{}'", path, e);
118                 throw new UncheckedIOException(e);
119             }
120         }
121     }
122 
123     @Override
124     public boolean delete(File file) {
125         Path path = file.toPath();
126         if (Files.isReadable(path)) {
127             synchronized (mutex(path)) {
128                 try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.WRITE);
129                         FileLock unused = fileLock(fileChannel, false)) {
130                     Files.delete(path);
131                     return true;
132                 } catch (NoSuchFileException e) {
133                     LOGGER.debug("No such file to delete {}: {}", path, e.getMessage());
134                 } catch (IOException e) {
135                     LOGGER.warn("Failed to delete tracking file '{}'", path, e);
136                     throw new UncheckedIOException(e);
137                 }
138             }
139         }
140         return false;
141     }
142 
143     private Object mutex(Path path) {
144         // The interned string of path is (mis)used as mutex, to exclude different threads going for same file,
145         // as JVM file locking happens on JVM not on Thread level. This is how original code did it  ¯\_(ツ)_/¯
146         return path.toAbsolutePath().normalize().toString().intern();
147     }
148 
149     private FileLock fileLock(FileChannel channel, boolean shared) throws IOException {
150         FileLock lock = null;
151         for (int attempts = 8; attempts >= 0; attempts--) {
152             try {
153                 lock = channel.lock(0, Long.MAX_VALUE, shared);
154                 break;
155             } catch (OverlappingFileLockException e) {
156                 if (attempts <= 0) {
157                     throw new IOException(e);
158                 }
159                 try {
160                     Thread.sleep(50L);
161                 } catch (InterruptedException e1) {
162                     Thread.currentThread().interrupt();
163                 }
164             }
165         }
166         if (lock == null) {
167             throw new IOException("Could not lock file");
168         }
169         return lock;
170     }
171 }