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.Path;
35  import java.nio.file.StandardOpenOption;
36  import java.util.Map;
37  import java.util.Properties;
38  
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  /**
43   * Manages access to a properties file.
44   * <p>
45   * Note: the file locking in this component (that predates {@link org.eclipse.aether.SyncContext}) is present only
46   * to back off two parallel implementations that coexist in Maven (this class and {@code maven-compat} one), as in
47   * certain cases the two implementations may collide on properties files. This locking must remain in place for as long
48   * as {@code maven-compat} code exists.
49   */
50  @Singleton
51  @Named
52  public final class DefaultTrackingFileManager implements TrackingFileManager {
53      private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTrackingFileManager.class);
54  
55      @Deprecated
56      @Override
57      public Properties read(File file) {
58          return read(file.toPath());
59      }
60  
61      @Override
62      public Properties read(Path path) {
63          if (Files.isReadable(path)) {
64              synchronized (getMutex(path)) {
65                  try {
66                      long fileSize = Files.size(path);
67                      try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
68                              FileLock unused = fileLock(fileChannel, Math.max(1, fileSize), true)) {
69                          Properties props = new Properties();
70                          props.load(Channels.newInputStream(fileChannel));
71                          return props;
72                      }
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          Properties props = new Properties();
91          try {
92              Files.createDirectories(path.getParent());
93          } catch (IOException e) {
94              LOGGER.warn("Failed to create tracking file parent '{}'", path, e);
95              throw new UncheckedIOException(e);
96          }
97  
98          synchronized (getMutex(path)) {
99              try {
100                 long fileSize;
101                 try {
102                     fileSize = Files.size(path);
103                 } catch (IOException e) {
104                     fileSize = 0L;
105                 }
106                 try (FileChannel fileChannel = FileChannel.open(
107                                 path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
108                         FileLock unused = fileLock(fileChannel, Math.max(1, fileSize), false)) {
109                     if (fileSize > 0) {
110                         props.load(Channels.newInputStream(fileChannel));
111                     }
112 
113                     for (Map.Entry<String, String> update : updates.entrySet()) {
114                         if (update.getValue() == null) {
115                             props.remove(update.getKey());
116                         } else {
117                             props.setProperty(update.getKey(), update.getValue());
118                         }
119                     }
120 
121                     LOGGER.debug("Writing tracking file '{}'", path);
122                     ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2);
123                     props.store(
124                             stream,
125                             "NOTE: This is a Maven Resolver internal implementation file"
126                                     + ", its format can be changed without prior notice.");
127                     fileChannel.position(0);
128                     int written = fileChannel.write(ByteBuffer.wrap(stream.toByteArray()));
129                     fileChannel.truncate(written);
130                 }
131             } catch (IOException e) {
132                 LOGGER.warn("Failed to write tracking file '{}'", path, e);
133                 throw new UncheckedIOException(e);
134             }
135         }
136 
137         return props;
138     }
139 
140     private Object getMutex(Path path) {
141         // The interned string of path is (mis)used as mutex, to exclude different threads going for same file,
142         // as JVM file locking happens on JVM not on Thread level. This is how original code did it  ¯\_(ツ)_/¯
143         /*
144          * NOTE: Locks held by one JVM must not overlap and using the canonical path is our best bet, still another
145          * piece of code might have locked the same file (unlikely though) or the canonical path fails to capture file
146          * identity sufficiently as is the case with Java 1.6 and symlinks on Windows.
147          */
148         return path.toAbsolutePath().normalize().toString().intern();
149     }
150 
151     @SuppressWarnings({"checkstyle:magicnumber"})
152     private FileLock fileLock(FileChannel channel, long size, boolean shared) throws IOException {
153         FileLock lock = null;
154         for (int attempts = 8; attempts >= 0; attempts--) {
155             try {
156                 lock = channel.lock(0, size, shared);
157                 break;
158             } catch (OverlappingFileLockException e) {
159                 if (attempts <= 0) {
160                     throw new IOException(e);
161                 }
162                 try {
163                     Thread.sleep(50L);
164                 } catch (InterruptedException e1) {
165                     Thread.currentThread().interrupt();
166                 }
167             }
168         }
169         if (lock == null) {
170             throw new IOException("Could not lock file");
171         }
172         return lock;
173     }
174 }