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 for (String term : relativePath.split("/")) 105 { 106 // This will occur if the relative path contains sequential slashes 107 108 if (term.equals("") || term.equals(".")) 109 { 110 continue; 111 } 112 113 if (term.equals("..")) 114 { 115 if (terms.isEmpty()) 116 { 117 throw new IllegalStateException(String.format("Relative path '%s' for %s would go above root.", relativePath, this)); 118 } 119 120 terms.remove(terms.size() - 1); 121 122 continue; 123 } 124 125 // TODO: term blank or otherwise invalid? 126 // TODO: final term should not be "." or "..", or for that matter, the 127 // name of a folder, since a Resource should be a file within 128 // a folder. 129 130 terms.add(term); 131 } 132 133 StringBuilder path = new StringBuilder(100); 134 String sep = ""; 135 136 for (String term : terms) 137 { 138 path.append(sep).append(term); 139 sep = "/"; 140 } 141 142 return createResource(path.toString()); 143 } 144 145 @Override 146 public final Resource forLocale(Locale locale) 147 { 148 try 149 { 150 acquireReadLock(); 151 152 for (Localization l = firstLocalization; l != null; l = l.next) 153 { 154 if (l.locale.equals(locale)) 155 { 156 return l.resource; 157 } 158 } 159 160 return populateLocalizationCache(locale); 161 } finally 162 { 163 releaseReadLock(); 164 } 165 } 166 167 private Resource populateLocalizationCache(Locale locale) 168 { 169 try 170 { 171 upgradeReadLockToWriteLock(); 172 173 // Race condition: another thread may have beaten us to it: 174 175 for (Localization l = firstLocalization; l != null; l = l.next) 176 { 177 if (l.locale.equals(locale)) 178 { 179 return l.resource; 180 } 181 } 182 183 Resource result = findLocalizedResource(locale); 184 185 firstLocalization = new Localization(locale, result, firstLocalization); 186 187 return result; 188 189 } finally 190 { 191 downgradeWriteLockToReadLock(); 192 } 193 } 194 195 private Resource findLocalizedResource(Locale locale) 196 { 197 for (String path : new LocalizedNameGenerator(this.path, locale)) 198 { 199 Resource potential = createResource(path); 200 201 if (potential.exists()) 202 return potential; 203 } 204 205 return null; 206 } 207 208 @Override 209 public final Resource withExtension(String extension) 210 { 211 assert InternalUtils.isNonBlank(extension); 212 int dotx = path.lastIndexOf('.'); 213 214 if (dotx < 0) 215 return createResource(path + "." + extension); 216 217 return createResource(path.substring(0, dotx + 1) + extension); 218 } 219 220 /** 221 * Creates a new resource, unless the path matches the current Resource's path (in which case, this resource is 222 * returned). 223 */ 224 private Resource createResource(String path) 225 { 226 if (this.path.equals(path)) 227 return this; 228 229 return newResource(path); 230 } 231 232 /** 233 * Simple check for whether {@link #toURL()} returns null or not. 234 */ 235 @Override 236 public boolean exists() 237 { 238 try 239 { 240 acquireReadLock(); 241 242 if (!existsComputed) 243 { 244 computeExists(); 245 } 246 247 return exists; 248 } finally 249 { 250 releaseReadLock(); 251 } 252 } 253 254 private void computeExists() 255 { 256 try 257 { 258 upgradeReadLockToWriteLock(); 259 260 if (!existsComputed) 261 { 262 exists = toURL() != null; 263 existsComputed = true; 264 } 265 } finally 266 { 267 downgradeWriteLockToReadLock(); 268 } 269 } 270 271 /** 272 * Obtains the URL for the Resource and opens the stream, wrapped by a BufferedInputStream. 273 */ 274 @Override 275 public InputStream openStream() throws IOException 276 { 277 URL url = toURL(); 278 279 if (url == null) 280 { 281 return null; 282 } 283 if ("jar".equals(url.getProtocol())){ 284 285 286 // TAP5-2448: make sure that the URL does not reference a directory 287 String urlAsString = url.toString(); 288 289 int indexOfExclamationMark = urlAsString.indexOf('!'); 290 291 String resourceInJar = urlAsString.substring(indexOfExclamationMark + 2); 292 293 URL directoryResource = Thread.currentThread().getContextClassLoader().getResource(resourceInJar + "/"); 294 295 boolean isDirectory = directoryResource != null && "jar".equals(directoryResource.getProtocol()); 296 297 if (isDirectory) 298 { 299 throw new IOException("Cannot open a stream for a resource that references a directory inside a JAR file (" + url + ")."); 300 } 301 302 } 303 304 return new BufferedInputStream(url.openStream()); 305 } 306 307 /** 308 * Factory method provided by subclasses. 309 */ 310 protected abstract Resource newResource(String path); 311 312 /** 313 * Validates that the URL is correct; at this time, a correct URL is one of: 314 * <ul><li>null</li> 315 * <li>a non-file: URL</li> 316 * <li>a file: URL where the case of the file matches the corresponding path element</li> 317 * </ul> 318 * See <a href="https://issues.apache.org/jira/browse/TAP5-1007">TAP5-1007</a> 319 * 320 * @param url 321 * to validate 322 * @since 5.4 323 */ 324 protected void validateURL(URL url) 325 { 326 if (url == null) 327 { 328 return; 329 } 330 331 // Don't have to be concerned with the ClasspathURLConverter since this is intended as a 332 // runtime check during development; it's about ensuring that what works in development on 333 // a case-insensitive file system will work in production on the classpath (or other case sensitive 334 // file system). 335 336 if (!url.getProtocol().equals("file")) 337 { 338 return; 339 } 340 341 File file = toFile(url); 342 343 String expectedFileName = null; 344 345 try 346 { 347 // On Windows, the canonical path uses backslash ('\') for the separator; an easy hack 348 // is to convert the platform file separator to match sane operating systems (which use a foward slash). 349 String sep = System.getProperty("file.separator"); 350 expectedFileName = extractFile(file.getCanonicalPath().replace(sep, "/")); 351 } catch (IOException e) 352 { 353 return; 354 } 355 356 String actualFileName = getFile(); 357 358 if (actualFileName.equals(expectedFileName)) 359 { 360 return; 361 } 362 363 throw new IllegalStateException(String.format("Resource %s does not match the case of the actual file name, '%s'.", 364 this, expectedFileName)); 365 366 } 367 368 private File toFile(URL url) 369 { 370 try 371 { 372 return new File(url.toURI()); 373 } catch (URISyntaxException ex) 374 { 375 return new File(url.getPath()); 376 } 377 } 378 379 @Override 380 public boolean isVirtual() 381 { 382 return false; 383 } 384}