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