org.jasig.resourceserver.utils.aggr.ResourcesElementsProviderImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.jasig.resourceserver.utils.aggr.ResourcesElementsProviderImpl.java

Source

package org.jasig.resourceserver.utils.aggr;

/**
 * Licensed to Jasig under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Jasig licenses this file to you 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.
 */

/**
 * 
 */

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.JAXBContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.jasig.resourceserver.aggr.ResourcesDao;
import org.jasig.resourceserver.aggr.ResourcesDaoImpl;
import org.jasig.resourceserver.aggr.om.BasicInclude;
import org.jasig.resourceserver.aggr.om.Css;
import org.jasig.resourceserver.aggr.om.Included;
import org.jasig.resourceserver.aggr.om.Js;
import org.jasig.resourceserver.aggr.om.Parameter;
import org.jasig.resourceserver.aggr.om.Resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ClassRelativeResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.context.support.ServletContextResourceLoader;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.google.common.cache.CacheBuilder;

/**
 * {@link ResourcesDao} implementation that resolves the String argument using
 * the {@link ServletContext}.
 * 
 * @see ServletContextAware
 * @see JAXBContext
 * @author Nicholas Blair, npblair@wisc.edu
 *
 */
public class ResourcesElementsProviderImpl
        implements ResourceLoaderAware, ServletContextAware, InitializingBean, ResourcesElementsProvider {

    private static final String OPEN_COND_COMMENT_PRE = "[";
    private static final String OPEN_COND_COMMENT_POST = "]> ";
    private static final String CLOSE_COND_COMMENT = " <![endif]";
    private static final String OPEN_SCRIPT = "<script type=\"text/javascript\" src=\"";
    private static final String CLOSE_SCRIPT = "\"></script>";
    private static final String OPEN_STYLE = "<link rel=\"stylesheet\" type=\"text/css\" href=\"";
    private static final String CLOSE_STYLE = "\"/>";

    private static final String SCRIPT = "script";
    private static final String LINK = "link";
    private static final String REL = "rel";
    private static final String SRC = "src";
    private static final String HREF = "href";
    private static final String TYPE = "type";
    private static final String MEDIA = "media";

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final DocumentBuilder documentBuilder;
    private final TransformerFactory transformerFactory;
    private ResourceLoader resourceLoader;
    private ServletContext servletContext;
    private ResourcesDao resourcesDao;
    private boolean registerWithServletContext = true;
    private Map<String, String> resolvedResourceCache = CacheBuilder.newBuilder().maximumSize(500)
            .<String, String>build().asMap();
    private Map<String, String> htmlResourcesCache = CacheBuilder.newBuilder().maximumSize(100)
            .<String, String>build().asMap();
    private Map<String, DocumentFragment> xmlResourcesCache = CacheBuilder.newBuilder().maximumSize(100)
            .<String, DocumentFragment>build().asMap();

    public ResourcesElementsProviderImpl() {
        final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        try {
            this.documentBuilder = documentBuilderFactory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new IllegalStateException("Failed to create DocumentBuilder", e);
        }

        this.transformerFactory = TransformerFactory.newInstance();
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    /**
     * The DAO to use for accessing skin resources. If none is specified {@link ResourcesDaoImpl} is used
     */
    public void setResourcesDao(ResourcesDao resourcesDao) {
        this.resourcesDao = resourcesDao;
    }

    /**
     * Sets if this class should register itself in the {@link ServletContext} after initialization. Defaults to true.
     */
    public void setRegisterWithServletContext(boolean registerWithServletContext) {
        this.registerWithServletContext = registerWithServletContext;
    }

    /**
     * Thread-safe Map used to cache resolved resource URLs
     */
    public void setResolvedResourceCache(Map<String, String> resolvedResourceCache) {
        this.resolvedResourceCache = resolvedResourceCache;
    }

    /**
     * Thread safe Map used to cache generated skin resources HTML snippets
     */
    public void setHtmlResourcesCache(Map<String, String> htmlResourcesCache) {
        this.htmlResourcesCache = htmlResourcesCache;
    }

    /**
     * Thread safe Map used to cache generated skin resources XML snippets
     */
    public void setXmlResourcesCache(Map<String, DocumentFragment> xmlResourcesCache) {
        this.xmlResourcesCache = xmlResourcesCache;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (this.registerWithServletContext && this.servletContext != null) {
            this.servletContext.setAttribute(RESOURCES_ELEMENTS_PROVIDER, this);
        }

        if (this.resourcesDao == null) {
            this.resourcesDao = new ResourcesDaoImpl();
        }

        if (this.resourceLoader == null) {
            if (this.servletContext != null) {
                this.resourceLoader = new ServletContextResourceLoader(this.servletContext);
            } else {
                this.resourceLoader = new ClassRelativeResourceLoader(getClass());
            }
        }
    }

    @Override
    public String resolveResourceUrl(HttpServletRequest request, String resource) {
        final Included includedType = this.getIncludedType(request);

        String resourceUrl;
        // If aggregation is enabled look in the cache to see if the resourceContext has already been determined
        if (Included.AGGREGATED == includedType) {
            resourceUrl = this.resolvedResourceCache.get(resource);
            if (resourceUrl != null) {
                return resourceUrl;
            }
        }

        final String resourceContextPath = this.resolveResourceContextPath(request, resource);

        //build the URL
        resourceUrl = resourceContextPath.concat(resource);

        this.logger.debug("Resoved {} to {}", resource, resourceUrl);

        // If aggregation is enabled cache the resolved resource
        if (Included.AGGREGATED == includedType) {
            this.resolvedResourceCache.put(resource, resourceUrl);
        }

        return resourceUrl;
    }

    @Override
    public void setDefaultIncludedType(Included included) {
        switch (included) {
        case AGGREGATED: {
            System.setProperty(AGGREGATED_THEME_PARAMETER, Boolean.TRUE.toString());
            break;
        }
        case PLAIN: {
            System.setProperty(AGGREGATED_THEME_PARAMETER, Boolean.FALSE.toString());
            this.resolvedResourceCache.clear();
            this.htmlResourcesCache.clear();
            this.xmlResourcesCache.clear();
            break;
        }
        case BOTH:
        default: {
            throw new UnsupportedOperationException("Unsupported Included type: " + included);
        }
        }
    }

    @Override
    public Included getDefaultIncludedType() {
        String aggregationParameter = System.getProperty(AGGREGATED_THEME_PARAMETER);
        if (aggregationParameter == null) {
            aggregationParameter = System.getProperty(LEGACY_AGGREGATED_THEME_PARAMETER_1);
        }
        if (aggregationParameter == null) {
            aggregationParameter = System.getProperty(LEGACY_AGGREGATED_THEME_PARAMETER_2);
        }
        if (aggregationParameter == null) {
            aggregationParameter = DEFAULT_AGGREGATION_ENABLED;
        }
        if (Boolean.parseBoolean(aggregationParameter)) {
            return Included.AGGREGATED;
        }

        return Included.PLAIN;
    }

    /* (non-Javadoc)
     * @see org.jasig.resource.aggr.util.ResourcesElementsProvider#getIncludedType(javax.servlet.http.HttpServletRequest, java.lang.String)
     */
    @Override
    public Included getIncludedType(HttpServletRequest request) {
        return this.getDefaultIncludedType();
    }

    /* (non-Javadoc)
     * @see org.jasig.resource.aggr.util.ResourcesElementsProvider#getResourcesParameter(javax.servlet.http.HttpServletRequest, java.lang.String, java.lang.String)
     */
    @Override
    public String getResourcesParameter(HttpServletRequest request, String skinXml, String name) {
        final Resources skinResources = this.getResources(request, skinXml);
        if (skinResources == null) {
            logger.warn("Could not find skin file " + skinXml);
            return null;
        }

        for (final Parameter parameter : skinResources.getParameter()) {
            if (parameter.getName().equals(name)) {
                return parameter.getValue();
            }
        }

        return null;
    }

    @Override
    public NodeList getResourcesXmlFragment(HttpServletRequest request, String skinXml) {
        final DocumentFragment headFragment = getResourcesXml(request, skinXml);

        return headFragment.getChildNodes();
    }

    @Override
    public String getResourcesHtmlFragment(HttpServletRequest request, String skinXml) {
        final Included includedType = this.getIncludedType(request);

        String htmlFragment;
        if (Included.AGGREGATED == includedType) {
            htmlFragment = this.htmlResourcesCache.get(skinXml);
            if (htmlFragment != null) {
                return htmlFragment;
            }
        }

        final DocumentFragment resourcesXml = this.getResourcesXml(request, skinXml);

        final StringWriter stringWriter = new StringWriter();
        try {
            final Transformer transformer = transformerFactory.newTransformer();
            if (Included.PLAIN == this.getIncludedType(request)) {
                transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            }
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            transformer.transform(new DOMSource(resourcesXml), new StreamResult(stringWriter));
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize XML fragment to HTML fragment", e);
        }

        htmlFragment = stringWriter.toString();
        if (Included.AGGREGATED == includedType) {
            this.htmlResourcesCache.put(skinXml, htmlFragment);
        }
        return htmlFragment;
    }

    @Override
    public Resources getResources(HttpServletRequest request, String skinXml) {
        final Included includedType = this.getIncludedType(request);

        final Resource skinResource = getResource(skinXml);
        final File skinFile;
        try {
            skinFile = skinResource.getFile();
        } catch (IOException e) {
            throw new IllegalArgumentException("Failed to get File for skin XML path: " + skinXml, e);
        }

        switch (includedType) {
        case AGGREGATED: {
            final String aggregatedSkinXml = resourcesDao.getAggregatedSkinName(skinFile.getName());
            final File aggregatedSkinFile = new File(skinFile.getParentFile(), aggregatedSkinXml);
            if (aggregatedSkinFile.exists()) {
                return resourcesDao.readResources(aggregatedSkinFile, includedType);
            }

            this.logger.warn("Could not find aggregated skin XML '" + aggregatedSkinFile + "' for '" + skinFile
                    + "', falling back on unaggregated version.");
        }
        case PLAIN: {
            return resourcesDao.readResources(skinFile, includedType);
        }
        default: {
            throw new UnsupportedOperationException("Unkown Included type: " + includedType);
        }
        }
    }

    /**
     * If the resource serving servlet context is available and the resource
     * is available in the context, create a URL to the resource in that context.
     * If not, create a local URL for the requested resource.
     */
    protected String resolveResourceContextPath(HttpServletRequest request, String resource) {
        final String resourceContextPath = this.getResourceServerContextPath();

        this.logger.debug("Attempting to locate resource serving webapp with context path: {}",
                resourceContextPath);

        //Try to resolve the 
        final ServletContext resourceContext = this.servletContext.getContext(resourceContextPath);
        if (resourceContext == null || !resourceContextPath.equals(resourceContext.getContextPath())) {
            this.logger.warn(
                    "Could not find resource serving webapp under context path {} ensure the resource server is deployed and cross context dispatching is enable for this web application",
                    resourceContextPath);
            return request.getContextPath();
        }

        this.logger.debug("Found resource serving webapp at: {}", resourceContextPath);

        URL url = null;
        try {
            url = resourceContext.getResource(resource);
        } catch (MalformedURLException e) {
            //Ignore
        }

        if (url == null) {
            this.logger.debug(
                    "Resource serving webapp {} doesn't contain resource {} Falling back to the local resource.",
                    resourceContextPath, resource);
            return request.getContextPath();
        }

        this.logger.debug("Resource serving webapp {} contains resource {} Using resource server.",
                resourceContextPath, resource);
        return resourceContextPath;
    }

    /**
     * Determine the context name of the resource serving webapp 
     */
    protected String getResourceServerContextPath() {
        final String resourceContextPath = this.servletContext.getInitParameter(RESOURCE_CONTEXT_INIT_PARAM);
        if (resourceContextPath == null) {
            // if no resource context path was defined in the web.xml, use the
            // default
            return DEFAULT_RESOURCE_CONTEXT;
        }

        if (!resourceContextPath.startsWith("/")) {
            // ensure that our context starts with a slash
            return "/".concat(resourceContextPath);
        }

        return resourceContextPath;
    }

    /**
     * Build XML {@link DocumentFragment} of link and script tags for the specified skin file 
     */
    protected DocumentFragment getResourcesXml(HttpServletRequest request, String skinXml) {
        final Included includedType = this.getIncludedType(request);

        DocumentFragment headFragment;
        if (Included.AGGREGATED == includedType) {
            headFragment = this.xmlResourcesCache.get(skinXml);
            if (headFragment != null) {
                return headFragment;
            }
        }

        final Resources skinResources = this.getResources(request, skinXml);
        if (skinResources == null) {
            logger.warn("Could not find skin file " + skinXml);
            return null;
        }

        final Document doc = this.documentBuilder.newDocument();
        headFragment = doc.createDocumentFragment();

        final String relativeRoot = request.getContextPath() + "/" + FilenameUtils.getPath(skinXml);
        for (final Css css : skinResources.getCss()) {
            appendCssNode(request, doc, headFragment, css, relativeRoot);
        }
        for (final Js js : skinResources.getJs()) {
            appendJsNode(request, doc, headFragment, js, relativeRoot);
        }

        if (Included.AGGREGATED == includedType) {
            this.xmlResourcesCache.put(skinXml, headFragment);
        }
        return headFragment;
    }

    protected Resource getResource(String skinXml) {
        if (!skinXml.startsWith("/")) {
            skinXml = "/" + skinXml;
        }

        return this.resourceLoader.getResource(skinXml);
    }

    /**
     * Convert the {@link Js} argument to an HTML script tag and append it
     * to the {@link DocumentFragment}.
     */
    protected void appendJsNode(HttpServletRequest request, Document document, DocumentFragment head, Js js,
            String relativeRoot) {
        final String scriptPath = getElementPath(request, js, relativeRoot);

        if (resourcesDao.isConditional(js)) {
            Comment c = document.createComment("");
            c.appendData(OPEN_COND_COMMENT_PRE);
            c.appendData(js.getConditional());
            c.appendData(OPEN_COND_COMMENT_POST);
            c.appendData(OPEN_SCRIPT);
            c.appendData(scriptPath);
            c.appendData(CLOSE_SCRIPT);
            c.appendData(CLOSE_COND_COMMENT);
            head.appendChild(c);
        } else {
            Element element = document.createElement(SCRIPT);
            element.setAttribute(TYPE, "text/javascript");
            element.setAttribute(SRC, scriptPath);
            element.appendChild(document.createTextNode(" "));

            head.appendChild(element);
        }
    }

    /**
     * Convert the {@link Css} argument to an HTML link tag and append it
     * to the {@link DocumentFragment}.
     */
    protected void appendCssNode(HttpServletRequest request, Document document, DocumentFragment head, Css css,
            String relativeRoot) {
        final String stylePath = getElementPath(request, css, relativeRoot);

        if (resourcesDao.isConditional(css)) {
            Comment c = document.createComment("");
            c.appendData(OPEN_COND_COMMENT_PRE);
            c.appendData(css.getConditional());
            c.appendData(OPEN_COND_COMMENT_POST);
            c.appendData(OPEN_STYLE);
            c.appendData(stylePath);
            if (StringUtils.isNotBlank(css.getMedia())) {
                c.appendData("\" media=\"");
                c.appendData(css.getMedia());
            }
            c.appendData(CLOSE_STYLE);
            c.appendData(CLOSE_COND_COMMENT);
            head.appendChild(c);
        } else {
            Element element = document.createElement(LINK);
            element.setAttribute(REL, "stylesheet");
            element.setAttribute(TYPE, "text/css");
            element.setAttribute(HREF, stylePath);
            if (StringUtils.isNotBlank(css.getMedia())) {
                element.setAttribute(MEDIA, css.getMedia());
            }
            head.appendChild(element);
        }
    }

    protected <T extends BasicInclude> String getElementPath(HttpServletRequest request, T basicInclude,
            String relativeRoot) {
        String path = basicInclude.getValue();

        if (!resourcesDao.isAbsolute(basicInclude)) {
            path = FilenameUtils.normalize(relativeRoot + path);
            path = FilenameUtils.separatorsToUnix(path);
            if (logger.isDebugEnabled()) {
                logger.debug("translated relative path {} to {}", basicInclude.getValue(), path);
            }
        } else if (basicInclude.isResource()) {
            path = this.resolveResourceUrl(request, path);
        }

        return path;
    }
}