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}