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.StringUtils.*; 021import static org.apache.juneau.commons.utils.Utils.*; 022 023import java.net.*; 024import java.nio.file.*; 025 026/** 027 * Represents a directory 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 directories regardless of their location, 031 * allowing code to transparently access files within directories from either the classpath (as resources) 032 * or the file system. This is particularly useful in applications that need to work with directory-based 033 * resources in both development (file system) and production (packaged JAR) environments. 034 * 035 * <h5 class='section'>Features:</h5> 036 * <ul class='spaced-list'> 037 * <li>Unified directory access - works with both classpath resources and file system directories 038 * <li>File resolution - can resolve files within the directory using relative paths 039 * <li>Transparent resolution - automatically resolves files based on construction parameters 040 * <li>Immutable - directory location cannot be changed after construction 041 * </ul> 042 * 043 * <h5 class='section'>Use Cases:</h5> 044 * <ul class='spaced-list'> 045 * <li>Accessing template directories that may be on classpath or file system 046 * <li>Loading configuration files from directories in both development and production 047 * <li>Resolving resources within package directories 048 * <li>Applications that need to support both embedded and external directory access 049 * </ul> 050 * 051 * <h5 class='section'>Usage:</h5> 052 * <p class='bjava'> 053 * <jc>// Classpath directory</jc> 054 * LocalDir <jv>classpathDir</jv> = <jk>new</jk> LocalDir(MyClass.<jk>class</jk>, <js>"templates"</js>); 055 * LocalFile <jv>file</jv> = <jv>classpathDir</jv>.resolve(<js>"index.html"</js>); 056 * 057 * <jc>// File system directory</jc> 058 * LocalDir <jv>fsDir</jv> = <jk>new</jk> LocalDir(Paths.get(<js>"/var/config"</js>)); 059 * LocalFile <jv>file2</jv> = <jv>fsDir</jv>.resolve(<js>"app.properties"</js>); 060 * 061 * <jc>// Package directory (null or empty path)</jc> 062 * LocalDir <jv>packageDir</jv> = <jk>new</jk> LocalDir(MyClass.<jk>class</jk>, <jk>null</jk>); 063 * LocalFile <jv>file3</jv> = <jv>packageDir</jv>.resolve(<js>"resource.txt"</js>); 064 * </p> 065 * 066 * <h5 class='section'>Path Resolution:</h5> 067 * <p> 068 * The {@link #resolve(String)} method resolves files within the directory using relative paths. 069 * For classpath directories, the path resolution follows Java resource path conventions: 070 * <ul class='spaced-list'> 071 * <li><jk>null</jk> or empty string - resolves relative to the class's package 072 * <li>Absolute path (starts with <js>'/'</js>) - resolves relative to classpath root 073 * <li>Relative path - resolves relative to the specified classpath path 074 * </ul> 075 * 076 * <h5 class='section'>Thread Safety:</h5> 077 * <p> 078 * This class is immutable and therefore thread-safe. Multiple threads can safely access a LocalDir 079 * instance concurrently without synchronization. 080 * 081 * <h5 class='section'>See Also:</h5><ul> 082 * <li class='jc'>{@link LocalFile} - File counterpart for individual file access 083 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauCommonsIO">I/O Package</a> 084 * </ul> 085 */ 086public class LocalDir { 087 088 /** 089 * Validates that the specified classpath resource exists and is a file. 090 * Note that the behavior of Class.getResource(path) is different when pointing to directories on the classpath. 091 * When packaged as a jar, calling Class.getResource(path) on a directory returns null. 092 * When unpackaged, calling Class.getResource(path) on a directory returns a URL starting with "file:". 093 * We perform a test to make the behavior the same regardless of whether we're packaged or not. 094 */ 095 private static boolean isClasspathFile(URL url) { 096 return safeSupplier(() -> { 097 if (url == null) 098 return false; 099 var uri = url.toURI(); 100 if (uri.toString().startsWith("file:")) 101 if (Files.isDirectory(Paths.get(uri))) 102 return false; 103 return true; 104 }); 105 } 106 107 private final Class<?> clazz; 108 private final String clazzPath; 109 private final Path path; 110 111 private final int hashCode; 112 113 /** 114 * Constructor for classpath directory. 115 * 116 * <p> 117 * Creates a LocalDir that references a directory on the classpath, relative to the specified class. 118 * The path resolution follows Java resource path conventions. 119 * 120 * <h5 class='section'>Path Examples:</h5> 121 * <p class='bjava'> 122 * <jc>// Package directory (null or empty)</jc> 123 * LocalDir <jv>dir1</jv> = <jk>new</jk> LocalDir(MyClass.<jk>class</jk>, <jk>null</jk>); 124 * <jc>// Resolves files in same package as MyClass</jc> 125 * 126 * <jc>// Absolute path from classpath root</jc> 127 * LocalDir <jv>dir2</jv> = <jk>new</jk> LocalDir(MyClass.<jk>class</jk>, <js>"/com/example/templates"</js>); 128 * <jc>// Resolves files from classpath root</jc> 129 * 130 * <jc>// Relative path from class package</jc> 131 * LocalDir <jv>dir3</jv> = <jk>new</jk> LocalDir(MyClass.<jk>class</jk>, <js>"templates"</js>); 132 * <jc>// Resolves files relative to MyClass package</jc> 133 * </p> 134 * 135 * @param clazz The class used to retrieve resources. Must not be <jk>null</jk>. 136 * @param clazzPath The subpath. Can be any of the following: 137 * <ul> 138 * <li><jk>null</jk> or an empty string - Package of the class 139 * <li>Absolute path (starts with <js>'/'</js>) - Relative to root package 140 * <li>Relative path (does not start with <js>'/'</js>) - Relative to class package 141 * </ul> 142 */ 143 public LocalDir(Class<?> clazz, String clazzPath) { 144 this.clazz = assertArgNotNull("clazz", clazz); 145 this.clazzPath = "/".equals(clazzPath) ? "/" : nullIfEmpty(trimTrailingSlashes(clazzPath)); 146 this.path = null; 147 this.hashCode = h(clazz, clazzPath); 148 } 149 150 /** 151 * Constructor for file system directory. 152 * 153 * <p> 154 * Creates a LocalDir that references a directory on the file system using a {@link Path}. 155 * 156 * <h5 class='section'>Example:</h5> 157 * <p class='bjava'> 158 * <jc>// Absolute path</jc> 159 * LocalDir <jv>dir1</jv> = <jk>new</jk> LocalDir(Paths.get(<js>"/var/config"</js>)); 160 * 161 * <jc>// Relative path</jc> 162 * LocalDir <jv>dir2</jv> = <jk>new</jk> LocalDir(Paths.get(<js>"data/templates"</js>)); 163 * 164 * <jc>// From File object</jc> 165 * File <jv>f</jv> = <jk>new</jk> File(<js>"output"</js>); 166 * LocalDir <jv>dir3</jv> = <jk>new</jk> LocalDir(<jv>f</jv>.toPath()); 167 * </p> 168 * 169 * @param path Filesystem directory location. Must not be <jk>null</jk>. 170 */ 171 public LocalDir(Path path) { 172 this.clazz = null; 173 this.clazzPath = null; 174 this.path = assertArgNotNull("path", path); 175 this.hashCode = path.hashCode(); 176 } 177 178 @Override /* Overridden from Object */ 179 public boolean equals(Object o) { 180 return o instanceof LocalDir o2 && eq(this, o2, (x, y) -> eq(x.clazz, y.clazz) && eq(x.clazzPath, y.clazzPath) && eq(x.path, y.path)); 181 } 182 183 @Override /* Overridden from Object */ 184 public int hashCode() { 185 return hashCode; 186 } 187 188 /** 189 * Resolves a file within this directory using the specified relative path. 190 * 191 * <p> 192 * This method attempts to locate a file within the directory. If the file exists and is readable, 193 * a {@link LocalFile} instance is returned. If the file does not exist or is not accessible, 194 * <jk>null</jk> is returned. 195 * 196 * <p> 197 * For classpath directories, the path is resolved according to the directory's path type: 198 * <ul class='spaced-list'> 199 * <li>Package directory (null clazzPath) - path is kept relative 200 * <li>Root directory ("/") - path is made absolute if not already 201 * <li>Absolute clazzPath - resolved path is made absolute 202 * <li>Relative clazzPath - resolved path remains relative 203 * </ul> 204 * 205 * <h5 class='section'>Example:</h5> 206 * <p class='bjava'> 207 * LocalDir <jv>dir</jv> = <jk>new</jk> LocalDir(MyClass.<jk>class</jk>, <js>"templates"</js>); 208 * 209 * <jc>// Resolve file in directory</jc> 210 * LocalFile <jv>file</jv> = <jv>dir</jv>.resolve(<js>"index.html"</js>); 211 * <jk>if</jk> (<jv>file</jv> != <jk>null</jk>) { 212 * InputStream <jv>is</jv> = <jv>file</jv>.read(); 213 * } 214 * 215 * <jc>// Resolve file in subdirectory</jc> 216 * LocalFile <jv>file2</jv> = <jv>dir</jv>.resolve(<js>"pages/about.html"</js>); 217 * </p> 218 * 219 * <h5 class='section'>Security Note:</h5> 220 * <p> 221 * This method does not perform path validation or security checks (e.g., checking for path 222 * traversal attacks or malformed values). The caller is responsible for ensuring the path 223 * is safe and valid. 224 * 225 * @param path The relative path to the file to resolve within this directory. 226 * Must be a non-null relative path. 227 * @return A {@link LocalFile} instance if the file exists and is readable, or <jk>null</jk> if it does not. 228 */ 229 public LocalFile resolve(String path) { 230 assertArgNotNull("path", path); 231 if (nn(clazz)) { 232 String p; 233 if (clazzPath == null) { 234 // Relative to class package - keep path relative 235 p = path; 236 } else if ("/".equals(clazzPath)) { 237 // Root - make path absolute 238 p = path.startsWith("/") ? path : "/" + path; 239 } else if (clazzPath.startsWith("/")) { 240 // Absolute clazzPath - make resolved path absolute 241 p = clazzPath + '/' + path; 242 } else { 243 // Relative clazzPath - keep resolved path relative 244 p = clazzPath + '/' + path; 245 } 246 if (isClasspathFile(clazz.getResource(p))) 247 return new LocalFile(clazz, p); 248 } else { 249 var p = this.path.resolve(path); 250 if (Files.isReadable(p) && ! Files.isDirectory(p)) 251 return new LocalFile(p); 252 } 253 return null; 254 } 255 256 @Override /* Overridden from Object */ 257 public String toString() { 258 if (clazz == null) 259 return path.toString(); 260 return cn(clazz) + ":" + clazzPath; 261 } 262}