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 javax.inject.Named;
022import javax.inject.Singleton;
023
024import java.io.ByteArrayOutputStream;
025import java.io.File;
026import java.io.IOException;
027import java.io.UncheckedIOException;
028import java.nio.ByteBuffer;
029import java.nio.channels.Channels;
030import java.nio.channels.FileChannel;
031import java.nio.channels.FileLock;
032import java.nio.channels.OverlappingFileLockException;
033import java.nio.file.Files;
034import java.nio.file.NoSuchFileException;
035import java.nio.file.Path;
036import java.nio.file.StandardOpenOption;
037import java.util.Map;
038import java.util.Properties;
039
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043/**
044 * Manages access to a properties file.
045 * <p>
046 * Note: the file locking in this component (that predates {@link org.eclipse.aether.SyncContext}) is present only
047 * to back off two parallel implementations that coexist in Maven (this class and {@code maven-compat} one), as in
048 * certain cases the two implementations may collide on properties files. This locking must remain in place for as long
049 * as {@code maven-compat} code exists.
050 *
051 * <em>IMPORTANT:</em> This class is kept fully in sync with the master branch one (w/ simple change to convert File
052 * to Path instances).
053 */
054@Singleton
055@Named
056public final class DefaultTrackingFileManager implements TrackingFileManager {
057    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTrackingFileManager.class);
058
059    @Override
060    public Properties read(File file) {
061        Path path = file.toPath();
062        if (Files.isReadable(path)) {
063            synchronized (mutex(path)) {
064                try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
065                        FileLock unused = fileLock(fileChannel, true)) {
066                    Properties props = new Properties();
067                    props.load(Channels.newInputStream(fileChannel));
068                    return props;
069                } catch (NoSuchFileException e) {
070                    LOGGER.debug("No such file to read {}: {}", path, e.getMessage());
071                } catch (IOException e) {
072                    LOGGER.warn("Failed to read tracking file '{}'", path, e);
073                    throw new UncheckedIOException(e);
074                }
075            }
076        }
077        return null;
078    }
079
080    @Override
081    public Properties update(File file, Map<String, String> updates) {
082        Path path = file.toPath();
083        try {
084            Files.createDirectories(path.getParent());
085        } catch (IOException e) {
086            LOGGER.warn("Failed to create tracking file parent '{}'", path, e);
087            throw new UncheckedIOException(e);
088        }
089        synchronized (mutex(path)) {
090            try (FileChannel fileChannel = FileChannel.open(
091                            path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
092                    FileLock unused = fileLock(fileChannel, false)) {
093                Properties props = new Properties();
094                if (fileChannel.size() > 0) {
095                    props.load(Channels.newInputStream(fileChannel));
096                }
097
098                for (Map.Entry<String, String> update : updates.entrySet()) {
099                    if (update.getValue() == null) {
100                        props.remove(update.getKey());
101                    } else {
102                        props.setProperty(update.getKey(), update.getValue());
103                    }
104                }
105
106                LOGGER.debug("Writing tracking file '{}'", path);
107                ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2);
108                props.store(
109                        stream,
110                        "NOTE: This is a Maven Resolver internal implementation file"
111                                + ", its format can be changed without prior notice.");
112                fileChannel.position(0);
113                int written = fileChannel.write(ByteBuffer.wrap(stream.toByteArray()));
114                fileChannel.truncate(written);
115                return props;
116            } catch (IOException e) {
117                LOGGER.warn("Failed to write tracking file '{}'", path, e);
118                throw new UncheckedIOException(e);
119            }
120        }
121    }
122
123    @Override
124    public boolean delete(File file) {
125        Path path = file.toPath();
126        if (Files.isReadable(path)) {
127            synchronized (mutex(path)) {
128                try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.WRITE);
129                        FileLock unused = fileLock(fileChannel, false)) {
130                    Files.delete(path);
131                    return true;
132                } catch (NoSuchFileException e) {
133                    LOGGER.debug("No such file to delete {}: {}", path, e.getMessage());
134                } catch (IOException e) {
135                    LOGGER.warn("Failed to delete tracking file '{}'", path, e);
136                    throw new UncheckedIOException(e);
137                }
138            }
139        }
140        return false;
141    }
142
143    private Object mutex(Path path) {
144        // The interned string of path is (mis)used as mutex, to exclude different threads going for same file,
145        // as JVM file locking happens on JVM not on Thread level. This is how original code did it  ¯\_(ツ)_/¯
146        return path.toAbsolutePath().normalize().toString().intern();
147    }
148
149    private FileLock fileLock(FileChannel channel, boolean shared) throws IOException {
150        FileLock lock = null;
151        for (int attempts = 8; attempts >= 0; attempts--) {
152            try {
153                lock = channel.lock(0, Long.MAX_VALUE, shared);
154                break;
155            } catch (OverlappingFileLockException e) {
156                if (attempts <= 0) {
157                    throw new IOException(e);
158                }
159                try {
160                    Thread.sleep(50L);
161                } catch (InterruptedException e1) {
162                    Thread.currentThread().interrupt();
163                }
164            }
165        }
166        if (lock == null) {
167            throw new IOException("Could not lock file");
168        }
169        return lock;
170    }
171}