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  @Singleton
52  @Named
53  public final class DefaultTrackingFileManager implements TrackingFileManager {
54      private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTrackingFileManager.class);
55  
56      @Deprecated
57      @Override
58      public Properties read(File file) {
59          return read(file.toPath());
60      }
61  
62      @Override
63      public Properties read(Path path) {
64          if (Files.isReadable(path)) {
65              synchronized (mutex(path)) {
66                  try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
67                          FileLock unused = fileLock(fileChannel, true)) {
68                      Properties props = new Properties();
69                      props.load(Channels.newInputStream(fileChannel));
70                      return props;
71                  } catch (NoSuchFileException e) {
72                      LOGGER.debug("No such file to read {}: {}", path, e.getMessage());
73                  } catch (IOException e) {
74                      LOGGER.warn("Failed to read tracking file '{}'", path, e);
75                      throw new UncheckedIOException(e);
76                  }
77              }
78          }
79          return null;
80      }
81  
82      @Deprecated
83      @Override
84      public Properties update(File file, Map<String, String> updates) {
85          return update(file.toPath(), updates);
86      }
87  
88      @Override
89      public Properties update(Path path, Map<String, String> updates) {
90          try {
91              Files.createDirectories(path.getParent());
92          } catch (IOException e) {
93              LOGGER.warn("Failed to create tracking file parent '{}'", path, e);
94              throw new UncheckedIOException(e);
95          }
96          synchronized (mutex(path)) {
97              try (FileChannel fileChannel = FileChannel.open(
98                              path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
99                      FileLock unused = fileLock(fileChannel, false)) {
100                 Properties props = new Properties();
101                 if (fileChannel.size() > 0) {
102                     props.load(Channels.newInputStream(fileChannel));
103                 }
104 
105                 for (Map.Entry<String, String> update : updates.entrySet()) {
106                     if (update.getValue() == null) {
107                         props.remove(update.getKey());
108                     } else {
109                         props.setProperty(update.getKey(), update.getValue());
110                     }
111                 }
112 
113                 LOGGER.debug("Writing tracking file '{}'", path);
114                 ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2);
115                 props.store(
116                         stream,
117                         "NOTE: This is a Maven Resolver internal implementation file"
118                                 + ", its format can be changed without prior notice.");
119                 fileChannel.position(0);
120                 int written = fileChannel.write(ByteBuffer.wrap(stream.toByteArray()));
121                 fileChannel.truncate(written);
122                 return props;
123             } catch (IOException e) {
124                 LOGGER.warn("Failed to write tracking file '{}'", path, e);
125                 throw new UncheckedIOException(e);
126             }
127         }
128     }
129 
130     @Override
131     public boolean delete(File file) {
132         return delete(file.toPath());
133     }
134 
135     @Override
136     public boolean delete(Path path) {
137         if (Files.isReadable(path)) {
138             synchronized (mutex(path)) {
139                 try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.WRITE);
140                         FileLock unused = fileLock(fileChannel, false)) {
141                     Files.delete(path);
142                     return true;
143                 } catch (NoSuchFileException e) {
144                     LOGGER.debug("No such file to delete {}: {}", path, e.getMessage());
145                 } catch (IOException e) {
146                     LOGGER.warn("Failed to delete tracking file '{}'", path, e);
147                     throw new UncheckedIOException(e);
148                 }
149             }
150         }
151         return false;
152     }
153 
154     private Object mutex(Path path) {
155         return path.toAbsolutePath().normalize().toString().intern();
156     }
157 
158     private FileLock fileLock(FileChannel channel, boolean shared) throws IOException {
159         FileLock lock = null;
160         for (int attempts = 8; attempts >= 0; attempts--) {
161             try {
162                 lock = channel.lock(0, Long.MAX_VALUE, shared);
163                 break;
164             } catch (OverlappingFileLockException e) {
165                 if (attempts <= 0) {
166                     throw new IOException(e);
167                 }
168                 try {
169                     Thread.sleep(50L);
170                 } catch (InterruptedException e1) {
171                     Thread.currentThread().interrupt();
172                 }
173             }
174         }
175         if (lock == null) {
176             throw new IOException("Could not lock file");
177         }
178         return lock;
179     }
180 }