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}