/**
* Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onebusaway.presentation.impl.resources;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.text.DateFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.PostConstruct;
import javax.servlet.ServletContext;
import org.json.JSONObject;
import org.onebusaway.presentation.impl.ServletLibrary;
import org.onebusaway.presentation.services.resources.Resource;
import org.onebusaway.presentation.services.resources.ResourceService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Component;
import com.opensymphony.xwork2.LocaleProvider;
import com.opensymphony.xwork2.TextProvider;
import com.opensymphony.xwork2.TextProviderFactory;
@Component
public class ResourceServiceImpl implements ResourceService {
public static final String DEBUG_PROPERTY = ResourceServiceImpl.class.getName()
+ ".debug";
private static final String PREFIX_CLASSPATH = "classpath:";
private static final String PREFIX_COLLECTION = "collection:";
private static final String PREFIX_COLLECTION_ENTRY = "collection-entry:";
private static final String PREFIX_MESSAGES = "messages:";
private static final String PREFIX_MESSAGES_DATE_LIBRARY = "DateLibrary";
private static final String PREFIX_FILE = "file:";
private static final Pattern _resourcePattern = Pattern.compile("^(.*)-\\w+\\.cache(\\.\\w+){0,1}$");
private static Logger _log = LoggerFactory.getLogger(ResourceServiceImpl.class);
private static TextProviderFactory _textProviderFactory = new TextProviderFactory();
private ConcurrentMap<String, Resource> _resourceEntriesByResourcePath = new ConcurrentHashMap<String, Resource>();
private ConcurrentMap<String, Resource> _resourceEntriesByExternalId = new ConcurrentHashMap<String, Resource>();
private ConcurrentMap<String, List<String>> _resourcePathsById = new ConcurrentHashMap<String, List<String>>();
/****
*
****/
private String _prefix;
private String _pattern;
private File _tempDir;
private ServletContext _servletContext;
private String _contextPath;
private boolean _debug = false;
public void setPrefix(String prefix) {
_prefix = prefix;
}
public void setPattern(String pattern) {
_pattern = pattern;
}
@Autowired
public void setServletContext(ServletContext servletContext) {
_servletContext = servletContext;
_contextPath = ServletLibrary.getContextPath(_servletContext);
File tmpDir = (File) _servletContext.getAttribute("javax.servlet.context.tempdir");
if (tmpDir == null) {
_log.warn("NO ServletContext TEMP DIR!");
tmpDir = new File(System.getProperty("java.io.tmpdir"));
}
_tempDir = new File(tmpDir, "OneBusAwayResources");
if (!_tempDir.exists())
_tempDir.mkdirs();
}
@PostConstruct
public void setup() {
String value = System.getProperty(DEBUG_PROPERTY, "false");
if (value != null && value.toLowerCase().equals("true"))
_debug = true;
}
/****
* {@link ResourceService} Interface
****/
@Override
public String getExternalUrlForResource(String resourcePath, Locale locale) {
LocaleProvider localeProvider = new LocaleProviderImpl(locale);
Resource resource = getResourceForPath(resourcePath, localeProvider, null);
if (resource == null) {
_log.warn("resource not found: " + resourcePath);
return null;
}
if (_debug)
refreshResource(resource);
return resource.getExternalUrl();
}
@Override
public String getExternalUrlForResources(List<String> resourcePaths,
Locale locale) {
return getExternalUrlForResources(null, resourcePaths, locale);
}
@Override
public String getExternalUrlForResources(String resourceId,
List<String> resourcePaths, Locale locale) {
LocaleProvider localeProvider = new LocaleProviderImpl(locale);
Resource resource = getResourceForPaths(resourceId, resourcePaths,
localeProvider);
if (resource == null) {
_log.warn("resource not found: " + resourceId);
return null;
}
if (_debug)
refreshResource(resource);
return resource.getExternalUrl();
}
@Override
public Resource getLocalResourceForExternalId(String externalId, Locale locale) {
Resource resource = _resourceEntriesByExternalId.get(externalId);
if (resource == null) {
/**
* In case the resource has not been first requested as a resource(url)
* first
*/
String resourcePath = getExternalIdAsResourcePath(externalId);
if (resourcePath != null) {
LocaleProvider localeProvider = new LocaleProviderImpl(locale);
/**
* First we see if this is a resource identified by id
*/
if (_resourcePathsById.containsKey(resourcePath)) {
List<String> paths = _resourcePathsById.get(resourcePath);
resource = getResourceForPaths(resourcePath, paths, localeProvider);
}
if (resource == null)
resource = getResourceForPath(resourcePath, localeProvider, null);
}
}
if (resource == null) {
_log.warn("resource not found for external id: " + externalId);
return null;
}
return resource;
}
/****
* Private Methods
****/
private String getResourcePathAsKey(String resourcePath,
LocaleProvider localeProvider) {
if (resourcePath.startsWith(PREFIX_MESSAGES)) {
Locale locale = localeProvider.getLocale();
return resourcePath + "-" + locale.toString();
}
return resourcePath;
}
private Resource getResourceForPath(String resourcePath,
LocaleProvider localeProvider, URL sourceUrl) {
String resourcePathKey = getResourcePathAsKey(resourcePath, localeProvider);
Resource resource = _resourceEntriesByResourcePath.get(resourcePathKey);
if (resource == null) {
resource = createResourceForPath(resourcePath, sourceUrl, localeProvider);
if (resource == null)
return null;
Resource existingResource = _resourceEntriesByResourcePath.putIfAbsent(
resourcePathKey, resource);
if (existingResource != null)
return existingResource;
}
return resource;
}
private ResourceEntry createResourceForPath(String resourcePath,
URL sourceUrl, LocaleProvider localeProvider) {
if (sourceUrl == null)
sourceUrl = getResourceAsSourceUrl(resourcePath, localeProvider);
/**
* If we can't find a source URL, then we can't create an entry
*/
if (sourceUrl == null) {
return null;
}
File localFile = getBundleResourceAsLocalFile(resourcePath, sourceUrl);
ResourceTransformationStrategy strategy = getResourceTransformationStrategyForResource(
resourcePath, localeProvider);
ResourceEntry resource = new ResourceEntry(resourcePath, sourceUrl,
localFile, strategy);
generateLocalResourceAndExternalUrl(resource);
return resource;
}
private URL getResourceAsSourceUrl(String resourceName,
LocaleProvider localeProvider) {
if (resourceName.startsWith(PREFIX_COLLECTION)) {
resourceName = resourceName.substring(PREFIX_COLLECTION.length());
return getCollectionResourceAsSourceUrl(resourceName, localeProvider);
}
if (resourceName.startsWith(PREFIX_MESSAGES)) {
resourceName = resourceName.substring(PREFIX_MESSAGES.length());
if (PREFIX_MESSAGES_DATE_LIBRARY.equals(resourceName)) {
return getDateLibraryMessagesResourceAsSourceUrl(localeProvider);
} else {
return getMessagesResourceAsSourceUrl(resourceName, localeProvider);
}
}
if (resourceName.startsWith(PREFIX_CLASSPATH)) {
resourceName = resourceName.substring(PREFIX_CLASSPATH.length());
ClassLoader loader = getClass().getClassLoader();
URL resource = loader.getResource(resourceName);
if (resource == null)
_log.warn("unknown classpath resource: name=" + resourceName);
return resource;
}
if (resourceName.startsWith(PREFIX_FILE)) {
resourceName = resourceName.substring(PREFIX_FILE.length());
File file = new File(resourceName);
try {
return file.toURI().toURL();
} catch (MalformedURLException e) {
throw new IllegalStateException("error requesting file url: "
+ resourceName);
}
}
try {
return _servletContext.getResource(resourceName);
} catch (MalformedURLException ex) {
throw new IllegalStateException("error requesting servlet url: "
+ resourceName, ex);
}
}
private URL getCollectionResourceAsSourceUrl(String resourceName,
LocaleProvider localeProvider) {
int index = resourceName.indexOf('=');
if (index == -1)
throw new IllegalStateException("invalid resource collection specifier: "
+ resourceName);
String collectionPrefix = resourceName.substring(0, index);
String collectionResourcePath = resourceName.substring(index + 1);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Map<String, String> resourceMapping = new HashMap<String, String>();
try {
org.springframework.core.io.Resource[] resources = resolver.getResources(collectionResourcePath);
for (org.springframework.core.io.Resource resource : resources) {
URL url = resource.getURL();
String name = getLocalUrlAsResourceName(url);
Resource r = getResourceForPath(name, localeProvider, url);
if (r != null) {
String path = url.getPath();
int sepIndex = path.lastIndexOf(File.separator);
if (sepIndex != -1)
path = path.substring(sepIndex + 1);
resourceMapping.put(path, r.getExternalUrl());
String alternateId = PREFIX_COLLECTION_ENTRY + collectionPrefix + ":"
+ path;
_resourceEntriesByResourcePath.put(alternateId, r);
}
}
File file = getOutputFile(PREFIX_COLLECTION + collectionPrefix + ".js");
PrintWriter out = new PrintWriter(file);
JSONObject obj = new JSONObject(resourceMapping);
out.println("var OBA = window.OBA || {};");
out.println("if(!OBA.Resources) { OBA.Resources = {}; }");
out.println("OBA.Resources." + collectionPrefix + " = " + obj.toString()
+ ";");
out.close();
return getFileAsUrl(file);
} catch (IOException ex) {
throw new IllegalStateException("error loading resources", ex);
}
}
private URL getMessagesResourceAsSourceUrl(String resourceName,
LocaleProvider localeProvider) {
int index = resourceName.indexOf('=');
if (index == -1)
throw new IllegalStateException("invalid resource messages specifier: "
+ resourceName);
String messagesPrefix = resourceName.substring(0, index);
String messagesResourceClassName = resourceName.substring(index + 1);
Class<?> messagesResourceClass = null;
try {
messagesResourceClass = Class.forName(messagesResourceClassName);
} catch (Throwable ex) {
throw new IllegalStateException("error loading messages resource class "
+ messagesResourceClassName, ex);
}
TextProvider provider = _textProviderFactory.createInstance(
messagesResourceClass, localeProvider);
ResourceBundle bundle = provider.getTexts();
Map<String, String> resourceMapping = new HashMap<String, String>();
for (Enumeration<String> en = bundle.getKeys(); en.hasMoreElements();) {
String key = en.nextElement();
String value = bundle.getString(key);
resourceMapping.put(key, value);
}
try {
File file = getOutputFile(PREFIX_MESSAGES + messagesPrefix + ".js");
PrintWriter out = new PrintWriter(file);
JSONObject obj = new JSONObject(resourceMapping);
out.println("var OBA = window.OBA || {};");
out.println("if(!OBA.Resources) { OBA.Resources = {}; }");
out.println("OBA.Resources." + messagesPrefix + " = " + obj.toString()
+ ";");
out.close();
return getFileAsUrl(file);
} catch (IOException ex) {
throw new IllegalStateException("error loading resources", ex);
}
}
private URL getDateLibraryMessagesResourceAsSourceUrl(
LocaleProvider localeProvider) {
String messagesPrefix = PREFIX_MESSAGES_DATE_LIBRARY;
DateFormatSymbols symbols = DateFormatSymbols.getInstance(localeProvider.getLocale());
Map<String, Object> resourceMapping = new HashMap<String, Object>();
resourceMapping.put("amPm", Arrays.asList(symbols.getAmPmStrings()));
resourceMapping.put("eras", Arrays.asList(symbols.getEras()));
resourceMapping.put("months", Arrays.asList(symbols.getMonths()));
resourceMapping.put("shortMonths", Arrays.asList(symbols.getShortMonths()));
resourceMapping.put("weekdays", Arrays.asList(symbols.getWeekdays()));
resourceMapping.put("shortWeekdays", Arrays.asList(symbols.getShortWeekdays()));
try {
File file = getOutputFile(PREFIX_MESSAGES + messagesPrefix + ".js");
PrintWriter out = new PrintWriter(file);
JSONObject obj = new JSONObject(resourceMapping);
out.println("var OBA = window.OBA || {};");
out.println("if(!OBA.Resources) { OBA.Resources = {}; }");
out.println("OBA.Resources." + messagesPrefix + " = " + obj.toString()
+ ";");
out.close();
return getFileAsUrl(file);
} catch (IOException ex) {
throw new IllegalStateException("error loading resources", ex);
}
}
private File getBundleResourceAsLocalFile(String resourcePath, URL resourceUrl) {
String protocol = resourceUrl.getProtocol();
if ("file".equals(protocol))
return new File(resourceUrl.getPath());
if ("jar".equals(protocol)) {
String path = resourceUrl.getPath();
int index = path.indexOf('!');
if (index != -1)
path = path.substring(0, index);
try {
URL jarResourceUrl = new URL(path);
File file = new File(jarResourceUrl.getPath());
if (file.exists())
return file;
} catch (MalformedURLException e) {
}
}
File path = new File(_servletContext.getRealPath(resourcePath));
if (path.exists())
return path;
return null;
}
private ResourceTransformationStrategy getResourceTransformationStrategyForResource(
String resourcePath, LocaleProvider localeProvider) {
if (resourcePath.endsWith(".css"))
return new CssResourceTransformationStrategy(localeProvider.getLocale());
return new DefaultResourceTransformationStrategy();
}
private boolean refreshResource(Resource resource) {
if (resource instanceof ResourceEntry) {
return refreshResourceEntry((ResourceEntry) resource);
} else if (resource instanceof ResourcesEntry) {
return refreshResourcesEntry((ResourcesEntry) resource);
}
return false;
}
private boolean refreshResourceEntry(ResourceEntry resource) {
/**
* The refresh mechanism only applies when we can map a resource to a local
* file
*/
File sourceFile = resource.getSourceFile();
if (sourceFile == null)
return false;
synchronized (resource) {
long lastModifiedTime = resource.getLastModifiedTime();
if (lastModifiedTime >= sourceFile.lastModified())
return false;
String existingId = resource.getExternalId();
_resourceEntriesByExternalId.remove(existingId);
generateLocalResourceAndExternalUrl(resource);
return true;
}
}
private void generateLocalResourceAndExternalUrl(ResourceEntry resource) {
ResourceTransformationStrategy strategy = resource.getTransformationStrategy();
URL localUrl = resource.getSourceResource();
long contentLength = -1;
if (strategy.requiresTransformation()) {
File outputFile = getOutputFile(resource.getResourcePath());
strategy.transformResource(this, localUrl, outputFile);
localUrl = getFileAsUrl(outputFile);
contentLength = outputFile.length();
}
if (contentLength == -1)
contentLength = computeContentLengthForLocalUrl(localUrl);
resource.setLocalUrl(localUrl);
resource.setContentLength(contentLength);
String key = getResourceKey(localUrl);
String externalId = constructExternalId(resource.getResourcePath(), key);
String externalUrl = constructExternalUrl(externalId);
resource.setExternalId(externalId);
resource.setExternalUrl(externalUrl);
_resourceEntriesByExternalId.put(externalId, resource);
}
private long computeContentLengthForLocalUrl(URL localUrl) {
InputStream in = null;
try {
in = localUrl.openStream();
long contentLength = 0;
byte[] buffer = new byte[1024];
while (true) {
int rc = in.read(buffer);
if (rc == -1)
return contentLength;
contentLength += rc;
}
} catch (IOException ex) {
throw new IllegalStateException("error reading local url " + localUrl, ex);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ex) {
_log.warn("error closing local url " + localUrl, ex);
}
}
}
}
private String getResourceKey(URL localResource) {
try {
InputStream in = localResource.openStream();
return ResourceSupport.getHash(in);
} catch (IOException ex) {
throw new IllegalStateException("error constructing key for resource: "
+ localResource, ex);
}
}
private String constructExternalId(String resourcePath, String resourceKey) {
String resourceExtension = null;
int index = resourcePath.lastIndexOf('.');
if (index != -1) {
resourceExtension = resourcePath.substring(index + 1);
resourcePath = resourcePath.substring(0, index);
}
StringBuilder b = new StringBuilder();
b.append(resourcePath);
if (!_debug) {
b.append('-');
b.append(resourceKey);
b.append(".cache");
}
if (resourceExtension != null && resourceExtension.length() > 0)
b.append('.').append(resourceExtension);
String url = b.toString();
return url;
}
private String getExternalIdAsResourcePath(String externalId) {
if (_debug)
return externalId;
Matcher m = _resourcePattern.matcher(externalId);
if (!m.matches())
return null;
String resourcePath = m.group(1);
if (m.groupCount() > 1)
resourcePath += m.group(2);
return resourcePath;
}
private String constructExternalUrl(String externalId) {
externalId = externalId.replaceAll("/", "%2f");
externalId = externalId.replaceAll("\\*", "%2a");
externalId = externalId.replaceAll(":", "%3a");
String url = externalId;
if (_pattern != null)
url = _pattern.replaceAll("\\{\\}", url);
if (_prefix != null)
url = _prefix + url;
if (_contextPath != null)
url = _contextPath + url;
return url;
}
/****
* Multi-Resource Methods
****/
private Resource getResourceForPaths(String resourceId,
List<String> resourcePaths, LocaleProvider localeProvider) {
if (resourceId == null) {
resourceId = getResourceIdForResourcePaths(resourcePaths);
} else {
_resourcePathsById.putIfAbsent(resourceId, resourcePaths);
}
String resourceIdKey = getResourcePathAsKey(resourceId, localeProvider);
Resource resource = _resourceEntriesByResourcePath.get(resourceIdKey);
if (resource == null) {
resource = createResourceForPaths(resourceId, resourcePaths,
localeProvider);
if (resource == null)
return null;
Resource existingResource = _resourceEntriesByResourcePath.putIfAbsent(
resourceIdKey, resource);
if (existingResource != null)
return existingResource;
}
return resource;
}
private Resource createResourceForPaths(String resourceId,
List<String> resourcePaths, LocaleProvider localeProvider) {
List<Resource> resources = new ArrayList<Resource>(resourcePaths.size());
String extension = null;
for (String resourcePath : resourcePaths) {
Resource resource = getResourceForPath(resourcePath, localeProvider, null);
if (resource == null)
return null;
resources.add(resource);
if (extension == null) {
int index = resourcePath.lastIndexOf('.');
if (index != -1)
extension = resourcePath.substring(index + 1);
}
}
if (extension != null)
resourceId += "." + extension;
ResourcesEntry entry = new ResourcesEntry(resourceId, resources);
generateLocalResourcesAndExternalUrl(entry);
return entry;
}
private String getResourceIdForResourcePaths(List<String> resourcePaths) {
StringBuilder resourcePathIds = new StringBuilder();
for (String resourcePath : resourcePaths) {
if (resourcePathIds.length() > 0)
resourcePathIds.append(File.pathSeparator);
resourcePathIds.append(resourcePath);
}
return ResourceSupport.getHash(resourcePathIds.toString());
}
private boolean refreshResourcesEntry(ResourcesEntry resources) {
boolean isRefreshed = false;
synchronized (resources) {
for (Resource resource : resources.getResources()) {
if (refreshResource(resource))
isRefreshed = true;
}
if (isRefreshed)
generateLocalResourcesAndExternalUrl(resources);
}
return isRefreshed;
}
private void generateLocalResourcesAndExternalUrl(ResourcesEntry entry) {
File outputFile = getOutputFile(entry.getResourceId());
try {
BufferedWriter out = new BufferedWriter(new FileWriter(outputFile));
for (Resource resource : entry.getResources()) {
synchronized (resource) {
URL url = resource.getLocalUrl();
BufferedReader reader = new BufferedReader(new InputStreamReader(
url.openStream()));
String line = null;
while ((line = reader.readLine()) != null) {
out.write(line);
out.write('\n');
}
reader.close();
}
}
out.close();
} catch (IOException ex) {
throw new IllegalStateException("error constructing resource", ex);
}
URL localUrl = getFileAsUrl(outputFile);
entry.setLocalUrl(localUrl);
entry.setContentLength(outputFile.length());
String key = getResourceKey(localUrl);
String externalId = constructExternalId(entry.getResourceId(), key);
String externalUrl = constructExternalUrl(externalId);
entry.setExternalId(externalId);
entry.setExternalUrl(externalUrl);
_resourceEntriesByExternalId.put(externalId, entry);
}
private String getLocalUrlAsResourceName(URL url) {
String name = url.toExternalForm();
int index = name.lastIndexOf('!');
if (index != -1)
name = name.substring(index + 1);
return name;
}
private File getOutputFile(String path) {
path.replace(':', File.separatorChar);
File file = new File(_tempDir, path);
File parent = file.getParentFile();
if (!parent.exists())
parent.mkdirs();
return file;
}
private URL getFileAsUrl(File outputFile) {
try {
URI uri = outputFile.toURI();
return uri.toURL();
} catch (MalformedURLException ex) {
throw new IllegalStateException("couldn't make url from file: "
+ outputFile, ex);
}
}
private static class LocaleProviderImpl implements LocaleProvider {
private final Locale _locale;
public LocaleProviderImpl(Locale locale) {
_locale = locale;
}
@Override
public Locale getLocale() {
return _locale;
}
}
}