/* * JBoss, Home of Professional Open Source * Copyright 2013, Red Hat, Inc. and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.richfaces.resource; import java.net.URL; import java.text.MessageFormat; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.LinkedList; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.faces.application.ProjectStage; import javax.faces.application.Resource; import javax.faces.application.ResourceHandler; import javax.faces.context.FacesContext; import org.richfaces.application.DependencyInjector; import org.richfaces.log.Logger; import org.richfaces.log.RichfacesLogger; import org.richfaces.resource.external.MappedResourceFactory; import org.richfaces.resource.external.ResourceTracker; import org.richfaces.resource.mapping.ResourcePath; import org.richfaces.webapp.ResourceServlet; import org.richfaces.application.ServiceTracker; import com.google.common.base.Function; import com.google.common.base.Strings; /** * @author Nick Belaevski * */ public class ResourceFactoryImpl implements ResourceFactory { private static final Logger LOGGER = RichfacesLogger.RESOURCE.getLogger(); private static final String MAPPED_RESOURCES_RESOLUTION_STACK = MappedResourceFactory.class.getName() + ".MAPPED_RESOURCES_RESOLUTION_STACK"; private static final Function<Entry<String, String>, MappedResourceData> DYNAMIC_MAPPINGS_DATA_PRODUCER = new Function<Entry<String, String>, MappedResourceData>() { public MappedResourceData apply(Entry<String, String> from) { String resourceLocation = from.getValue(); Map<String, String> params = ResourceUtils.parseResourceParameters(resourceLocation); String resourceQualifier = extractParametersFromResourceName(resourceLocation); return new MappedResourceData(ResourceKey.create(resourceQualifier), params); } }; private ResourceHandler defaultHandler; // private Map<ResourceKey, ExternalStaticResourceFactory> externalStaticResourceFactories; private Map<ResourceKey, MappedResourceData> mappedResourceDataMap; private MappedResourceFactory mappedResourceFactory; private ResourceTracker resourceTracker; public ResourceFactoryImpl(ResourceHandler defaultHandler) { super(); this.defaultHandler = defaultHandler; this.mappedResourceDataMap = ResourceUtils.readMappings(DYNAMIC_MAPPINGS_DATA_PRODUCER, ResourceFactory.DYNAMIC_RESOURCE_MAPPINGS); this.mappedResourceFactory = ServiceTracker.getProxy(MappedResourceFactory.class); this.resourceTracker = ServiceTracker.getProxy(ResourceTracker.class); } private static String extractParametersFromResourceName(String resourceName) { if (!(resourceName.lastIndexOf("{") != -1)) { return resourceName; } return resourceName.substring(0, resourceName.lastIndexOf("{")); } private void logResourceProblem(FacesContext context, Throwable throwable, String messagePattern, Object... arguments) { boolean isProductionStage = context.isProjectStage(ProjectStage.Production); if (LOGGER.isWarnEnabled() || (!isProductionStage && LOGGER.isInfoEnabled())) { String formattedMessage = MessageFormat.format(messagePattern, arguments); if (throwable != null) { LOGGER.warn(formattedMessage, throwable); } else { if (isProductionStage) { LOGGER.info(formattedMessage); } else { LOGGER.warn(formattedMessage); } } } } private void logMissingResource(FacesContext context, String resourceData) { logResourceProblem(context, null, "Resource {0} was not found", resourceData); } private Resource createCompiledCSSResource(ResourceKey resourceKey) { Resource sourceResource = defaultHandler.createResource(resourceKey.getResourceName(), resourceKey.getLibraryName(), "text/plain"); if (sourceResource != null) { return new CompiledCSSResource(sourceResource); } return null; } protected void injectProperties(Object resource, Map<String, String> parameters) { FacesContext facesContext = FacesContext.getCurrentInstance(); Map<Object, Object> attributes = facesContext.getAttributes(); try { attributes.put(ResourceParameterELResolver.CONTEXT_ATTRIBUTE_NAME, parameters); ServiceTracker.getService(DependencyInjector.class).inject(facesContext, resource); } finally { attributes.remove(ResourceParameterELResolver.CONTEXT_ATTRIBUTE_NAME); } } private boolean isCacheableSet(Class<?> c) { DynamicUserResource annotation = c.getAnnotation(DynamicUserResource.class); return annotation != null && annotation.cacheable(); } private boolean isVersionedSet(Class<?> c) { DynamicUserResource annotation = c.getAnnotation(DynamicUserResource.class); return annotation != null && annotation.versioned(); } private Resource createDynamicUserResourceInstance(Class<?> loadedClass) throws Exception, LinkageError { String resourceName = loadedClass.getName(); boolean checkResult = false; DynamicUserResource dynamicUserResource = loadedClass.getAnnotation(DynamicUserResource.class); if (dynamicUserResource != null) { checkResult = true; LOGGER.debug(MessageFormat.format("Dynamic resource annotation is present on resource class {0}", resourceName)); } if (!checkResult) { DynamicResource dynamicResource = loadedClass.getAnnotation(DynamicResource.class); if (dynamicResource != null) { LOGGER.debug(MessageFormat.format("Dynamic resource annotation is present on resource class {0}", resourceName)); checkResult = true; } } if (!checkResult) { LOGGER.debug(MessageFormat.format("Dynamic resource annotation is not present on resource class {0}", resourceName)); checkResult = checkResourceMarker(resourceName); } if (!checkResult) { return null; } Resource result = null; if (Java2DUserResource.class.isAssignableFrom(loadedClass)) { Java2DUserResource java2DUserResource = (Java2DUserResource) loadedClass.newInstance(); result = createResource(java2DUserResource); } else if (UserResource.class.isAssignableFrom(loadedClass)) { UserResource userResource = (UserResource) loadedClass.newInstance(); result = createResource(userResource); } return result; } private Resource createDynamicResourceInstance(Class<?> loadedClass) throws Exception, LinkageError { String resourceName = loadedClass.getName(); boolean checkResult = false; DynamicResource annotation = loadedClass.getAnnotation(DynamicResource.class); if (annotation != null) { LOGGER.debug(MessageFormat.format("Dynamic resource annotation is present on resource class {0}", resourceName)); checkResult = true; } else { LOGGER.debug(MessageFormat.format("Dynamic resource annotation is not present on resource class {0}", resourceName)); } if (!checkResult) { checkResult = checkResourceMarker(resourceName); } if (!checkResult) { return null; } Class<? extends Resource> resourceClass = loadedClass.asSubclass(Resource.class); Resource result = (Resource) resourceClass.newInstance(); return result; } /** * Should be called only if {@link #isResourceExists(String)} returns <code>true</code> * * @param resourceKey * @param parameters * @return */ protected Resource createHandlerDependentResource(ResourceKey resourceKey, Map<String, String> parameters) { if (!Strings.isNullOrEmpty(resourceKey.getLibraryName())) { return null; } String resourceName = resourceKey.getResourceName(); Resource resource = null; ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if (contextClassLoader != null) { try { Class<?> loadedClass = Class.forName(resourceName, false, contextClassLoader); resource = createDynamicUserResourceInstance(loadedClass); if (resource == null) { resource = createDynamicResourceInstance(loadedClass); } if (resource != null) { resource.setResourceName(resourceName); if (parameters != null) { if (resource instanceof BaseResourceWrapper<?>) { BaseResourceWrapper<?> baseResourceWrapper = (BaseResourceWrapper<?>) resource; injectProperties(baseResourceWrapper.getWrapped(), parameters); } else { injectProperties(resource, parameters); } } if (LOGGER.isDebugEnabled()) { LOGGER.debug(MessageFormat.format("Successfully created instance of {0} resource", resourceName)); } } } catch (ClassNotFoundException e) { // do nothing } catch (Exception e) { logResourceProblem(FacesContext.getCurrentInstance(), e, "Error creating resource {0}", resourceName); } catch (LinkageError e) { logResourceProblem(FacesContext.getCurrentInstance(), e, "Error creating resource {0}", resourceName); } } return resource; } private boolean checkResourceMarker(String resourceName) { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); URL resourceMarkerUrl = contextClassLoader.getResource("META-INF/" + resourceName + ".resource.properties"); boolean result = (resourceMarkerUrl != null); if (LOGGER.isDebugEnabled()) { if (result) { LOGGER.debug(MessageFormat.format("Marker file for {0} resource found in classpath", resourceName)); } else { LOGGER.debug(MessageFormat.format("Marker file for {0} resource does not exist", resourceName)); } } return result; } public Resource createResource(FacesContext context, ResourceRequestData resourceData) { String resourceName = resourceData.getResourceName(); if ((resourceName == null) || (resourceName.length() == 0)) { return null; } String libraryName = resourceData.getLibraryName(); Resource resource = createDynamicResource(new ResourceKey(resourceName, libraryName), false); if (resource == null) { logMissingResource(context, resourceData.getResourceKey()); return null; } if (resource instanceof VersionedResource) { VersionedResource versionedResource = (VersionedResource) resource; String existingVersion = versionedResource.getVersion(); String requestedVersion = resourceData.getVersion(); if (LOGGER.isDebugEnabled()) { LOGGER.debug(MessageFormat.format("Client requested {0} version of resource, server has {1} version", String.valueOf(requestedVersion), String.valueOf(existingVersion))); } if ((existingVersion != null) && (requestedVersion != null) && !existingVersion.equals(requestedVersion)) { logResourceProblem(context, null, "Resource {0} of version {1} was not found", resourceName, requestedVersion); return null; } } Object decodedData = resourceData.getData(); if (LOGGER.isDebugEnabled()) { if (decodedData != null) { LOGGER.debug("Resource state data succesfully decoded"); } else { LOGGER.debug("Resource state data decoded as null"); } } ResourceUtils.restoreResourceState(context, resource, decodedData); return resource; } public Resource createResource(String resourceName, String libraryName, String contentType) { ResourceKey resourceKey = new ResourceKey(resourceName, libraryName); Resource resource = createMappedResource(resourceKey); if (resource != null) { return resource; } else { return createDynamicResource(resourceKey, true); } } private Resource createMappedResource(ResourceKey resourceKey) { final FacesContext context = FacesContext.getCurrentInstance(); // do not map resources for ResourceServlet requests (they should be already mapped) if (context.getExternalContext().getRequestMap().get(ResourceServlet.RESOURCE_SERVLET_REQUEST_FLAG) == Boolean.TRUE) { return null; } Resource mappedResource = resolveMappedResource(context, resourceKey); if (mappedResource == null) { return null; } resourceTracker.markResourceRendered(context, resourceKey); ResourcePath path = new ResourcePath(mappedResource.getRequestPath()); Set<ResourceKey> aggregatedResources = mappedResourceFactory.getAggregatedResources(path); for (ResourceKey key : aggregatedResources) { resourceTracker.markResourceRendered(context, key); } if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("Resource '%s' is redirected to following resource path: %s", resourceKey, path)); if (aggregatedResources.size() >= 1) { LOGGER.debug(String.format("Following resources are marked as rendered: %s", resourceKey, aggregatedResources)); } } return mappedResource; } private Resource resolveMappedResource(FacesContext context, ResourceKey resourceKey) { // check whether we are not resolving given key (prevents infinite loop) Deque<ResourceKey> mappedResourcesResolutionStack = getMappedResourcesResolutionStack(context); if (mappedResourcesResolutionStack.contains(resourceKey)) { return null; } mappedResourcesResolutionStack.push(resourceKey); try { return mappedResourceFactory.createResource(context, resourceKey); } finally { mappedResourcesResolutionStack.pop(); } } private Deque<ResourceKey> getMappedResourcesResolutionStack(FacesContext context) { LinkedList<ResourceKey> list = (LinkedList<ResourceKey>) context.getAttributes().get(MAPPED_RESOURCES_RESOLUTION_STACK); if (list == null) { list = new LinkedList<ResourceKey>(); context.getAttributes().put(MAPPED_RESOURCES_RESOLUTION_STACK, list); } return list; } protected Resource createDynamicResource(ResourceKey resourceKey, boolean useDependencyInjection) { Resource result = null; Map<String, String> params = null; MappedResourceData mappedResourceData = mappedResourceDataMap.get(resourceKey); ResourceKey actualKey; if (mappedResourceData != null) { actualKey = mappedResourceData.getResourceKey(); if (useDependencyInjection) { params = mappedResourceData.getParams(); } } else { actualKey = resourceKey; if (useDependencyInjection) { params = Collections.<String, String>emptyMap(); } } if (Strings.isNullOrEmpty(resourceKey.getResourceName())) { return null; } if (actualKey.getResourceName().endsWith(".ecss")) { // TODO nick - params? result = createCompiledCSSResource(actualKey); } else { result = createHandlerDependentResource(actualKey, params); } if (result != null) { result.setLibraryName(resourceKey.getLibraryName()); result.setResourceName(resourceKey.getResourceName()); } else if (mappedResourceData != null) { result = defaultHandler.createResource(actualKey.getResourceName(), actualKey.getLibraryName()); } return result; } public Collection<ResourceKey> getMappedDynamicResourceKeys() { return Collections.unmodifiableSet(mappedResourceDataMap.keySet()); } protected Resource createResource(Java2DUserResource resource) { boolean cacheable = isCacheableSet(resource.getClass()); boolean versioned = isVersionedSet(resource.getClass()); if (resource instanceof Java2DAnimatedUserResource) { Java2DAnimatedUserResource java2DAnimatedUserResource = (Java2DAnimatedUserResource) resource; return new Java2DAnimatedUserResourceWrapperImpl(java2DAnimatedUserResource, cacheable, versioned); } else { return new Java2DUserResourceWrapperImpl(resource, cacheable, versioned); } } protected Resource createResource(UserResource resource) { boolean cacheable = isCacheableSet(resource.getClass()); boolean versioned = isVersionedSet(resource.getClass()); return new UserResourceWrapperImpl(resource, cacheable, versioned); } private static class MappedResourceData { private ResourceKey resourceKey; private Map<String, String> params; public MappedResourceData(ResourceKey resourceKey, Map<String, String> params) { this.resourceKey = resourceKey; this.params = params; } public ResourceKey getResourceKey() { return resourceKey; } public Map<String, String> getParams() { return params; } } }