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