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 java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.io.UncheckedIOException;
26  import java.nio.file.Files;
27  import java.nio.file.NoSuchFileException;
28  import java.nio.file.Path;
29  import java.nio.file.StandardOpenOption;
30  import java.util.Map;
31  import java.util.Properties;
32  import java.util.concurrent.TimeUnit;
33  
34  import org.eclipse.aether.named.NamedLock;
35  import org.eclipse.aether.named.NamedLockFactory;
36  import org.eclipse.aether.named.NamedLockKey;
37  import org.eclipse.aether.util.FileUtils;
38  import org.eclipse.aether.util.StringDigestUtil;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  /**
43   * Manages access to a properties file using named locks.
44   * <p>
45   * This implementation uses {@link NamedLock} to protect tracking files from concurrent access, and can be used
46   * when it is single "modern" Maven version used to access same local repository concurrently.
47   *
48   * @since 2.0.17
49   * @see LegacyTrackingFileManager
50   * @see TrackingFileManagerSupplier
51   */
52  public final class NamedLocksTrackingFileManager implements TrackingFileManager {
53      private static final Logger LOGGER = LoggerFactory.getLogger(NamedLocksTrackingFileManager.class);
54  
55      private final NamedLockFactory namedLockFactory;
56      private final long time;
57      private final TimeUnit unit;
58  
59      public NamedLocksTrackingFileManager(NamedLockFactory namedLockFactory, long time, TimeUnit unit) {
60          this.namedLockFactory = namedLockFactory;
61          this.time = time;
62          this.unit = unit;
63      }
64  
65      @Deprecated
66      @Override
67      public Properties read(File file) {
68          return read(file.toPath());
69      }
70  
71      @Override
72      public Properties read(Path path) {
73          try (NamedLock namedLock = namedLock(path)) {
74              if (namedLock.lockShared(time, unit)) {
75                  try {
76                      Properties props = new Properties();
77                      try (InputStream in = Files.newInputStream(path)) {
78                          props.load(in);
79                      }
80                      return props;
81                  } catch (NoSuchFileException e) {
82                      LOGGER.debug("No such file to read {}: {}", path, e.getMessage());
83                      return null;
84                  } catch (IOException e) {
85                      LOGGER.warn("Failed to read tracking file '{}'", path, e);
86                      throw new UncheckedIOException(e);
87                  } finally {
88                      namedLock.unlock();
89                  }
90              }
91              throw new IllegalStateException("Failed to lock for read the tracking file " + path);
92          } catch (InterruptedException e) {
93              Thread.currentThread().interrupt();
94              throw new IllegalStateException("Interrupted while reading tracking file " + path, e);
95          }
96      }
97  
98      @Deprecated
99      @Override
100     public Properties update(File file, Map<String, String> updates) {
101         return update(file.toPath(), updates);
102     }
103 
104     @Override
105     public Properties update(Path path, Map<String, String> updates) {
106         try {
107             Path parent = path.getParent();
108             if (parent != null) {
109                 Files.createDirectories(parent);
110             }
111         } catch (IOException e) {
112             LOGGER.warn("Failed to create tracking file parent '{}'", path, e);
113             throw new UncheckedIOException(e);
114         }
115         try (NamedLock lock = namedLock(path)) {
116             if (lock.lockExclusively(time, unit)) {
117                 try {
118                     Properties props = new Properties();
119                     if (Files.isRegularFile(path)) {
120                         try (InputStream stream = Files.newInputStream(path, StandardOpenOption.READ)) {
121                             props.load(stream);
122                         }
123                     }
124                     for (Map.Entry<String, String> update : updates.entrySet()) {
125                         if (update.getValue() == null) {
126                             props.remove(update.getKey());
127                         } else {
128                             props.setProperty(update.getKey(), update.getValue());
129                         }
130                     }
131                     LOGGER.debug("Writing tracking file '{}'", path);
132                     // write atomically: a truncated tracking file would make artifacts look untracked
133                     try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(path)) {
134                         try (OutputStream out = Files.newOutputStream(tempFile.getPath())) {
135                             props.store(
136                                     out,
137                                     "NOTE: This is a Maven Resolver internal implementation file"
138                                             + ", its format can be changed without prior notice.");
139                         }
140                         tempFile.move();
141                     }
142                     return props;
143                 } catch (IOException e) {
144                     LOGGER.warn("Failed to write tracking file '{}'", path, e);
145                     throw new UncheckedIOException(e);
146                 } finally {
147                     lock.unlock();
148                 }
149             }
150             throw new IllegalStateException("Failed to lock for update the tracking file " + path);
151         } catch (InterruptedException e) {
152             Thread.currentThread().interrupt();
153             throw new IllegalStateException("Interrupted while updating tracking file " + path, e);
154         }
155     }
156 
157     @Deprecated
158     @Override
159     public boolean delete(File file) {
160         return delete(file.toPath());
161     }
162 
163     @Override
164     public boolean delete(Path path) {
165         try (NamedLock lock = namedLock(path)) {
166             if (lock.lockExclusively(time, unit)) {
167                 try {
168                     return Files.deleteIfExists(path);
169                 } catch (NoSuchFileException e) {
170                     LOGGER.debug("No such file to delete {}: {}", path, e.getMessage());
171                     return false;
172                 } catch (IOException e) {
173                     LOGGER.warn("Failed to delete tracking file '{}'", path, e);
174                     throw new UncheckedIOException(e);
175                 } finally {
176                     lock.unlock();
177                 }
178             }
179             throw new IllegalStateException("Failed to lock for delete the tracking file " + path);
180         } catch (InterruptedException e) {
181             Thread.currentThread().interrupt();
182             throw new IllegalStateException("Interrupted while deleting tracking file " + path, e);
183         }
184     }
185 
186     /**
187      * Creates unique named lock for given path with name that is unique for paths, but carries some extra
188      * information useful for debugging.
189      * <p>
190      * Note: it is important that created named lock names remain (and carry) file friendly URLs, as this makes it
191      * work with all lock factories, even the file one. Using non-file friendly names would make it work it with
192      * all <strong>except the file lock factory</strong>.
193      */
194     private NamedLock namedLock(Path path) {
195         Path canonical = canonicalPath(path);
196         // Place lock file next to the tracking file, using its hash as filename
197         Path lockPath = canonical.resolveSibling("tracking-" + StringDigestUtil.sha1(canonical.toString()) + ".lock");
198         return namedLockFactory.getLock(
199                 NamedLockKey.of(lockPath.toAbsolutePath().toUri().toASCIIString(), path.toString()));
200     }
201 
202     /**
203      * Tries the best it can to figure out actual file the workload is about, while resolving cases like symlinked
204      * local repository etc.
205      */
206     private static Path canonicalPath(Path path) {
207         try {
208             return path.toRealPath();
209         } catch (IOException e) {
210             return path.getParent() != null
211                     ? canonicalPath(path.getParent()).resolve(path.getFileName())
212                     : path.toAbsolutePath();
213         }
214     }
215 }