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}