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.ByteArrayInputStream;
25  import java.io.ByteArrayOutputStream;
26  import java.io.File;
27  import java.io.FileInputStream;
28  import java.io.IOException;
29  import java.io.RandomAccessFile;
30  import java.io.UncheckedIOException;
31  import java.nio.channels.FileChannel;
32  import java.nio.channels.FileLock;
33  import java.nio.channels.OverlappingFileLockException;
34  import java.nio.file.Files;
35  import java.util.Map;
36  import java.util.Properties;
37  
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  /**
42   * Manages access to a properties file.
43   * <p>
44   * Note: the file locking in this component (that predates {@link org.eclipse.aether.SyncContext}) is present only
45   * to back off two parallel implementations that coexist in Maven (this class and {@code maven-compat} one), as in
46   * certain cases the two implementations may collide on properties files. This locking must remain in place for as long
47   * as {@code maven-compat} code exists.
48   */
49  @Singleton
50  @Named
51  public final class DefaultTrackingFileManager implements TrackingFileManager {
52      private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTrackingFileManager.class);
53  
54      @Override
55      public Properties read(File file) {
56          if (Files.isReadable(file.toPath())) {
57              synchronized (getMutex(file)) {
58                  try (FileInputStream stream = new FileInputStream(file);
59                          FileLock unused = fileLock(stream.getChannel(), Math.max(1, file.length()), true)) {
60                      Properties props = new Properties();
61                      props.load(stream);
62                      return props;
63                  } catch (IOException e) {
64                      LOGGER.warn("Failed to read tracking file '{}'", file, e);
65                      throw new UncheckedIOException(e);
66                  }
67              }
68          }
69          return null;
70      }
71  
72      @Override
73      public Properties update(File file, Map<String, String> updates) {
74          Properties props = new Properties();
75  
76          try {
77              Files.createDirectories(file.getParentFile().toPath());
78          } catch (IOException e) {
79              LOGGER.warn("Failed to create tracking file parent '{}'", file, e);
80              throw new UncheckedIOException(e);
81          }
82  
83          synchronized (getMutex(file)) {
84              try (RandomAccessFile raf = new RandomAccessFile(file, "rw");
85                      FileLock unused = fileLock(raf.getChannel(), Math.max(1, raf.length()), false)) {
86                  if (raf.length() > 0) {
87                      byte[] buffer = new byte[(int) raf.length()];
88                      raf.readFully(buffer);
89                      props.load(new ByteArrayInputStream(buffer));
90                  }
91  
92                  for (Map.Entry<String, String> update : updates.entrySet()) {
93                      if (update.getValue() == null) {
94                          props.remove(update.getKey());
95                      } else {
96                          props.setProperty(update.getKey(), update.getValue());
97                      }
98                  }
99  
100                 LOGGER.debug("Writing tracking file '{}'", file);
101                 ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2);
102                 props.store(
103                         stream,
104                         "NOTE: This is a Maven Resolver internal implementation file"
105                                 + ", its format can be changed without prior notice.");
106                 raf.seek(0L);
107                 raf.write(stream.toByteArray());
108                 raf.setLength(raf.getFilePointer());
109             } catch (IOException e) {
110                 LOGGER.warn("Failed to write tracking file '{}'", file, e);
111                 throw new UncheckedIOException(e);
112             }
113         }
114 
115         return props;
116     }
117 
118     private Object getMutex(File file) {
119         // The interned string of path is (mis)used as mutex, to exclude different threads going for same file,
120         // as JVM file locking happens on JVM not on Thread level. This is how original code did it  ¯\_(ツ)_/¯
121         /*
122          * NOTE: Locks held by one JVM must not overlap and using the canonical path is our best bet, still another
123          * piece of code might have locked the same file (unlikely though) or the canonical path fails to capture file
124          * identity sufficiently as is the case with Java 1.6 and symlinks on Windows.
125          */
126         try {
127             return file.getCanonicalPath().intern();
128         } catch (IOException e) {
129             LOGGER.warn("Failed to canonicalize path {}", file, e);
130             // TODO This is code smell and deprecated
131             return file.getAbsolutePath().intern();
132         }
133     }
134 
135     @SuppressWarnings({"checkstyle:magicnumber"})
136     private FileLock fileLock(FileChannel channel, long size, boolean shared) throws IOException {
137         FileLock lock = null;
138         for (int attempts = 8; attempts >= 0; attempts--) {
139             try {
140                 lock = channel.lock(0, size, shared);
141                 break;
142             } catch (OverlappingFileLockException e) {
143                 if (attempts <= 0) {
144                     throw new IOException(e);
145                 }
146                 try {
147                     Thread.sleep(50L);
148                 } catch (InterruptedException e1) {
149                     Thread.currentThread().interrupt();
150                 }
151             }
152         }
153         if (lock == null) {
154             throw new IOException("Could not lock file");
155         }
156         return lock;
157     }
158 }