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}