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.marshall.cp;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.io.*;
023import java.nio.file.*;
024import java.util.*;
025import java.util.regex.*;
026
027import org.apache.juneau.commons.inject.*;
028import org.apache.juneau.commons.io.*;
029
030/**
031 * Utility class for finding regular or localized files on the classpath and file system.
032 *
033 * <h5 class='section'>Example:</h5>
034 * <p class='bjava'>
035 *    <jc>// Constructor a file source that looks for files in the "files" working directory, then in the
036 *    // package "foo.bar", then in the package "foo.bar.files", then in the package "files".</jc>
037 *    FileFinder <jv>finder</jv> = FileFinder
038 *       .<jsm>create</jsm>()
039 *       .dir(<js>"files"</js>)
040 *       .cp(foo.bar.MyClass.<jk>class</jk>,<jk>null</jk>,<jk>true</jk>)
041 *       .cp(foo.bar.MyClass.<jk>class</jk>,<js>"files"</js>,<jk>true</jk>)
042 *       .cp(foo.bar.MyClass.<jk>class</jk>,<js>"/files"</js>,<jk>true</jk>)
043 *       .cache(1_000_000l)  <jc>// Cache files less than 1MB in size.</jc>
044 *       .ignore(Pattern.<jsm>compile</jsm>(<js>"(?i)(.*\\.(class|properties))|(package.html)"</js>)) <jc>// Ignore certain files.</jc>
045 *       .build();
046 *
047 *    <jc>// Find a normal file.</jc>
048 *    InputStream <jv>is1</jv> = <jv>finder</jv>.getStream(<js>"text.txt"</js>);
049 *
050 *    <jc>// Find a localized file called "text_ja_JP.txt".</jc>
051 *    InputStream <jv>is2</jv> = <jv>finder</jv>.getStream(<js>"text.txt"</js>, Locale.<jsf>JAPAN</jsf>);
052 * </p>
053 *
054 * <p>
055 * If the <c>locale</c> is specified, then we look for resources whose name matches that locale.
056 * For example, if looking for the resource <js>"MyResource.txt"</js> for the Japanese locale, we will look for
057 * files in the following order:
058 * <ol>
059 *    <li><js>"MyResource_ja_JP.txt"</js>
060 *    <li><js>"MyResource_ja.txt"</js>
061 *    <li><js>"MyResource.txt"</js>
062 * </ol>
063 *
064 * <p>
065 * The default implementation of this interface is {@link BasicFileFinder}.
066 * To instantiate subclasses, create a subclass-specific builder and pass it to the subclass constructor.
067 *
068 * <h5 class='section'>Example:</h5>
069 * <p class='bjava'>
070 *    <jk>public class</jk> MyFileFinder <jk>extends</jk> BasicFileFinder {
071 *       <jk>public</jk> MyFileFinder(FileFinder.Builder <jv>builder</jv>) {
072 *          <jk>super</jk>(<jv>builder</jv>);
073 *       }
074 *    }
075 *
076 *    <jc>// Instantiate subclass.</jc>
077 *    FileFinder.Builder <jv>b</jv> = FileFinder.<jsm>create</jsm>();
078 *    FileFinder <jv>myFinder</jv> = <jk>new</jk> MyFileFinder(<jv>b</jv>);
079 * </p>
080 *
081 * <p>
082 * Subclasses must provide a public constructor that takes in any of the following arguments:
083 * <ul>
084 *    <li>{@link Builder} - The builder object.
085 *    <li>Any beans present in the bean store passed into the constructor.
086 *    <li>Any {@link Optional} beans optionally present in bean store passed into the constructor.
087 * </ul>
088 *
089 */
090@SuppressWarnings({
091   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
092   "java:S9398" // Nested type is public API; moving it would break callers.
093})
094public interface FileFinder {
095
096   /**
097    * Builder class.
098    */
099   public static class Builder {
100
101      private final BeanStore beanStore;
102      final Set<LocalDir> roots;
103      long cachingLimit;
104      Pattern[] include;
105      Pattern[] exclude;
106
107      /**
108       * Constructor.
109       *
110       * @param beanStore The bean store to use for creating beans.
111       */
112      protected Builder(BeanStore beanStore) {
113         this.beanStore = beanStore;
114         roots = set();
115         cachingLimit = -1;
116         include = a(Pattern.compile(".*"));
117         exclude = a();
118      }
119
120      /**
121       * Returns the bean store used by this builder.
122       *
123       * @return The bean store used by this builder.
124       */
125      public BeanStore beanStore() {
126         return beanStore;
127      }
128
129      /**
130       * Enables in-memory caching of files for quicker retrieval.
131       *
132       * @param cachingLimit The maximum file size in bytes.
133       * @return This object.
134       */
135      public Builder caching(long cachingLimit) {
136         this.cachingLimit = cachingLimit;
137         return this;
138      }
139
140      /**
141       * Adds a class subpackage to the lookup paths.
142       *
143       * @param c The class whose package will be added to the lookup paths.  Must not be <jk>null</jk>.
144       * @param path The absolute or relative subpath.  Can be <jk>null</jk> (the class's own package root is used with no subpath appended).
145       * @param recursive If <jk>true</jk>, also recursively adds all the paths of the parent classes as well.
146       * @return This object.
147       */
148      @SuppressWarnings({
149         "null" // Null check handled by reqnn
150      })
151      public Builder cp(Class<?> c, String path, boolean recursive) {
152         reqnn("c", c);
153         while (nn(c)) {
154            roots.add(new LocalDir(c, path));
155            c = recursive ? c.getSuperclass() : null;
156         }
157         return this;
158      }
159
160      /**
161       * Adds a file system directory to the lookup paths.
162       *
163       * @param value The path relative to the working directory.  Must not be <jk>null</jk>.
164       * @return This object.
165       */
166      public Builder dir(String value) {
167         reqnn("value", value);
168         return path(Paths.get(".").resolve(value));
169      }
170
171      /**
172       * Specifies the regular expression file name pattern to use to exclude files from being retrieved from the file source.
173       *
174       * @param patterns
175       *    The regular expression exclude patterns.
176       *    <br>If none are specified, no files will be excluded.
177       * @return This object.
178       */
179      public Builder exclude(String...patterns) {
180         this.exclude = l(patterns).stream().map(Pattern::compile).toArray(Pattern[]::new);
181         return this;
182      }
183
184
185      /**
186       * Specifies the regular expression file name patterns to use to include files being retrieved from the file source.
187       *
188       * @param patterns
189       *    The regular expression include patterns.
190       *    <br>The default is <js>".*"</js>.
191       * @return This object.
192       */
193      public Builder include(String...patterns) {
194         this.include = l(patterns).stream().map(Pattern::compile).toArray(Pattern[]::new);
195         return this;
196      }
197
198      /**
199       * Adds a file system directory to the lookup paths.
200       *
201       * @param value The directory path.
202       * @return This object.
203       */
204      public Builder path(Path value) {
205         roots.add(new LocalDir(value));
206         return this;
207      }
208
209      /**
210       * Builds the file finder.
211       *
212       * @return A new {@link BasicFileFinder}.
213       */
214      public FileFinder build() {
215         return new BasicFileFinder(this);
216      }
217   }
218
219   /** Represents no file finder */
220   @SuppressWarnings({
221      "java:S9398" // Public annotation-default marker; moving breaks API.
222   })
223   public abstract class Void implements FileFinder {}
224
225   /**
226    * Static creator.
227    *
228    * @return A new builder for this object.
229    */
230   static Builder create() {
231      return new Builder(BasicBeanStore.INSTANCE);
232   }
233
234   /**
235    * Static creator.
236    *
237    * @param beanStore The bean store to use for creating beans.
238    * @return A new builder for this object.
239    */
240   static Builder create(BeanStore beanStore) {
241      return new Builder(beanStore);
242   }
243
244   /**
245    * Returns the contents of the resource with the specified name.
246    *
247    * @param name The resource name.
248    *    See {@link Class#getResource(String)} for format.
249    * @param locale
250    *    The locale of the resource to retrieve.
251    *    <br>If <jk>null</jk>, won't look for localized file names.
252    * @return The resolved resource contents, or an empty {@link Optional} if the resource was not found.
253    * @throws IOException Thrown by underlying stream.
254    */
255   Optional<InputStream> getStream(String name, Locale locale) throws IOException;
256
257   /**
258    * Returns the file with the specified name as a string.
259    *
260    * @param name The file name.
261    * @param locale
262    *    The locale of the resource to retrieve.
263    *    <br>If <jk>null</jk>, won't look for localized file names.
264    * @return The contents of the file as a string.  Assumes UTF-8 encoding.
265    * @throws IOException If file could not be read.
266    */
267   Optional<String> getString(String name, Locale locale) throws IOException;
268}