001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.utils;
018
019import static org.apache.juneau.commons.utils.AssertionUtils.*;
020import static org.apache.juneau.commons.utils.IoUtils.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022import static org.apache.juneau.commons.utils.ThrowableUtils.*;
023import static org.apache.juneau.commons.utils.Utils.*;
024
025import java.io.*;
026import java.nio.file.*;
027
028/**
029 * File utilities.
030 *
031 */
032public class FileUtils {
033
034   /**
035    * Creates a file if it doesn't already exist using {@link File#createNewFile()}.
036    *
037    * <p>
038    * Throws a {@link RuntimeException} if the file could not be created.
039    *
040    * @param f The file to create.
041    */
042   public static void create(File f) {
043      if (f.exists())
044         return;
045      safe(() -> {
046         opt(f.createNewFile()).filter(x -> x).orElseThrow(() -> rex("Could not create file ''{0}''", f.getAbsolutePath()));
047      });
048   }
049
050   /**
051    * Create a temporary file with the specified name.
052    *
053    * <p>
054    * The name is broken into file name and suffix, and the parts are passed to
055    * {@link File#createTempFile(String, String)}.
056    *
057    * <p>
058    * {@link File#deleteOnExit()} is called on the resulting file before being returned by this method.
059    *
060    * @param name The file name
061    * @return A newly-created temporary file.
062    * @throws IOException Thrown by underlying stream.
063    */
064   public static File createTempFile(String name) throws IOException {
065      var parts = name.split("\\.");
066      var f = File.createTempFile(parts[0], "." + parts[1]);
067      f.deleteOnExit();
068      return f;
069   }
070
071   /**
072    * Create a temporary file with the specified name and specified contents.
073    *
074    * <p>
075    * The name is broken into file name and suffix, and the parts are passed to
076    * {@link File#createTempFile(String, String)}.
077    *
078    * <p>
079    * {@link File#deleteOnExit()} is called on the resulting file before being returned by this method.
080    *
081    * @param name The file name
082    * @param contents The file contents.
083    * @return A newly-created temporary file.
084    * @throws IOException Thrown by underlying stream.
085    */
086   public static File createTempFile(String name, String contents) throws IOException {
087      var f = createTempFile(name);
088      if (contents != null) {
089         try (var r = new StringReader(contents); Writer w = new FileWriter(f)) {
090            pipe(r, w);
091            w.flush();
092         }
093      }
094      // If contents is null, create an empty file
095      return f;
096   }
097
098   /**
099    * Recursively deletes a file or directory.
100    *
101    * @param f The file or directory to delete.
102    * @return <jk>true</jk> if file or directory was successfully deleted.
103    */
104   public static boolean deleteFile(File f) {
105      if (f == null)
106         return true;
107      if (f.isDirectory()) {
108         var cf = f.listFiles();
109         if (nn(cf))
110            for (var c : cf)
111               deleteFile(c);
112      }
113      return f.delete();
114   }
115
116   /**
117    * Returns <jk>true</jk> if the specified file exists in the specified directory.
118    *
119    * @param dir The directory.
120    * @param fileName The file name.
121    * @return <jk>true</jk> if the specified file exists in the specified directory.
122    */
123   public static boolean fileExists(File dir, String fileName) {
124      if (dir == null || fileName == null)
125         return false;
126      return Files.exists(dir.toPath().resolve(fileName));
127   }
128
129   /**
130    * Strips the extension from a file name.
131    *
132    * @param name The file name.
133    * @return The file name without the extension, or <jk>null</jk> if name was <jk>null</jk>.
134    */
135   public static String getBaseName(String name) {
136      if (name == null)
137         return null;
138      var i = name.lastIndexOf('.');
139      if (i == -1)
140         return name;
141      return name.substring(0, i);
142   }
143
144   /**
145    * Returns the extension from a file name.
146    *
147    * @param name The file name.
148    * @return The the extension, or <jk>null</jk> if name was <jk>null</jk>.
149    */
150   public static String getFileExtension(String name) {
151      if (name == null)
152         return null;
153      var i = name.lastIndexOf('.');
154      if (i == -1)
155         return "";
156      return name.substring(i + 1);
157   }
158
159   /**
160    * Given an arbitrary path, returns the file name portion of that path.
161    *
162    * @param path The path to check.
163    * @return The file name.
164    */
165   public static String getFileName(String path) {
166      if (isEmpty(path))
167         return null;
168      path = trimTrailingSlashes(path);
169      if (isEmpty(path))
170         return null;  // Path contained only slashes
171      // Handle both forward slashes (Unix) and backslashes (Windows)
172      var i = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
173      return i == -1 ? path : path.substring(i + 1);
174   }
175
176   /**
177    * Returns <jk>true</jk> if the specified file name contains the specified extension.
178    *
179    * @param name The file name.
180    * @param ext The extension.
181    * @return <jk>true</jk> if the specified file name contains the specified extension.
182    */
183   public static boolean hasExtension(String name, String ext) {
184      if (name == null || ext == null)
185         return false;
186      return ext.equals(getFileExtension(name));
187   }
188
189   /**
190    * Same as {@link File#mkdirs()} except throws a RuntimeExeption if directory could not be created.
191    *
192    * @param f The directory to create.  Must not be <jk>null</jk>.
193    * @param clean If <jk>true</jk>, deletes the contents of the directory if it already exists.
194    * @return The same file.
195    * @throws RuntimeException if directory could not be created.
196    */
197   public static File mkdirs(File f, boolean clean) {
198      assertArgNotNull("f", f);
199      if (f.exists()) {
200         if (clean) {
201            opt(deleteFile(f)).filter(x -> x).orElseThrow(() -> rex("Could not clean directory ''{0}''", f.getAbsolutePath()));
202         } else {
203            return f;
204         }
205      }
206      opt(f.mkdirs()).filter(x -> x).orElseThrow(() -> rex("Could not create directory ''{0}''", f.getAbsolutePath()));
207      return f;
208   }
209
210   /**
211    * Same as {@link #mkdirs(String, boolean)} but uses String path.
212    *
213    * @param path The path of the directory to create.  Must not be <jk>null</jk>
214    * @param clean If <jk>true</jk>, deletes the contents of the directory if it already exists.
215    * @return The directory.
216    */
217   public static File mkdirs(String path, boolean clean) {
218      assertArgNotNull("path", path);
219      return mkdirs(new File(path), clean);
220   }
221
222   /**
223    * Updates the modified timestamp on the specified file.
224    *
225    * <p>
226    * Method ensures that the timestamp changes even if it's been modified within the past millisecond.
227    *
228    * @param f The file to modify the modified timestamp on.
229    */
230   public static void modifyTimestamp(File f) {
231      var lm = f.lastModified();
232      var l = System.currentTimeMillis();
233      if (lm == l)
234         l++;
235      opt(f.setLastModified(l)).filter(x -> x).orElseThrow(() -> rex("Could not modify timestamp on file ''{0}''", f.getAbsolutePath()));
236
237      // Linux only gives 1s precision, so set the date 1s into the future.
238      if (lm == f.lastModified())
239         opt(f.setLastModified(l + 1000)).filter(x -> x).orElseThrow(() -> rex("Could not modify timestamp on file ''{0}''", f.getAbsolutePath()));
240   }
241}