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