001// Copyright 2006-2013 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// 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 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.ioc.internal.util; 016 017import org.apache.tapestry5.ioc.Resource; 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 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 public final String getPath() 068 { 069 return path; 070 } 071 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 public final String getFolder() 085 { 086 int slashx = path.lastIndexOf('/'); 087 088 return (slashx < 0) ? "" : path.substring(0, slashx); 089 } 090 091 public final Resource forFile(String relativePath) 092 { 093 assert relativePath != null; 094 095 List<String> terms = CollectionFactory.newList(); 096 097 for (String term : getFolder().split("/")) 098 { 099 terms.add(term); 100 } 101 102 for (String term : relativePath.split("/")) 103 { 104 // This will occur if the relative path contains sequential slashes 105 106 if (term.equals("") || term.equals(".")) 107 { 108 continue; 109 } 110 111 if (term.equals("..")) 112 { 113 if (terms.isEmpty()) 114 { 115 throw new IllegalStateException(String.format("Relative path '%s' for %s would go above root.", relativePath, this)); 116 } 117 118 terms.remove(terms.size() - 1); 119 120 continue; 121 } 122 123 // TODO: term blank or otherwise invalid? 124 // TODO: final term should not be "." or "..", or for that matter, the 125 // name of a folder, since a Resource should be a file within 126 // a folder. 127 128 terms.add(term); 129 } 130 131 StringBuilder path = new StringBuilder(100); 132 String sep = ""; 133 134 for (String term : terms) 135 { 136 path.append(sep).append(term); 137 sep = "/"; 138 } 139 140 return createResource(path.toString()); 141 } 142 143 public final Resource forLocale(Locale locale) 144 { 145 try 146 { 147 acquireReadLock(); 148 149 for (Localization l = firstLocalization; l != null; l = l.next) 150 { 151 if (l.locale.equals(locale)) 152 { 153 return l.resource; 154 } 155 } 156 157 return populateLocalizationCache(locale); 158 } finally 159 { 160 releaseReadLock(); 161 } 162 } 163 164 private Resource populateLocalizationCache(Locale locale) 165 { 166 try 167 { 168 upgradeReadLockToWriteLock(); 169 170 // Race condition: another thread may have beaten us to it: 171 172 for (Localization l = firstLocalization; l != null; l = l.next) 173 { 174 if (l.locale.equals(locale)) 175 { 176 return l.resource; 177 } 178 } 179 180 Resource result = findLocalizedResource(locale); 181 182 firstLocalization = new Localization(locale, result, firstLocalization); 183 184 return result; 185 186 } finally 187 { 188 downgradeWriteLockToReadLock(); 189 } 190 } 191 192 private Resource findLocalizedResource(Locale locale) 193 { 194 for (String path : new LocalizedNameGenerator(this.path, locale)) 195 { 196 Resource potential = createResource(path); 197 198 if (potential.exists()) 199 return potential; 200 } 201 202 return null; 203 } 204 205 public final Resource withExtension(String extension) 206 { 207 assert InternalUtils.isNonBlank(extension); 208 int dotx = path.lastIndexOf('.'); 209 210 if (dotx < 0) 211 return createResource(path + "." + extension); 212 213 return createResource(path.substring(0, dotx + 1) + extension); 214 } 215 216 /** 217 * Creates a new resource, unless the path matches the current Resource's path (in which case, this resource is 218 * returned). 219 */ 220 private Resource createResource(String path) 221 { 222 if (this.path.equals(path)) 223 return this; 224 225 return newResource(path); 226 } 227 228 /** 229 * Simple check for whether {@link #toURL()} returns null or not. 230 */ 231 public boolean exists() 232 { 233 try 234 { 235 acquireReadLock(); 236 237 if (!existsComputed) 238 { 239 computeExists(); 240 } 241 242 return exists; 243 } finally 244 { 245 releaseReadLock(); 246 } 247 } 248 249 private void computeExists() 250 { 251 try 252 { 253 upgradeReadLockToWriteLock(); 254 255 if (!existsComputed) 256 { 257 exists = toURL() != null; 258 existsComputed = true; 259 } 260 } finally 261 { 262 downgradeWriteLockToReadLock(); 263 } 264 } 265 266 /** 267 * Obtains the URL for the Resource and opens the stream, wrapped by a BufferedInputStream. 268 */ 269 public InputStream openStream() throws IOException 270 { 271 URL url = toURL(); 272 273 if (url == null) 274 { 275 return null; 276 } 277 278 return new BufferedInputStream(url.openStream()); 279 } 280 281 /** 282 * Factory method provided by subclasses. 283 */ 284 protected abstract Resource newResource(String path); 285 286 /** 287 * Validates that the URL is correct; at this time, a correct URL is one of: 288 * <ul><li>null</li> 289 * <li>a non-file: URL</li> 290 * <li>a file: URL where the case of the file matches the corresponding path element</li> 291 * </ul> 292 * See <a href="https://issues.apache.org/jira/browse/TAP5-1007">TAP5-1007</a> 293 * 294 * @param url 295 * to validate 296 * @since 5.4 297 */ 298 protected void validateURL(URL url) 299 { 300 if (url == null) 301 { 302 return; 303 } 304 305 // Don't have to be concerned with the ClasspathURLConverter since this is intended as a 306 // runtime check during development; it's about ensuring that what works in development on 307 // a case-insensitive file system will work in production on the classpath (or other case sensitive 308 // file system). 309 310 if (!url.getProtocol().equals("file")) 311 { 312 return; 313 } 314 315 File file = toFile(url); 316 317 String expectedFileName = null; 318 319 try 320 { 321 // On Windows, the canonical path uses backslash ('\') for the separator; an easy hack 322 // is to convert the platform file separator to match sane operating systems (which use a foward slash). 323 String sep = System.getProperty("file.separator"); 324 expectedFileName = extractFile(file.getCanonicalPath().replace(sep, "/")); 325 } catch (IOException e) 326 { 327 return; 328 } 329 330 String actualFileName = getFile(); 331 332 if (actualFileName.equals(expectedFileName)) 333 { 334 return; 335 } 336 337 throw new IllegalStateException(String.format("Resource %s does not match the case of the actual file name, '%s'.", 338 this, expectedFileName)); 339 340 } 341 342 private File toFile(URL url) 343 { 344 try 345 { 346 return new File(url.toURI()); 347 } catch (URISyntaxException ex) 348 { 349 return new File(url.getPath()); 350 } 351 } 352}