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.nio.file.Path;
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      @Override
56      public Properties read(File file) {
57          Path filePath = file.toPath();
58          if (Files.isReadable(filePath)) {
59              synchronized (getMutex(filePath)) {
60                  try (FileInputStream stream = new FileInputStream(filePath.toFile());
61                          FileLock unused = fileLock(stream.getChannel(), Math.max(1, file.length()), true)) {
62                      Properties props = new Properties();
63                      props.load(stream);
64                      return props;
65                  } catch (IOException e) {
66                      LOGGER.warn("Failed to read tracking file '{}'", file, e);
67                      throw new UncheckedIOException(e);
68                  }
69              }
70          }
71          return null;
72      }
73  
74      @Override
75      public Properties update(File file, Map<String, String> updates) {
76          Path filePath = file.toPath();
77          Properties props = new Properties();
78  
79          try {
80              Files.createDirectories(filePath.getParent());
81          } catch (IOException e) {
82              LOGGER.warn("Failed to create tracking file parent '{}'", file, e);
83              throw new UncheckedIOException(e);
84          }
85  
86          synchronized (getMutex(filePath)) {
87              try (RandomAccessFile raf = new RandomAccessFile(filePath.toFile(), "rw");
88                      FileLock unused = fileLock(raf.getChannel(), Math.max(1, raf.length()), false)) {
89                  if (raf.length() > 0) {
90                      byte[] buffer = new byte[(int) raf.length()];
91                      raf.readFully(buffer);
92                      props.load(new ByteArrayInputStream(buffer));
93                  }
94  
95                  for (Map.Entry<String, String> update : updates.entrySet()) {
96                      if (update.getValue() == null) {
97                          props.remove(update.getKey());
98                      } else {
99                          props.setProperty(update.getKey(), update.getValue());
100                     }
101                 }
102 
103                 LOGGER.debug("Writing tracking file '{}'", file);
104                 ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2);
105                 props.store(
106                         stream,
107                         "NOTE: This is a Maven Resolver internal implementation file"
108                                 + ", its format can be changed without prior notice.");
109                 raf.seek(0L);
110                 raf.write(stream.toByteArray());
111                 raf.setLength(raf.getFilePointer());
112             } catch (IOException e) {
113                 LOGGER.warn("Failed to write tracking file '{}'", file, e);
114                 throw new UncheckedIOException(e);
115             }
116         }
117 
118         return props;
119     }
120 
121     private Object getMutex(Path file) {
122         /*
123          * NOTE: Locks held by one JVM must not overlap and using the canonical path is our best bet, still another
124          * piece of code might have locked the same file (unlikely though) or the canonical path fails to capture file
125          * identity sufficiently as is the case with Java 1.6 and symlinks on Windows.
126          */
127         try {
128             return file.toRealPath().toString().intern();
129         } catch (IOException e) {
130             LOGGER.warn("Failed to get real path {}", file, e);
131             return file.toAbsolutePath().toString().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 }