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.io;
018
019import static org.apache.juneau.commons.utils.IoUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.io.*;
023import java.nio.file.*;
024
025/**
026 * Represents a file that can be located either on the classpath or in the file system.
027 *
028 * <p>
029 * This class provides a unified interface for working with files regardless of their location,
030 * allowing code to transparently access files from either the classpath (as resources) or the
031 * file system. This is particularly useful in applications that need to work with files in both
032 * development (file system) and production (packaged JAR) environments.
033 *
034 * <h5 class='section'>Features:</h5>
035 * <ul class='spaced-list'>
036 *    <li>Unified file access - works with both classpath resources and file system files
037 *    <li>Optional caching - can cache file contents in memory for fast repeated access
038 *    <li>Transparent resolution - automatically resolves files based on construction parameters
039 *    <li>Thread-safe reading - synchronized access for cached content
040 * </ul>
041 *
042 * <h5 class='section'>Use Cases:</h5>
043 * <ul class='spaced-list'>
044 *    <li>Reading configuration files that may be on classpath or file system
045 *    <li>Accessing template files in both development and production environments
046 *    <li>Loading resources that need to work in both JAR and unpackaged scenarios
047 *    <li>Applications that need to support both embedded and external file access
048 * </ul>
049 *
050 * <h5 class='section'>Usage:</h5>
051 * <p class='bjava'>
052 *    <jc>// Classpath file</jc>
053 *    LocalFile <jv>classpathFile</jv> = <jk>new</jk> LocalFile(MyClass.<jk>class</jk>, <js>"config.properties"</js>);
054 *    InputStream <jv>is</jv> = <jv>classpathFile</jv>.read();
055 *
056 *    <jc>// File system file</jc>
057 *    LocalFile <jv>fsFile</jv> = <jk>new</jk> LocalFile(Paths.get(<js>"/path/to/file.txt"</js>));
058 *    InputStream <jv>is2</jv> = <jv>fsFile</jv>.read();
059 *
060 *    <jc>// With caching for repeated access</jc>
061 *    LocalFile <jv>cachedFile</jv> = <jk>new</jk> LocalFile(MyClass.<jk>class</jk>, <js>"template.html"</js>);
062 *    <jv>cachedFile</jv>.cache();  <jc>// Cache contents in memory</jc>
063 *    InputStream <jv>is3</jv> = <jv>cachedFile</jv>.read();  <jc>// Fast - uses cache</jc>
064 * </p>
065 *
066 * <h5 class='section'>Caching:</h5>
067 * <p>
068 * The {@link #cache()} method loads the entire file contents into memory, which can improve
069 * performance for files that are accessed multiple times. Once cached, subsequent calls to
070 * {@link #read()} return a {@link ByteArrayInputStream} backed by the cached data. Caching is
071 * thread-safe and synchronized.
072 *
073 * <h5 class='section'>Thread Safety:</h5>
074 * <p>
075 * This class is thread-safe for reading operations. The caching mechanism uses synchronization
076 * to ensure thread-safe access to cached content. Multiple threads can safely call {@link #read()}
077 * concurrently.
078 *
079 * <h5 class='section'>See Also:</h5><ul>
080 *    <li class='jc'>{@link LocalDir} - Directory counterpart for resolving files within directories
081 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauCommonsIO">I/O Package</a>
082 * </ul>
083 */
084@SuppressWarnings({
085   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
086   "resource" // File resources managed by calling code
087})
088public class LocalFile {
089
090   private final Class<?> clazz;
091   private final String clazzPath;
092   private final Path path;
093   private final String name;
094   private byte[] cache;
095
096   /**
097    * Constructor for classpath file.
098    *
099    * <p>
100    * Creates a LocalFile that references a file on the classpath, relative to the specified class.
101    * The path is resolved using {@link Class#getResourceAsStream(String)}.
102    *
103    * <h5 class='section'>Example:</h5>
104    * <p class='bjava'>
105    *    <jc>// File in same package as MyClass</jc>
106    *    LocalFile <jv>file1</jv> = <jk>new</jk> LocalFile(MyClass.<jk>class</jk>, <js>"config.properties"</js>);
107    *
108    *    <jc>// File in subdirectory</jc>
109    *    LocalFile <jv>file2</jv> = <jk>new</jk> LocalFile(MyClass.<jk>class</jk>, <js>"templates/index.html"</js>);
110    *
111    *    <jc>// Absolute path from classpath root</jc>
112    *    LocalFile <jv>file3</jv> = <jk>new</jk> LocalFile(MyClass.<jk>class</jk>, <js>"/com/example/config.xml"</js>);
113    * </p>
114    *
115    * @param clazz The class used to retrieve resources. Must not be <jk>null</jk>.
116    * @param clazzPath The path relative to the class. Must be a non-null normalized relative path.
117    *                  Use absolute paths (starting with <js>'/'</js>) to reference from classpath root.
118    */
119   public LocalFile(Class<?> clazz, String clazzPath) {
120      this.clazz = reqnn("clazz", clazz);
121      this.clazzPath = reqnn("clazzPath", clazzPath);
122      this.path = null;
123      var i = clazzPath.lastIndexOf('/');
124      this.name = i == -1 ? clazzPath : clazzPath.substring(i + 1);
125   }
126
127   /**
128    * Constructor for file system file.
129    *
130    * <p>
131    * Creates a LocalFile that references a file on the file system using a {@link Path}.
132    * The path must point to an actual file (not a directory) and must have a filename component.
133    *
134    * <h5 class='section'>Example:</h5>
135    * <p class='bjava'>
136    *    <jc>// Absolute path</jc>
137    *    LocalFile <jv>file1</jv> = <jk>new</jk> LocalFile(Paths.get(<js>"/etc/config.properties"</js>));
138    *
139    *    <jc>// Relative path</jc>
140    *    LocalFile <jv>file2</jv> = <jk>new</jk> LocalFile(Paths.get(<js>"data/input.txt"</js>));
141    *
142    *    <jc>// From File object</jc>
143    *    File <jv>f</jv> = <jk>new</jk> File(<js>"output.log"</js>);
144    *    LocalFile <jv>file3</jv> = <jk>new</jk> LocalFile(<jv>f</jv>.toPath());
145    * </p>
146    *
147    * @param path Filesystem file location. Must not be <jk>null</jk>.
148    *             Must not be a root path (must have a filename).
149    * @throws IllegalArgumentException if the path is a root path (has no filename).
150    */
151   public LocalFile(Path path) {
152      this.clazz = null;
153      this.clazzPath = null;
154      this.path = reqnn("path", path);
155      var fileName = path.getFileName();
156      req(fileName != null, "Argument 'path' must not be a root path (must have a filename).");
157      this.name = o(fileName).map(Object::toString).orElse(null);
158   }
159
160   /**
161    * Caches the contents of this file into an internal byte array for quick future retrieval.
162    *
163    * <p>
164    * This method reads the entire file into memory and stores it in a byte array. Subsequent
165    * calls to {@link #read()} will return a {@link ByteArrayInputStream} backed by this cached
166    * data, avoiding repeated file I/O operations. This is useful for files that are accessed
167    * multiple times.
168    *
169    * <p>
170    * The caching operation is thread-safe and synchronized. If multiple threads call this method
171    * concurrently, only one will perform the actual read operation.
172    *
173    * <h5 class='section'>Example:</h5>
174    * <p class='bjava'>
175    *    LocalFile <jv>file</jv> = <jk>new</jk> LocalFile(MyClass.<jk>class</jk>, <js>"template.html"</js>);
176    *    <jv>file</jv>.cache();  <jc>// Load into memory</jc>
177    *
178    *    <jc>// Multiple reads are fast - no I/O</jc>
179    *    InputStream <jv>is1</jv> = <jv>file</jv>.read();
180    *    InputStream <jv>is2</jv> = <jv>file</jv>.read();
181    * </p>
182    *
183    * <h5 class='section'>Memory Considerations:</h5>
184    * <ul class='spaced-list'>
185    *    <li>The entire file is loaded into memory, so use with caution for large files
186    *    <li>Once cached, the file contents remain in memory for the lifetime of the LocalFile object
187    *    <li>Cache is shared across all threads accessing this LocalFile instance
188    * </ul>
189    *
190    * @return This object for method chaining.
191    * @throws IOException If the file could not be read or does not exist.
192    */
193   public LocalFile cache() throws IOException {
194      synchronized (this) {
195         this.cache = readBytes(read());
196      }
197      return this;
198   }
199
200   /**
201    * Returns the name of this file (filename without directory path).
202    *
203    * <p>
204    * For classpath files, this is the last component of the classpath path.
205    * For file system files, this is the filename component of the path.
206    *
207    * <h5 class='section'>Example:</h5>
208    * <p class='bjava'>
209    *    LocalFile <jv>file1</jv> = <jk>new</jk> LocalFile(MyClass.<jk>class</jk>, <js>"templates/index.html"</js>);
210    *    String <jv>name1</jv> = <jv>file1</jv>.getName();  <jc>// Returns "index.html"</jc>
211    *
212    *    LocalFile <jv>file2</jv> = <jk>new</jk> LocalFile(Paths.get(<js>"/var/log/app.log"</js>));
213    *    String <jv>name2</jv> = <jv>file2</jv>.getName();  <jc>// Returns "app.log"</jc>
214    * </p>
215    *
216    * @return The name of this file (filename component only).
217    */
218   public String getName() { return name; }
219
220   /**
221    * Returns an input stream for reading the contents of this file.
222    *
223    * <p>
224    * If the file has been cached via {@link #cache()}, this method returns a
225    * {@link ByteArrayInputStream} backed by the cached data. Otherwise, it returns
226    * a new input stream that reads directly from the file (classpath resource or file system).
227    *
228    * <p>
229    * Each call to this method returns a new input stream. The caller is responsible
230    * for closing the returned stream.
231    *
232    * <h5 class='section'>Example:</h5>
233    * <p class='bjava'>
234    *    LocalFile <jv>file</jv> = <jk>new</jk> LocalFile(MyClass.<jk>class</jk>, <js>"data.txt"</js>);
235    *
236    *    <jc>// Read file contents</jc>
237    *    <jk>try</jk> (InputStream <jv>is</jv> = <jv>file</jv>.read()) {
238    *       <jc>// Process stream</jc>
239    *    }
240    * </p>
241    *
242    * @return An input stream for reading the contents of this file.
243    * @throws IOException If the file could not be read, does not exist, or is not accessible.
244    */
245   public InputStream read() throws IOException {
246      synchronized (this) {
247         if (nn(cache))
248            return new ByteArrayInputStream(cache);
249      }
250      if (nn(clazz)) {
251         var is = clazz.getResourceAsStream(clazzPath);
252         if (is == null)
253            throw ioex("Classpath resource not found: %s (relative to %s)", clazzPath, cn(clazz));
254         return is;
255      }
256      return Files.newInputStream(path);
257   }
258
259   /**
260    * Returns <jk>true</jk> if this file exists, without throwing an exception.
261    *
262    * <p>
263    * For classpath files, existence is checked via {@link Class#getResource(String)}.
264    * For file system files, existence is checked via {@link Files#exists(Path, LinkOption...)}.
265    * A previously {@link #cache() cached} file is always considered to exist.
266    *
267    * <p>
268    * This is a convenience method for the common classpath-or-file-system fallback pattern, where
269    * callers need to check for existence before deciding whether to call {@link #read()} (which
270    * throws an {@link IOException} if the file is missing).
271    *
272    * <h5 class='section'>Example:</h5>
273    * <p class='bjava'>
274    *    LocalFile <jv>file</jv> = <jk>new</jk> LocalFile(MyClass.<jk>class</jk>, <js>"optional.properties"</js>);
275    *    <jk>if</jk> (<jv>file</jv>.exists()) {
276    *       <jk>try</jk> (InputStream <jv>is</jv> = <jv>file</jv>.read()) {
277    *          <jc>// Process stream</jc>
278    *       }
279    *    }
280    * </p>
281    *
282    * @return <jk>true</jk> if this file exists.
283    */
284   public boolean exists() {
285      synchronized (this) {
286         if (nn(cache))
287            return true;
288      }
289      if (nn(clazz))
290         return clazz.getResource(clazzPath) != null;
291      return Files.exists(path);
292   }
293
294   /**
295    * Returns the size of this file in bytes.
296    *
297    * <p>
298    * For file system files, this method returns the actual file size using {@link Files#size(Path)}.
299    * For classpath files, the size cannot be determined and this method returns <c>-1</c>.
300    *
301    * <h5 class='section'>Example:</h5>
302    * <p class='bjava'>
303    *    LocalFile <jv>fsFile</jv> = <jk>new</jk> LocalFile(Paths.get(<js>"/var/log/app.log"</js>));
304    *    <jk>long</jk> <jv>size</jv> = <jv>fsFile</jv>.size();  <jc>// Returns actual file size</jc>
305    *
306    *    LocalFile <jv>cpFile</jv> = <jk>new</jk> LocalFile(MyClass.<jk>class</jk>, <js>"resource.txt"</js>);
307    *    <jk>long</jk> <jv>size2</jv> = <jv>cpFile</jv>.size();  <jc>// Returns -1 (unknown)</jc>
308    * </p>
309    *
310    * @return The size of this file in bytes, or <c>-1</c> if the size cannot be determined
311    *         (e.g., for classpath resources).
312    * @throws IOException If the file size could not be determined (for file system files).
313    */
314   public long size() throws IOException {
315      return (path == null ? -1 : Files.size(path));
316   }
317}