/**
* 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 com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ResourcePrototype;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import org.onebusaway.presentation.services.resources.WebappSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ClientBundleFactory {
private static final String PREFIX_CLASSPATH = "classpath:";
private static Logger _log = LoggerFactory.getLogger(ClientBundleFactory.class);
private Map<String, Object> _bundlesByNamePath = new HashMap<String, Object>();
private Map<Class<?>, ClientBundle> _bundlesByType = new HashMap<Class<?>, ClientBundle>();
private Map<LocalResource, String> _resourceToUrl = new HashMap<LocalResource, String>();
private Map<String, LocalResource> _urlToResource = new HashMap<String, LocalResource>();
private String _prefix;
private String _pattern;
private File _tempDir;
private ContextImpl _context = new ContextImpl();
private ServletContext _servletContext;
private String _contextPath;
public void setPrefix(String prefix) {
_prefix = prefix;
}
public void setPattern(String pattern) {
_pattern = pattern;
}
public void setServletContext(ServletContext servletContext) {
_servletContext = servletContext;
_contextPath = 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, "ClientBundles");
if (!_tempDir.exists())
_tempDir.mkdirs();
_servletContext.setAttribute("resources", _bundlesByNamePath);
}
public void setResources(List<Class<?>> resources) throws IOException {
for (Class<?> bundleType : resources) {
try {
addResource(bundleType);
} catch (Exception ex) {
_log.error("error adding resource of type: " + bundleType.getName(), ex);
throw new IllegalStateException("error adding resource of type: "
+ bundleType.getName(), ex);
}
}
}
public void addResource(Class<?> bundleType) throws IOException {
if (!ClientBundle.class.isAssignableFrom(bundleType))
throw new IllegalArgumentException("class is not assignable to "
+ ClientBundle.class + ": " + bundleType);
// Have we already seen this bundle?
if (_bundlesByType.containsKey(bundleType))
return;
ClientBundleImpl bundle = new ClientBundleImpl(bundleType);
List<ResourcePrototypeImpl> resources = processBundleMethods(bundleType,
bundle);
for (ResourcePrototypeImpl resource : resources)
resource.refresh();
ClientBundle bundleProxy = (ClientBundle) Proxy.newProxyInstance(
bundleType.getClassLoader(), new Class[] {bundleType}, bundle);
_bundlesByType.put(bundleType, bundleProxy);
addBundleToResourcePath(bundleType, bundleProxy);
}
public Map<String, Object> getBundles() {
return _bundlesByNamePath;
}
public LocalResource getResourceForExternalUrl(String externalUrl) {
return _urlToResource.get(externalUrl);
}
@SuppressWarnings("unchecked")
public <T extends ClientBundle> T getBundleForType(Class<T> bundleType) {
ClientBundle bundle = _bundlesByType.get(bundleType);
return (T) bundle;
}
/****
* Private Methods
*
* @return
****/
private List<ResourcePrototypeImpl> processBundleMethods(Class<?> bundleType,
ClientBundleImpl bundle) throws MalformedURLException {
List<ResourcePrototypeImpl> resources = new ArrayList<ResourcePrototypeImpl>();
for (Method method : bundleType.getMethods()) {
String methodName = method.getName();
WebappSource source = method.getAnnotation(WebappSource.class);
if (source == null) {
_log.warn("no Resource annotation found: " + methodName);
continue;
}
String[] values = source.value();
if (values == null || values.length != 1)
throw new IllegalStateException("@WebappSource has no value: "
+ bundleType.getName() + "#" + methodName);
String resourceName = values[0];
Class<?> returnType = method.getReturnType();
URL localURL = getBundleResourceAsLocalUrl(bundleType, resourceName);
if (localURL == null) {
_log.warn("could not find ClientBundle resource for "
+ bundleType.getName() + "#" + methodName + " source="
+ resourceName);
continue;
}
File localFile = getBundleResourceAsLocalFile(resourceName, localURL);
String name = getMethodNameAsPropertyName(methodName);
if (name == null) {
_log.warn("invalid resource name: " + bundleType.getName() + "#"
+ methodName);
continue;
}
if (CssResource.class.isAssignableFrom(returnType)) {
CssResourceImpl resourceImpl = new CssResourceImpl(_context, bundle,
methodName, localURL);
if (localFile != null)
resourceImpl.setLocalFile(localFile);
resources.add(resourceImpl);
ResourcePrototype resourceProxy = (ResourcePrototype) Proxy.newProxyInstance(
bundleType.getClassLoader(), new Class[] {
returnType, ResourceWithUrl.class}, resourceImpl);
bundle.addResource(resourceProxy);
} else if (ImageResource.class.isAssignableFrom(returnType)) {
ImageResourceImpl resourceImpl = new ImageResourceImpl(_context,
bundle, methodName, localURL);
bundle.addResource(resourceImpl);
resources.add(resourceImpl);
}
}
return resources;
}
private URL getBundleResourceAsLocalUrl(Class<?> bundleType,
String resourceName) throws MalformedURLException {
if (resourceName.startsWith(PREFIX_CLASSPATH)) {
resourceName = resourceName.substring(PREFIX_CLASSPATH.length());
URL resource = bundleType.getResource(resourceName);
if( resource == null)
_log.warn("unknown classpath resource: name=" + resourceName + " bundleType=" + bundleType.getName());
return resource;
}
return _servletContext.getResource(resourceName);
}
private File getBundleResourceAsLocalFile(String resourceName, URL resourceUrl) {
String protocol = resourceUrl.getProtocol();
if ("file".equals(protocol))
return new File(resourceUrl.getPath());
File path = new File(_servletContext.getRealPath(resourceName));
if (path.exists())
return path;
return null;
}
private String construct(String bundleName, String resourceName,
String resourceKey, String resourceExtension) {
StringBuilder b = new StringBuilder();
b.append(bundleName);
b.append('-');
if (resourceName.startsWith("get"))
resourceName = resourceName.substring(3);
b.append(resourceName);
b.append('-');
b.append(resourceKey);
b.append(".cache");
if (resourceExtension != null && resourceExtension.length() > 0)
b.append('.').append(resourceExtension);
return b.toString();
}
private String addContext(String url) {
if (_contextPath != null && _contextPath.length() > 0)
url = _contextPath + url;
return url;
}
private String getContextPath(ServletContext context) {
// Get the context path without the request.
String contextPath = "";
try {
String path = context.getResource("/").getPath();
contextPath = path.substring(0, path.lastIndexOf("/"));
contextPath = contextPath.substring(contextPath.lastIndexOf("/"));
if (contextPath.equals("/localhost"))
contextPath = "";
} catch (Exception e) {
e.printStackTrace();
}
return contextPath;
}
private String getMethodNameAsPropertyName(String methodName) {
if (!methodName.startsWith("get"))
return methodName;
String name = methodName.substring(3);
if (name.length() == 0)
return null;
return name.substring(0, 1).toLowerCase() + name.substring(1);
}
@SuppressWarnings("unchecked")
private void addBundleToResourcePath(Class<?> bundleType,
ClientBundle bundleProxy) {
List<String> keys = getKeysForBundleType(bundleType);
for (String key : keys) {
Map<String, Object> current = _bundlesByNamePath;
String[] tokens = key.split("\\.");
for (int i = 0; i < tokens.length - 1; i++) {
Object obj = current.get(tokens[i]);
if (obj == null)
obj = new HashMap<String, Object>();
if (!(obj instanceof Map<?, ?>))
throw new IllegalStateException("name collision: " + key
+ " bundleType=" + bundleType.getName());
current = (Map<String, Object>) obj;
}
current.put(tokens[tokens.length - 1], bundleProxy);
}
}
private List<String> getKeysForBundleType(Class<?> bundleType) {
List<String> names = new ArrayList<String>();
String name = bundleType.getName();
names.add(name);
int index = name.lastIndexOf(".");
if (index != -1) {
name = name.substring(index + 1);
if (name.length() > 0)
names.add(name);
}
return names;
}
/****
*
****/
private class ContextImpl implements ClientBundleContext {
public String addContext(String url) {
return ClientBundleFactory.this.addContext(url);
}
public String handleResource(String bundleName, String resourceName,
String resourceKey, String resourceExtension, LocalResource resource) {
String url = construct(bundleName, resourceName, resourceKey,
resourceExtension);
String existingUrl = _resourceToUrl.get(resource);
if (existingUrl != null)
_urlToResource.remove(existingUrl);
_resourceToUrl.put(resource, url);
_urlToResource.put(url, resource);
if( _pattern != null)
url = _pattern.replaceAll("\\{\\}", url);
if (_prefix != null)
url = _prefix + url;
if (_contextPath != null)
url = _contextPath + url;
return url;
}
public File getTempDir() {
return _tempDir;
}
}
}