001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.cp;
014
015import static org.apache.juneau.collections.JsonMap.*;
016import static org.apache.juneau.common.internal.IOUtils.*;
017import static org.apache.juneau.common.internal.StringUtils.*;
018import static org.apache.juneau.internal.CollectionUtils.*;
019import static org.apache.juneau.internal.FileUtils.*;
020import static org.apache.juneau.internal.ObjectUtils.*;
021
022import java.io.*;
023import java.util.*;
024import java.util.ResourceBundle.*;
025import java.util.concurrent.*;
026import java.util.regex.*;
027
028import org.apache.juneau.common.internal.*;
029import org.apache.juneau.internal.*;
030
031/**
032 * Basic implementation of a {@link FileFinder}.
033 *
034 * <p>
035 * Specialized behavior can be implemented by overridding the {@link #find(String, Locale)} method.
036 *
037 * <h5 class='section'>Example:</h5>
038 * <p class='bjava'>
039 *    <jk>public class</jk> MyFileFinder <jk>extends</jk> BasicFileFinder {
040 *       <ja>@Override</ja>
041 *       <jk>protected</jk> Optional&lt;InputStream&gt; find(String <jv>name</jv>, Locale <jv>locale</jv>) <jk>throws</jk> IOException {
042 *          <jc>// Do special handling or just call super.find().</jc>
043 *          <jk>return super</jk>.find(<jv>name</jv>, <jv>locale</jv>);
044 *       }
045 *    }
046 * </p>
047 *
048 * <h5 class='section'>See Also:</h5><ul>
049 * </ul>
050 */
051public class BasicFileFinder implements FileFinder {
052
053
054   private static final ResourceBundle.Control RB_CONTROL = ResourceBundle.Control.getControl(Control.FORMAT_DEFAULT);
055
056   private final Map<String,LocalFile> files = new ConcurrentHashMap<>();
057   private final Map<Locale,Map<String,LocalFile>> localizedFiles = new ConcurrentHashMap<>();
058
059   private final LocalDir[] roots;
060   private final long cachingLimit;
061   private final Pattern[] include, exclude;
062   private final String[] includePatterns, excludePatterns;
063   private final int hashCode;
064
065   /**
066    * Builder-based constructor.
067    *
068    * @param builder The builder object.
069    */
070   public BasicFileFinder(FileFinder.Builder builder) {
071      this.roots = builder.roots.toArray(new LocalDir[builder.roots.size()]);
072      this.cachingLimit = builder.cachingLimit;
073      this.include = builder.include;
074      this.exclude = builder.exclude;
075      this.includePatterns = alist(include).stream().map(Pattern::pattern).toArray(String[]::new);
076      this.excludePatterns = alist(exclude).stream().map(Pattern::pattern).toArray(String[]::new);
077      this.hashCode = HashCode.of(getClass(), roots, cachingLimit, includePatterns, excludePatterns);
078   }
079
080   /**
081    * Default constructor.
082    *
083    * <p>
084    * Can be used when providing a subclass that overrides the {@link #find(String, Locale)} method.
085    */
086   protected BasicFileFinder() {
087      this.roots = new LocalDir[0];
088      this.cachingLimit = -1;
089      this.include = new Pattern[0];
090      this.exclude = new Pattern[0];
091      this.includePatterns = new String[0];
092      this.excludePatterns = new String[0];
093      this.hashCode = HashCode.of(getClass(), roots, cachingLimit, includePatterns, excludePatterns);
094   }
095
096   //-----------------------------------------------------------------------------------------------------------------
097   // FileFinder methods
098   //-----------------------------------------------------------------------------------------------------------------
099
100   @Override /* FileFinder */
101   public final Optional<InputStream> getStream(String name, Locale locale) throws IOException {
102      return find(name, locale);
103   }
104
105   @Override /* FileFinder */
106   public Optional<String> getString(String name, Locale locale) throws IOException {
107      return optional(read(find(name, locale).orElse(null)));
108   }
109
110   //-----------------------------------------------------------------------------------------------------------------
111   // Implementation methods
112   //-----------------------------------------------------------------------------------------------------------------
113
114   /**
115    * The main implementation method for finding files.
116    *
117    * <p>
118    * Subclasses can override this method to provide their own handling.
119    *
120    * @param name The resource name.
121    *    See {@link Class#getResource(String)} for format.
122    * @param locale
123    *    The locale of the resource to retrieve.
124    *    <br>If <jk>null</jk>, won't look for localized file names.
125    * @return The resolved resource contents, or <jk>null</jk> if the resource was not found.
126    * @throws IOException Thrown by underlying stream.
127    */
128   protected Optional<InputStream> find(String name, Locale locale) throws IOException {
129      name = StringUtils.trimSlashesAndSpaces(name);
130
131      if (isInvalidPath(name))
132         return empty();
133
134      if (locale != null)
135         localizedFiles.putIfAbsent(locale, new ConcurrentHashMap<>());
136
137      Map<String,LocalFile> fileCache = locale == null ? files : localizedFiles.get(locale);
138
139      LocalFile lf = fileCache.get(name);
140
141      if (lf == null) {
142         List<String> candidateFileNames = getCandidateFileNames(name, locale);
143         paths: for (LocalDir root : roots) {
144            for (String cfn : candidateFileNames) {
145               lf = root.resolve(cfn);
146               if (lf != null)
147                  break paths;
148            }
149         }
150
151         if (lf != null && isIgnoredFile(lf.getName()))
152            lf = null;
153
154         if (lf != null) {
155            fileCache.put(name, lf);
156
157            if (cachingLimit >= 0) {
158               long size = lf.size();
159               if (size > 0 && size <= cachingLimit)
160                  lf.cache();
161            }
162         }
163      }
164
165      return optional(lf == null ? null : lf.read());
166   }
167
168   /**
169    * Returns the candidate file names for the specified file name in the specified locale.
170    *
171    * <p>
172    * For example, if looking for the <js>"MyResource.txt"</js> file in the Japanese locale, the iterator will return
173    * names in the following order:
174    * <ol>
175    *    <li><js>"MyResource_ja_JP.txt"</js>
176    *    <li><js>"MyResource_ja.txt"</js>
177    *    <li><js>"MyResource.txt"</js>
178    * </ol>
179    *
180    * <p>
181    * If the locale is <jk>null</jk>, then it will only return <js>"MyResource.txt"</js>.
182    *
183    * @param fileName The name of the file to get candidate file names on.
184    * @param locale
185    *    The locale.
186    *    <br>If <jk>null</jk>, won't look for localized file names.
187    * @return An iterator of file names to look at.
188    */
189   protected List<String> getCandidateFileNames(final String fileName, final Locale locale) {
190
191      if (locale == null)
192         return Collections.singletonList(fileName);
193
194      List<String> list = new ArrayList<>();
195      String baseName = getBaseName(fileName);
196      String ext = getExtension(fileName);
197
198      getCandidateLocales(locale).forEach(x -> {
199         String ls = x.toString();
200         if (ls.isEmpty())
201            list.add(fileName);
202         else {
203            list.add(baseName + "_" + ls + (ext.isEmpty() ? "" : ('.' + ext)));
204            list.add(ls.replace('_', '/') + '/' + fileName);
205         }
206      });
207
208      return list;
209   }
210
211   /**
212    * Returns the candidate locales for the specified locale.
213    *
214    * <p>
215    * For example, if <c>locale</c> is <js>"ja_JP"</js>, then this method will return:
216    * <ol>
217    *    <li><js>"ja_JP"</js>
218    *    <li><js>"ja"</js>
219    *    <li><js>""</js>
220    * </ol>
221    *
222    * @param locale The locale to get the list of candidate locales for.
223    * @return The list of candidate locales.
224    */
225   protected List<Locale> getCandidateLocales(Locale locale) {
226      return RB_CONTROL.getCandidateLocales("", locale);
227   }
228
229   /**
230    * Checks for path malformations such as use of <js>".."</js> which can be used to open up security holes.
231    *
232    * <p>
233    * Default implementation returns <jk>true</jk> if the path is any of the following:
234    * <ul>
235    *    <li>Is blank or <jk>null</jk>.
236    *    <li>Contains <js>".."</js> (to prevent traversing out of working directory).
237    *    <li>Contains <js>"%"</js> (to prevent URI trickery).
238    * </ul>
239    *
240    * @param path The path to check.
241    * @return <jk>true</jk> if the path is invalid.
242    */
243   protected boolean isInvalidPath(String path) {
244      return isEmpty(path) || path.contains("..") || path.contains("%");
245   }
246
247   /**
248    * Returns <jk>true</jk> if the file should be ignored based on file name.
249    *
250    * @param name The name to check.
251    * @return <jk>true</jk> if the file should be ignored.
252    */
253   protected boolean isIgnoredFile(String name) {
254      for (Pattern p : exclude)
255         if (p.matcher(name).matches())
256            return true;
257      for (Pattern p : include)
258         if (p.matcher(name).matches())
259            return false;
260      return true;
261   }
262
263   @Override
264   public int hashCode() {
265      return hashCode;
266   }
267
268   @Override /* Object */
269   public boolean equals(Object o) {
270      return o instanceof BasicFileFinder && eq(this, (BasicFileFinder)o, (x,y)->eq(x.hashCode, y.hashCode) && eq(x.getClass(), y.getClass()) && eq(x.roots, y.roots) && eq(x.cachingLimit, y.cachingLimit) && eq(x.includePatterns, y.includePatterns) && eq(x.excludePatterns, y.excludePatterns));
271   }
272
273   @Override /* Object */
274   public String toString() {
275      return filteredMap()
276         .append("class", getClass().getSimpleName())
277         .append("roots", roots)
278         .append("cachingLimit", cachingLimit)
279         .append("include", includePatterns)
280         .append("exclude", excludePatterns)
281         .append("hashCode", hashCode)
282         .asReadableString();
283   }
284}