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.internal.FileUtils.*; 016import static org.apache.juneau.internal.StringUtils.*; 017 018import java.io.*; 019import java.util.*; 020import java.util.ResourceBundle.*; 021 022/** 023 * Utility class for finding resources for a class. 024 * 025 * <p> 026 * If the <c>locale</c> is specified, then we look for resources whose name matches that locale. 027 * For example, if looking for the resource <js>"MyResource.txt"</js> for the Japanese locale, we will look for 028 * files in the following order: 029 * <ol> 030 * <li><js>"MyResource_ja_JP.txt"</js> 031 * <li><js>"MyResource_ja.txt"</js> 032 * <li><js>"MyResource.txt"</js> 033 * </ol> 034 * 035 * <p> 036 * The default behavior first searches the working filesystem directory for matching files. 037 * <br>Path traversals outside the working directory are not allowed for security reasons. 038 * 039 * <p> 040 * Support is provided for recursively searching for files up the class hierarchy chain. 041 */ 042public class BasicResourceFinder implements ResourceFinder { 043 044 /** 045 * Reusable instance. 046 */ 047 public static final BasicResourceFinder INSTANCE = new BasicResourceFinder(); 048 049 private static final ResourceBundle.Control RB_CONTROL = ResourceBundle.Control.getControl(Control.FORMAT_DEFAULT); 050 private static final List<Locale> ROOT_LOCALE = Arrays.asList(Locale.ROOT); 051 052 private final boolean includeFileSystem, recursive; 053 054 /** 055 * Constructor. 056 * 057 * <p> 058 * Same as calling <c>new BasicClasspathResourceFinder(<jk>true</jk>, <jk>false</jk>. 059 */ 060 public BasicResourceFinder() { 061 this(true, false); 062 } 063 064 /** 065 * Constructor. 066 * 067 * @param includeFileSystem Search the working filesystem directory for matching resources first. The default is <jk>true</jk>. 068 * @param recursive Recursively search up the parent class hierarchy for resources. 069 */ 070 public BasicResourceFinder(boolean includeFileSystem, boolean recursive) { 071 this.includeFileSystem = includeFileSystem; 072 this.recursive = recursive; 073 } 074 075 @SuppressWarnings("resource") 076 @Override /* ClasspathResourceFinder */ 077 public InputStream findResource(Class<?> baseClass, String name, Locale locale) throws IOException { 078 if (isInvalidName(name)) 079 return null; 080 InputStream is = null; 081 if (includeFileSystem) 082 is = findFileSystemResource(name, locale); 083 while (is == null && baseClass != null) { 084 is = findClasspathResource(baseClass, name, locale); 085 baseClass = recursive ? baseClass.getSuperclass() : null; 086 } 087 return is; 088 } 089 090 /** 091 * Workhorse method for retrieving a resource from the classpath. 092 * 093 * <p> 094 * This method can be overridden by subclasses to provide customized handling of resource retrieval from the classpath. 095 * 096 * @param baseClass The base class providing the classloader. 097 * @param name The resource name. 098 * @param locale 099 * The resource locale. 100 * <br>If <jk>null</jk>, won't look for localized file names. 101 * @return The resource stream, or <jk>null</jk> if it couldn't be found. 102 * @throws IOException Thrown by underlying stream. 103 */ 104 protected InputStream findClasspathResource(Class<?> baseClass, String name, Locale locale) throws IOException { 105 106 if (locale == null) 107 return baseClass.getResourceAsStream(name); 108 109 for (String n : getCandidateFileNames(name, locale)) { 110 InputStream is = baseClass.getResourceAsStream(n); 111 if (is != null) 112 return is; 113 } 114 return null; 115 } 116 117 /** 118 * Workhorse method for retrieving a resource from the file system. 119 * 120 * <p> 121 * This method can be overridden by subclasses to provide customized handling of resource retrieval from file systems. 122 * 123 * @param name The resource name. 124 * @param locale 125 * The resource locale. 126 * <br>Can be <jk>null</jk>. 127 * @return The resource stream, or <jk>null</jk> if it couldn't be found. 128 * @throws IOException Thrown by underlying stream. 129 */ 130 protected InputStream findFileSystemResource(String name, Locale locale) throws IOException { 131 for (String n2 : getCandidateFileNames(name, locale)) { 132 File f = new File(n2); 133 if (f.exists() && f.isFile() && f.canRead() && ! f.isAbsolute()) { 134 return new FileInputStream(f); 135 } 136 } 137 return null; 138 } 139 140 /* 141 * Returns the candidate file names for the specified file name in the specified locale. 142 * 143 * <p> 144 * For example, if looking for the <js>"MyResource.txt"</js> file in the Japanese locale, the iterator will return 145 * names in the following order: 146 * <ol> 147 * <li><js>"MyResource_ja_JP.txt"</js> 148 * <li><js>"MyResource_ja.txt"</js> 149 * <li><js>"MyResource.txt"</js> 150 * </ol> 151 * 152 * <p> 153 * If the locale is <jk>null</jk>, then it will only return <js>"MyResource.txt"</js>. 154 * 155 * @param fileName The name of the file to get candidate file names on. 156 * @param l 157 * The locale. 158 * <br>If <jk>null</jk>, won't look for localized file names. 159 * @return An iterator of file names to look at. 160 */ 161 Iterable<String> getCandidateFileNames(final String fileName, final Locale l) { 162 return new Iterable<String>() { 163 @Override 164 public Iterator<String> iterator() { 165 return new Iterator<String>() { 166 final Iterator<Locale> locales = getCandidateLocales(l).iterator(); 167 String baseName, ext; 168 169 @Override 170 public boolean hasNext() { 171 return locales.hasNext(); 172 } 173 174 @Override 175 public String next() { 176 Locale l2 = locales.next(); 177 if (l2.toString().isEmpty()) 178 return fileName; 179 if (baseName == null) 180 baseName = getBaseName(fileName); 181 if (ext == null) 182 ext = getExtension(fileName); 183 return baseName + "_" + l2.toString() + (ext.isEmpty() ? "" : ('.' + ext)); 184 } 185 @Override 186 public void remove() { 187 throw new UnsupportedOperationException(); 188 } 189 }; 190 } 191 }; 192 } 193 194 /* 195 * Returns the candidate locales for the specified locale. 196 * 197 * <p> 198 * For example, if <c>locale</c> is <js>"ja_JP"</js>, then this method will return: 199 * <ol> 200 * <li><js>"ja_JP"</js> 201 * <li><js>"ja"</js> 202 * <li><js>""</js> 203 * </ol> 204 * 205 * @param locale The locale to get the list of candidate locales for. 206 * @return The list of candidate locales. 207 */ 208 static final List<Locale> getCandidateLocales(Locale locale) { 209 if (locale == null) 210 return ROOT_LOCALE; 211 return RB_CONTROL.getCandidateLocales("", locale); 212 } 213 214 private static boolean isInvalidName(String name) { 215 return isEmpty(name) || name.contains(".."); 216 } 217}