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;
027
028import static java.util.Objects.requireNonNull;
029
030/**
031 * A utility class to write files.
032 *
033 * @since 1.9.0
034 */
035public final class FileUtils
036{
037    private FileUtils()
038    {
039        // hide constructor
040    }
041
042    /**
043     * A temporary file, that is removed when closed.
044     */
045    public interface TempFile extends Closeable
046    {
047        /**
048         * Returns the path of the created temp file.
049         */
050        Path getPath();
051    }
052
053    /**
054     * A collocated temporary file, that resides next to a "target" file, and is removed when closed.
055     */
056    public interface CollocatedTempFile extends TempFile
057    {
058        /**
059         * Atomically moves temp file to target file it is collocated with.
060         */
061        void move() throws IOException;
062    }
063
064    /**
065     * Creates a {@link TempFile}. It will be in the default temporary-file directory. Returned instance should be
066     * handled in try-with-resource construct and created temp file is removed on close, if exists.
067     */
068    public static TempFile newTempFile() throws IOException
069    {
070        Path tempFile = Files.createTempFile( "resolver", "tmp" );
071        return new TempFile()
072        {
073            @Override
074            public Path getPath()
075            {
076                return tempFile;
077            }
078
079            @Override
080            public void close() throws IOException
081            {
082                Files.deleteIfExists( tempFile );
083            }
084        };
085    }
086
087    /**
088     * Creates a {@link TempFile} for given file. It will be in same directory where given file is, and will reuse its
089     * name for generated name. Returned instance should be handled in try-with-resource construct and created temp
090     * file once ready can be moved to passed in {@code file} parameter place.
091     * <p>
092     * The {@code file} nor it's parent directories have to exist. The parent directories are created if needed.
093     */
094    public static CollocatedTempFile newTempFile( Path file ) throws IOException
095    {
096        Path parent = requireNonNull( file.getParent(), "file must have parent" );
097        Files.createDirectories( parent );
098        Path tempFile = Files.createTempFile( parent, file.getFileName().toString(), "tmp" );
099        return new CollocatedTempFile()
100        {
101            @Override
102            public Path getPath()
103            {
104                return tempFile;
105            }
106
107            @Override
108            public void move() throws IOException
109            {
110                Files.move( tempFile, file, StandardCopyOption.ATOMIC_MOVE );
111            }
112
113            @Override
114            public void close() throws IOException
115            {
116                Files.deleteIfExists( tempFile );
117            }
118        };
119    }
120
121    /**
122     * A file writer, that accepts a {@link Path} to write some content to. Note: the file denoted by path may exist,
123     * hence implementation have to ensure it is able to achieve its goal ("replace existing" option or equivalent
124     * should be used).
125     */
126    @FunctionalInterface
127    public interface FileWriter
128    {
129        void write( Path path ) throws IOException;
130    }
131
132    /**
133     * Writes file without backup.
134     *
135     * @param target that is the target file (must be file, the path must have parent).
136     * @param writer the writer that will accept a {@link Path} to write content to.
137     * @throws IOException if at any step IO problem occurs.
138     */
139    public static void writeFile( Path target, FileWriter writer ) throws IOException
140    {
141        writeFile( target, writer, false );
142    }
143
144    /**
145     * Writes file with backup copy (appends ".bak" extension).
146     *
147     * @param target that is the target file (must be file, the path must have parent).
148     * @param writer the writer that will accept a {@link Path} to write content to.
149     * @throws IOException if at any step IO problem occurs.
150     */
151    public static void writeFileWithBackup( Path target, FileWriter writer ) throws IOException
152    {
153        writeFile( target, writer, true );
154    }
155
156    /**
157     * Utility method to write out file to disk in "atomic" manner, with optional backups (".bak") if needed. This
158     * ensures that no other thread or process will be able to read not fully written files. Finally, this methos
159     * may create the needed parent directories, if the passed in target parents does not exist.
160     *
161     * @param target   that is the target file (must be an existing or non-existing file, the path must have parent).
162     * @param writer   the writer that will accept a {@link Path} to write content to.
163     * @param doBackup if {@code true}, and target file is about to be overwritten, a ".bak" file with old contents will
164     *                 be created/overwritten.
165     * @throws IOException if at any step IO problem occurs.
166     */
167    private static void writeFile( Path target, FileWriter writer, boolean doBackup ) throws IOException
168    {
169        requireNonNull( target, "target is null" );
170        requireNonNull( writer, "writer is null" );
171        Path parent = requireNonNull( target.getParent(), "target must have parent" );
172
173        try ( CollocatedTempFile tempFile = newTempFile( target ) )
174        {
175            writer.write( tempFile.getPath() );
176            if ( doBackup && Files.isRegularFile( target ) )
177            {
178                Files.copy( target, parent.resolve( target.getFileName() + ".bak" ),
179                        StandardCopyOption.REPLACE_EXISTING );
180            }
181            tempFile.move();
182        }
183    }
184}