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