001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.internal.impl; 020 021import java.io.File; 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.OutputStream; 025import java.io.UncheckedIOException; 026import java.nio.file.Files; 027import java.nio.file.NoSuchFileException; 028import java.nio.file.Path; 029import java.nio.file.StandardOpenOption; 030import java.util.Map; 031import java.util.Properties; 032import java.util.concurrent.TimeUnit; 033 034import org.eclipse.aether.named.NamedLock; 035import org.eclipse.aether.named.NamedLockFactory; 036import org.eclipse.aether.named.NamedLockKey; 037import org.eclipse.aether.util.FileUtils; 038import org.eclipse.aether.util.StringDigestUtil; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042/** 043 * Manages access to a properties file using named locks. 044 * <p> 045 * This implementation uses {@link NamedLock} to protect tracking files from concurrent access, and can be used 046 * when it is single "modern" Maven version used to access same local repository concurrently. 047 * 048 * @since 2.0.17 049 * @see LegacyTrackingFileManager 050 * @see TrackingFileManagerSupplier 051 */ 052public final class NamedLocksTrackingFileManager implements TrackingFileManager { 053 private static final Logger LOGGER = LoggerFactory.getLogger(NamedLocksTrackingFileManager.class); 054 055 private final NamedLockFactory namedLockFactory; 056 private final long time; 057 private final TimeUnit unit; 058 059 public NamedLocksTrackingFileManager(NamedLockFactory namedLockFactory, long time, TimeUnit unit) { 060 this.namedLockFactory = namedLockFactory; 061 this.time = time; 062 this.unit = unit; 063 } 064 065 @Deprecated 066 @Override 067 public Properties read(File file) { 068 return read(file.toPath()); 069 } 070 071 @Override 072 public Properties read(Path path) { 073 try (NamedLock namedLock = namedLock(path)) { 074 if (namedLock.lockShared(time, unit)) { 075 try { 076 Properties props = new Properties(); 077 try (InputStream in = Files.newInputStream(path)) { 078 props.load(in); 079 } 080 return props; 081 } catch (NoSuchFileException e) { 082 LOGGER.debug("No such file to read {}: {}", path, e.getMessage()); 083 return null; 084 } catch (IOException e) { 085 LOGGER.warn("Failed to read tracking file '{}'", path, e); 086 throw new UncheckedIOException(e); 087 } finally { 088 namedLock.unlock(); 089 } 090 } 091 throw new IllegalStateException("Failed to lock for read the tracking file " + path); 092 } catch (InterruptedException e) { 093 Thread.currentThread().interrupt(); 094 throw new IllegalStateException("Interrupted while reading tracking file " + path, e); 095 } 096 } 097 098 @Deprecated 099 @Override 100 public Properties update(File file, Map<String, String> updates) { 101 return update(file.toPath(), updates); 102 } 103 104 @Override 105 public Properties update(Path path, Map<String, String> updates) { 106 try { 107 Path parent = path.getParent(); 108 if (parent != null) { 109 Files.createDirectories(parent); 110 } 111 } catch (IOException e) { 112 LOGGER.warn("Failed to create tracking file parent '{}'", path, e); 113 throw new UncheckedIOException(e); 114 } 115 try (NamedLock lock = namedLock(path)) { 116 if (lock.lockExclusively(time, unit)) { 117 try { 118 Properties props = new Properties(); 119 if (Files.isRegularFile(path)) { 120 try (InputStream stream = Files.newInputStream(path, StandardOpenOption.READ)) { 121 props.load(stream); 122 } 123 } 124 for (Map.Entry<String, String> update : updates.entrySet()) { 125 if (update.getValue() == null) { 126 props.remove(update.getKey()); 127 } else { 128 props.setProperty(update.getKey(), update.getValue()); 129 } 130 } 131 LOGGER.debug("Writing tracking file '{}'", path); 132 // write atomically: a truncated tracking file would make artifacts look untracked 133 try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(path)) { 134 try (OutputStream out = Files.newOutputStream(tempFile.getPath())) { 135 props.store( 136 out, 137 "NOTE: This is a Maven Resolver internal implementation file" 138 + ", its format can be changed without prior notice."); 139 } 140 tempFile.move(); 141 } 142 return props; 143 } catch (IOException e) { 144 LOGGER.warn("Failed to write tracking file '{}'", path, e); 145 throw new UncheckedIOException(e); 146 } finally { 147 lock.unlock(); 148 } 149 } 150 throw new IllegalStateException("Failed to lock for update the tracking file " + path); 151 } catch (InterruptedException e) { 152 Thread.currentThread().interrupt(); 153 throw new IllegalStateException("Interrupted while updating tracking file " + path, e); 154 } 155 } 156 157 @Deprecated 158 @Override 159 public boolean delete(File file) { 160 return delete(file.toPath()); 161 } 162 163 @Override 164 public boolean delete(Path path) { 165 try (NamedLock lock = namedLock(path)) { 166 if (lock.lockExclusively(time, unit)) { 167 try { 168 return Files.deleteIfExists(path); 169 } catch (NoSuchFileException e) { 170 LOGGER.debug("No such file to delete {}: {}", path, e.getMessage()); 171 return false; 172 } catch (IOException e) { 173 LOGGER.warn("Failed to delete tracking file '{}'", path, e); 174 throw new UncheckedIOException(e); 175 } finally { 176 lock.unlock(); 177 } 178 } 179 throw new IllegalStateException("Failed to lock for delete the tracking file " + path); 180 } catch (InterruptedException e) { 181 Thread.currentThread().interrupt(); 182 throw new IllegalStateException("Interrupted while deleting tracking file " + path, e); 183 } 184 } 185 186 /** 187 * Creates unique named lock for given path with name that is unique for paths, but carries some extra 188 * information useful for debugging. 189 * <p> 190 * Note: it is important that created named lock names remain (and carry) file friendly URLs, as this makes it 191 * work with all lock factories, even the file one. Using non-file friendly names would make it work it with 192 * all <strong>except the file lock factory</strong>. 193 */ 194 private NamedLock namedLock(Path path) { 195 Path canonical = canonicalPath(path); 196 // Place lock file next to the tracking file, using its hash as filename 197 Path lockPath = canonical.resolveSibling("tracking-" + StringDigestUtil.sha1(canonical.toString()) + ".lock"); 198 return namedLockFactory.getLock( 199 NamedLockKey.of(lockPath.toAbsolutePath().toUri().toASCIIString(), path.toString())); 200 } 201 202 /** 203 * Tries the best it can to figure out actual file the workload is about, while resolving cases like symlinked 204 * local repository etc. 205 */ 206 private static Path canonicalPath(Path path) { 207 try { 208 return path.toRealPath(); 209 } catch (IOException e) { 210 return path.getParent() != null 211 ? canonicalPath(path.getParent()).resolve(path.getFileName()) 212 : path.toAbsolutePath(); 213 } 214 } 215}