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}