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<InputStream> 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}