001package org.eclipse.aether.util;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *  http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.io.Closeable;
023import java.io.IOException;
024import java.nio.file.Files;
025import java.nio.file.Path;
026import java.nio.file.StandardCopyOption;
027import java.util.concurrent.ThreadLocalRandom;
028
029import static java.util.Objects.requireNonNull;
030
031/**
032 * A utility class to write files.
033 *
034 * @since 1.9.0
035 */
036public final class FileUtils
037{
038    private FileUtils()
039    {
040        // hide constructor
041    }
042
043    /**
044     * A temporary file, that is removed when closed.
045     */
046    public interface TempFile extends Closeable
047    {
048        /**
049         * Returns the path of the created temp file.
050         */
051        Path getPath();
052    }
053
054    /**
055     * A collocated temporary file, that resides next to a "target" file, and is removed when closed.
056     */
057    public interface CollocatedTempFile extends TempFile
058    {
059        /**
060         * Atomically moves temp file to target file it is collocated with.
061         */
062        void move() throws IOException;
063    }
064
065    /**
066     * Creates a {@link TempFile} instance and backing temporary file on file system. It will be located in the default
067     * temporary-file directory. Returned instance should be handled in try-with-resource construct and created
068     * temp file is removed (if exists) when returned instance is closed.
069     * <p>
070     * This method uses {@link Files#createTempFile(String, String, java.nio.file.attribute.FileAttribute[])} to create
071     * the temporary file on file system.
072     */
073    public static TempFile newTempFile() throws IOException
074    {
075        Path tempFile = Files.createTempFile( "resolver", "tmp" );
076        return new TempFile()
077        {
078            @Override
079            public Path getPath()
080            {
081                return tempFile;
082            }
083
084            @Override
085            public void close() throws IOException
086            {
087                Files.deleteIfExists( tempFile );
088            }
089        };
090    }
091
092    /**
093     * Creates a {@link CollocatedTempFile} instance for given file without backing file. The path will be located in
094     * same directory where given file is, and will reuse its name for generated (randomized) name. Returned instance
095     * should be handled in try-with-resource and created temp path is removed (if exists) when returned instance is
096     * closed. The {@link CollocatedTempFile#move()} makes possible to atomically replace passed in file with the
097     * processed content written into a file backing the {@link CollocatedTempFile} instance.
098     * <p>
099     * The {@code file} nor it's parent directories have to exist. The parent directories are created if needed.
100     * <p>
101     * This method uses {@link Path#resolve(String)} to create the temporary file path in passed in file parent
102     * directory, but it does NOT create backing file on file system.
103     */
104    public static CollocatedTempFile newTempFile( Path file ) throws IOException
105    {
106        Path parent = requireNonNull( file.getParent(), "file must have parent" );
107        Files.createDirectories( parent );
108        Path tempFile = parent.resolve( file.getFileName() + "."
109                + Long.toUnsignedString( ThreadLocalRandom.current().nextLong() ) + ".tmp" );
110        return new CollocatedTempFile()
111        {
112            @Override
113            public Path getPath()
114            {
115                return tempFile;
116            }
117
118            @Override
119            public void move() throws IOException
120            {
121                Files.move( tempFile, file, StandardCopyOption.ATOMIC_MOVE );
122            }
123
124            @Override
125            public void close() throws IOException
126            {
127                Files.deleteIfExists( tempFile );
128            }
129        };
130    }
131
132    /**
133     * A file writer, that accepts a {@link Path} to write some content to. Note: the file denoted by path may exist,
134     * hence implementation have to ensure it is able to achieve its goal ("replace existing" option or equivalent
135     * should be used).
136     */
137    @FunctionalInterface
138    public interface FileWriter
139    {
140        void write( Path path ) throws IOException;
141    }
142
143    /**
144     * Writes file without backup.
145     *
146     * @param target that is the target file (must be file, the path must have parent).
147     * @param writer the writer that will accept a {@link Path} to write content to.
148     * @throws IOException if at any step IO problem occurs.
149     */
150    public static void writeFile( Path target, FileWriter writer ) throws IOException
151    {
152        writeFile( target, writer, false );
153    }
154
155    /**
156     * Writes file with backup copy (appends ".bak" extension).
157     *
158     * @param target that is the target file (must be file, the path must have parent).
159     * @param writer the writer that will accept a {@link Path} to write content to.
160     * @throws IOException if at any step IO problem occurs.
161     */
162    public static void writeFileWithBackup( Path target, FileWriter writer ) throws IOException
163    {
164        writeFile( target, writer, true );
165    }
166
167    /**
168     * Utility method to write out file to disk in "atomic" manner, with optional backups (".bak") if needed. This
169     * ensures that no other thread or process will be able to read not fully written files. Finally, this methos
170     * may create the needed parent directories, if the passed in target parents does not exist.
171     *
172     * @param target   that is the target file (must be an existing or non-existing file, the path must have parent).
173     * @param writer   the writer that will accept a {@link Path} to write content to.
174     * @param doBackup if {@code true}, and target file is about to be overwritten, a ".bak" file with old contents will
175     *                 be created/overwritten.
176     * @throws IOException if at any step IO problem occurs.
177     */
178    private static void writeFile( Path target, FileWriter writer, boolean doBackup ) throws IOException
179    {
180        requireNonNull( target, "target is null" );
181        requireNonNull( writer, "writer is null" );
182        Path parent = requireNonNull( target.getParent(), "target must have parent" );
183
184        try ( CollocatedTempFile tempFile = newTempFile( target ) )
185        {
186            writer.write( tempFile.getPath() );
187            if ( doBackup && Files.isRegularFile( target ) )
188            {
189                Files.copy( target, parent.resolve( target.getFileName() + ".bak" ),
190                        StandardCopyOption.REPLACE_EXISTING );
191            }
192            tempFile.move();
193        }
194    }
195}