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