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