001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.ioc.internal.util; 014 015import org.apache.tapestry5.ioc.Resource; 016import org.apache.tapestry5.ioc.util.LocalizedNameGenerator; 017 018import java.io.BufferedInputStream; 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.net.URISyntaxException; 023import java.net.URL; 024import java.util.List; 025import java.util.Locale; 026 027/** 028 * Abstract implementation of {@link Resource}. Subclasses must implement the abstract methods {@link Resource#toURL()} 029 * and {@link #newResource(String)} as well as toString(), hashCode() and equals(). 030 */ 031public abstract class AbstractResource extends LockSupport implements Resource 032{ 033 private static class Localization 034 { 035 final Locale locale; 036 037 final Resource resource; 038 039 final Localization next; 040 041 private Localization(Locale locale, Resource resource, Localization next) 042 { 043 this.locale = locale; 044 this.resource = resource; 045 this.next = next; 046 } 047 } 048 049 private final String path; 050 051 // Guarded by Lock 052 private boolean exists, existsComputed; 053 054 // Guarded by lock 055 private Localization firstLocalization; 056 057 protected AbstractResource(String path) 058 { 059 assert path != null; 060 061 // Normalize paths to NOT start with a leading slash 062 this.path = path.startsWith("/") ? path.substring(1) : path; 063 } 064 065 @Override 066 public final String getPath() 067 { 068 return path; 069 } 070 071 @Override 072 public final String getFile() 073 { 074 return extractFile(path); 075 } 076 077 private static String extractFile(String path) 078 { 079 int slashx = path.lastIndexOf('/'); 080 081 return path.substring(slashx + 1); 082 } 083 084 @Override 085 public final String getFolder() 086 { 087 int slashx = path.lastIndexOf('/'); 088 089 return (slashx < 0) ? "" : path.substring(0, slashx); 090 } 091 092 @Override 093 public final Resource forFile(String relativePath) 094 { 095 assert relativePath != null; 096 097 List<String> terms = CollectionFactory.newList(); 098 099 for (String term : getFolder().split("/")) 100 { 101 terms.add(term); 102 } 103 104 // Handling systems using backslash as the path separator, such as Windows 105 relativePath = relativePath.replace('\\', '/'); 106 107 for (String term : relativePath.split("/")) 108 { 109 // This will occur if the relative path contains sequential slashes 110 111 if (term.equals("") || term.equals(".")) 112 { 113 continue; 114 } 115 116 if (term.equals("..")) 117 { 118 if (terms.isEmpty()) 119 { 120 throw new IllegalStateException(String.format("Relative path '%s' for %s would go above root.", relativePath, this)); 121 } 122 123 terms.remove(terms.size() - 1); 124 125 continue; 126 } 127 128 // TODO: term blank or otherwise invalid? 129 // TODO: final term should not be "." or "..", or for that matter, the 130 // name of a folder, since a Resource should be a file within 131 // a folder. 132 133 terms.add(term); 134 } 135 136 StringBuilder path = new StringBuilder(100); 137 String sep = ""; 138 139 for (String term : terms) 140 { 141 path.append(sep).append(term); 142 sep = "/"; 143 } 144 145 return createResource(path.toString()); 146 } 147 148 @Override 149 public final Resource forLocale(Locale locale) 150 { 151 try 152 { 153 acquireReadLock(); 154 155 for (Localization l = firstLocalization; l != null; l = l.next) 156 { 157 if (l.locale.equals(locale)) 158 { 159 return l.resource; 160 } 161 } 162 163 return populateLocalizationCache(locale); 164 } finally 165 { 166 releaseReadLock(); 167 } 168 } 169 170 private Resource populateLocalizationCache(Locale locale) 171 { 172 try 173 { 174 upgradeReadLockToWriteLock(); 175 176 // Race condition: another thread may have beaten us to it: 177 178 for (Localization l = firstLocalization; l != null; l = l.next) 179 { 180 if (l.locale.equals(locale)) 181 { 182 return l.resource; 183 } 184 } 185 186 Resource result = findLocalizedResource(locale); 187 188 firstLocalization = new Localization(locale, result, firstLocalization); 189 190 return result; 191 192 } finally 193 { 194 downgradeWriteLockToReadLock(); 195 } 196 } 197 198 private Resource findLocalizedResource(Locale locale) 199 { 200 for (String path : new LocalizedNameGenerator(this.path, locale)) 201 { 202 Resource potential = createResource(path); 203 204 if (potential.exists()) 205 return potential; 206 } 207 208 return null; 209 } 210 211 @Override 212 public final Resource withExtension(String extension) 213 { 214 assert InternalUtils.isNonBlank(extension); 215 int dotx = path.lastIndexOf('.'); 216 217 if (dotx < 0) 218 return createResource(path + "." + extension); 219 220 return createResource(path.substring(0, dotx + 1) + extension); 221 } 222 223 /** 224 * Creates a new resource, unless the path matches the current Resource's path (in which case, this resource is 225 * returned). 226 */ 227 private Resource createResource(String path) 228 { 229 if (this.path.equals(path)) 230 return this; 231 232 return newResource(path); 233 } 234 235 /** 236 * Simple check for whether {@link #toURL()} returns null or not. 237 */ 238 @Override 239 public boolean exists() 240 { 241 try 242 { 243 acquireReadLock(); 244 245 if (!existsComputed) 246 { 247 computeExists(); 248 } 249 250 return exists; 251 } finally 252 { 253 releaseReadLock(); 254 } 255 } 256 257 private void computeExists() 258 { 259 try 260 { 261 upgradeReadLockToWriteLock(); 262 263 if (!existsComputed) 264 { 265 exists = toURL() != null; 266 existsComputed = true; 267 } 268 } finally 269 { 270 downgradeWriteLockToReadLock(); 271 } 272 } 273 274 /** 275 * Obtains the URL for the Resource and opens the stream, wrapped by a BufferedInputStream. 276 */ 277 @Override 278 public InputStream openStream() throws IOException 279 { 280 URL url = toURL(); 281 282 if (url == null) 283 { 284 return null; 285 } 286 if ("jar".equals(url.getProtocol())){ 287 288 289 // TAP5-2448: make sure that the URL does not reference a directory 290 String urlAsString = url.toString(); 291 292 int indexOfExclamationMark = urlAsString.indexOf('!'); 293 294 String resourceInJar = urlAsString.substring(indexOfExclamationMark + 2); 295 296 URL directoryResource = Thread.currentThread().getContextClassLoader().getResource(resourceInJar + "/"); 297 298 boolean isDirectory = directoryResource != null && "jar".equals(directoryResource.getProtocol()); 299 300 if (isDirectory) 301 { 302 throw new IOException("Cannot open a stream for a resource that references a directory inside a JAR file (" + url + ")."); 303 } 304 305 } 306 307 return new BufferedInputStream(url.openStream()); 308 } 309 310 /** 311 * Factory method provided by subclasses. 312 */ 313 protected abstract Resource newResource(String path); 314 315 /** 316 * Validates that the URL is correct; at this time, a correct URL is one of: 317 * <ul><li>null</li> 318 * <li>a non-file: URL</li> 319 * <li>a file: URL where the case of the file matches the corresponding path element</li> 320 * </ul> 321 * See <a href="https://issues.apache.org/jira/browse/TAP5-1007">TAP5-1007</a> 322 * 323 * @param url 324 * to validate 325 * @since 5.4 326 */ 327 protected void validateURL(URL url) 328 { 329 if (url == null) 330 { 331 return; 332 } 333 334 // Don't have to be concerned with the ClasspathURLConverter since this is intended as a 335 // runtime check during development; it's about ensuring that what works in development on 336 // a case-insensitive file system will work in production on the classpath (or other case sensitive 337 // file system). 338 339 if (!url.getProtocol().equals("file")) 340 { 341 return; 342 } 343 344 File file = toFile(url); 345 346 String expectedFileName = null; 347 348 try 349 { 350 // On Windows, the canonical path uses backslash ('\') for the separator; an easy hack 351 // is to convert the platform file separator to match sane operating systems (which use a foward slash). 352 String sep = System.getProperty("file.separator"); 353 expectedFileName = extractFile(file.getCanonicalPath().replace(sep, "/")); 354 } catch (IOException e) 355 { 356 return; 357 } 358 359 String actualFileName = getFile(); 360 361 if (actualFileName.equals(expectedFileName)) 362 { 363 return; 364 } 365 366 throw new IllegalStateException(String.format("Resource %s does not match the case of the actual file name, '%s'.", 367 this, expectedFileName)); 368 369 } 370 371 private File toFile(URL url) 372 { 373 try 374 { 375 return new File(url.toURI()); 376 } catch (URISyntaxException ex) 377 { 378 return new File(url.getPath()); 379 } 380 } 381 382 @Override 383 public boolean isVirtual() 384 { 385 return false; 386 } 387}