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}