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