org.apereo.portal.portlets.dynamicskin.FileSystemDynamicSkinService.java Source code

Java tutorial

Introduction

Here is the source code for org.apereo.portal.portlets.dynamicskin.FileSystemDynamicSkinService.java

Source

/**
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 the following location:
 *
 *   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.apereo.portal.portlets.dynamicskin;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.portlet.PortletContext;
import javax.portlet.PortletPreferences;
import javax.portlet.PortletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;
import org.apache.commons.io.IOUtils;
import org.lesscss.LessCompiler;
import org.lesscss.LessException;
import org.lesscss.LessSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * File system based implementation of services for the Skin Manager.
 *
 * @since 4.1.0
 * @author James Wennmacher, jwennmacher@unicon.net
 */
@Service
public class FileSystemDynamicSkinService implements DynamicSkinService {

    private static final String DYNASKIN_DEFAULT_ROOT_FOLDER = "/media/skins/respondr";
    private static final String DYNASKIN_TEMPLATE_INCLUDE_FILE = "{0}/{1}.less";
    private static final String DYNASKIN_INCLUDE_FILE = "{0}/configuredSkin-{1}.less";
    private static final String LESS_CSS_JAVASCRIPT_URL = "/media/skins/common/javascript/less/less-1.6.2.js";

    private final Logger log = LoggerFactory.getLogger(getClass());
    private String rootFolder = DYNASKIN_DEFAULT_ROOT_FOLDER;
    private MessageFormat skinTemplateIncludeFile = new MessageFormat(DYNASKIN_TEMPLATE_INCLUDE_FILE);
    private MessageFormat skinIncludeFile = new MessageFormat(DYNASKIN_INCLUDE_FILE);
    private String lessCssJavascriptUrlPath = LESS_CSS_JAVASCRIPT_URL;

    /**
     *  Set of skinFilePath values for skin files that currently exists on file
     *  system.  Thread-safe for concurrent reads and inserts.
     */
    private Set<String> compiledCssFilepaths = new CopyOnWriteArraySet<String>();

    @Autowired
    @Qualifier(value = "org.apereo.portal.skinManager.failureCache")
    private Cache cssSkinFailureCache;

    public void setLessCssJavascriptUrlPath(String lessCssJavascriptUrlPath) {
        this.lessCssJavascriptUrlPath = lessCssJavascriptUrlPath;
    }

    public void setRootFolder(String rootFolder) {
        this.rootFolder = rootFolder;
    }

    public void setSkinTemplateIncludeFile(String skinTemplateIncludeFile) {
        this.skinTemplateIncludeFile = new MessageFormat(skinTemplateIncludeFile);
    }

    public void setSkinIncludeFile(String skinIncludeFile) {
        this.skinIncludeFile = new MessageFormat(skinIncludeFile);
    }

    /**
     * Return true if the filePathname already exists on the file system.  Check memory first in a concurrent manner
     * to allow multiple threads to check simultaneously.
     *
     * @param filePathname Fully-qualified file path name of the .css file
     * @return True if file exists on the file system.
     */
    @Override
    public boolean skinFileExists(String filePathname) {
        // Check the existing filepaths map first since it is faster than accessing the file system.
        if (compiledCssFilepaths.contains(filePathname)) {
            return true;
        }
        // If file already exists on file system, add it to the existing filepaths map in a thread-safe manner.
        // This situation would occur when the portal servers are restarted and the in-memory map is empty.
        boolean exists = new File(filePathname).exists();
        if (exists) {
            compiledCssFilepaths.add(filePathname);
        }
        return exists;
    }

    @Override
    public void generateSkinCssFile(PortletRequest request, String filePathname, String skinToken,
            String lessfileBaseName) {

        synchronized (filePathname) {
            if (compiledCssFilepaths.contains(filePathname)) {
                /*
                 * Two or more threads needing the same CSS file managed to invoke
                 * this method.  An earlier thread has already generated the file
                 * we need.  Concurrency features of the CopyOnWriteArraySet
                 * (compiledCssFilepaths) guarantee that we will enter this if {}
                 * block (and exit) for a filePathname that's been successfully
                 * generated by another thread.
                 */
                return;
            }
            try {
                if (!cssSkinFailureCache.getKeysWithExpiryCheck().contains(filePathname)) {
                    PortletContext ctx = request.getPortletSession().getPortletContext();

                    String templateRelativePath = skinTemplateIncludeFile
                            .format(new Object[] { rootFolder, lessfileBaseName });
                    String templateFilepath = ctx.getRealPath(templateRelativePath);

                    String includeRelativePath = skinIncludeFile.format(new Object[] { rootFolder, skinToken });
                    String includeFilepath = ctx.getRealPath(includeRelativePath);

                    createLessIncludeFile(request.getPreferences(), includeFilepath, templateFilepath);

                    URL lessCssJavascriptUrl = ctx.getResource(lessCssJavascriptUrlPath);
                    processLessFile(includeFilepath, filePathname, lessCssJavascriptUrl);
                    compiledCssFilepaths.add(filePathname);
                } else {
                    // Though this should never happen except when developers are modifying the LESS files and make a mistake,
                    // if we previously tried to create the CSS file and failed for some reason, don't try to compile it
                    // again for a bit since the process is so processor intensive. It would virtually hang the uPortal
                    // service trying to compile a bad LESS file repeatedly on different threads.
                    log.warn("Skipping generation of CSS file {} due to previous LESS compilation failures",
                            filePathname);
                }
            } catch (Exception e) {
                cssSkinFailureCache.put(new Element(filePathname, filePathname));
                throw new RuntimeException("Error compiling the following LESS file:  " + filePathname, e);
            }
        }

    }

    /**
     * Create the less include file by appending the configurable preference definitions (minus the configuration
     * prefix string) to the end of the template file; e.g. portlet preference name
     * PREFcolor1 is written to the less file as @color1:prefValue
     *
     * @param prefs Portlet preferences
     * @param filename name of the less include file to create
     * @param templateFile template less include file
     * @throws IOException
     */
    private void createLessIncludeFile(PortletPreferences prefs, String filename, String templateFile)
            throws IOException {
        // Create a set of less variable assignments.
        StringBuilder str = new StringBuilder();
        Enumeration<String> prefNames = prefs.getNames();
        while (prefNames.hasMoreElements()) {
            String prefName = prefNames.nextElement();
            if (prefName.startsWith(DynamicSkinService.CONFIGURABLE_PREFIX)) {
                String nameWithoutPrefix = prefName.substring(DynamicSkinService.CONFIGURABLE_PREFIX.length());
                String value = prefs.getValue(prefName, "");

                if (value.trim().equals("")) {
                    log.warn("Dynamic Skin Variable \"{}\" is not set", nameWithoutPrefix);
                } else {
                    str.append("@").append(nameWithoutPrefix).append(": ").append(value).append(";\n");
                }
            }
        }

        // Create byte[]s of the template and preferences content
        byte[] prefsContent = str.toString().getBytes();
        File f = new File(templateFile);
        byte[] templateContent = IOUtils.toByteArray(f.toURI());

        // Create a less include file by appending the less variable definitions to the end of the template less
        // include file.  Insure there is a newline at the end of the template content or the first preference
        // value will be lost.
        byte[] newline = "\n".getBytes();
        byte[] fileContent = new byte[templateContent.length + newline.length + prefsContent.length];
        System.arraycopy(templateContent, 0, fileContent, 0, templateContent.length);
        System.arraycopy(newline, 0, fileContent, templateContent.length, newline.length);
        System.arraycopy(prefsContent, 0, fileContent, templateContent.length + newline.length,
                prefsContent.length);
        File lessInclude = new File(filename);
        IOUtils.write(fileContent, new FileOutputStream(lessInclude));
    }

    /**
     * Less compile the include file into a temporary css file.  When done rename the temporary css file to the
     * correct output filename.  Since the less compilation phase takes several seconds, this insures the
     * output css file is does not exist on the filesystem until it is complete.
     *
     * @param lessIncludeFilepath less include file that includes all dependencies
     * @param outputFilepath name of the output css file
     * @param lessCssJavascriptUrl lessCssJavascript compiler url
     * @throws IOException
     * @throws LessException
     */
    private void processLessFile(String lessIncludeFilepath, String outputFilepath, URL lessCssJavascriptUrl)
            throws IOException, LessException {
        LessSource lessSource = new LessSource(new File(lessIncludeFilepath));
        if (log.isDebugEnabled()) {
            String result = lessSource.getNormalizedContent();
            File lessSourceOutput = new File(outputFilepath + ".lesssource");
            IOUtils.write(result, new FileOutputStream(lessSourceOutput));
            log.debug(
                    "Full Less source from include file {}, using lessCssJavascript at {}"
                            + ", is at {}, output css will be written to {}",
                    lessIncludeFilepath, lessCssJavascriptUrl.toString(), lessSourceOutput, outputFilepath);
        }
        LessCompiler compiler = new LessCompiler();
        compiler.setLessJs(lessCssJavascriptUrl);
        compiler.setCompress(true);
        File tempOutputFile = new File(outputFilepath + "tmp");
        compiler.compile(lessSource, tempOutputFile);
        tempOutputFile.renameTo(new File(outputFilepath));
    }

    @Override
    public String calculateTokenForCurrentSkin(PortletRequest request) {
        int hash = 0;
        PortletPreferences preferences = request.getPreferences();

        // Add the list of preference names to an ordered list so we can get reliable hashcode calculations.
        Map<String, String[]> prefs = preferences.getMap();
        TreeSet<String> orderedNames = new TreeSet<String>(prefs.keySet());
        Iterator<String> iterator = orderedNames.iterator();
        while (iterator.hasNext()) {
            String preferenceName = iterator.next();
            if (preferenceName.startsWith(DynamicSkinService.CONFIGURABLE_PREFIX)) {
                hash = hash * 31 + preferences.getValue(preferenceName, "").trim().hashCode();
            }
        }
        return Integer.toString(hash);
    }

    @Override
    /**
     * Returns the set of skins to use.  This implementation parses the skinList.xml file and returns the set of
     * skin-key element values.  If there is an error parsing the XML file, return an empty set.
     */
    public SortedSet<String> getSkinNames(PortletRequest request) {
        // Context to access the filesystem
        PortletContext ctx = request.getPortletSession().getPortletContext();

        // Determine the full path to the skins directory
        String skinsFilepath = ctx.getRealPath(rootFolder + "/skinList.xml");

        // Create File object to access the filesystem
        File skinList = new File(skinsFilepath);

        TreeSet<String> skins = new TreeSet<>();
        try {
            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
            Document doc = dBuilder.parse(skinList);
            doc.getDocumentElement().normalize();

            NodeList nList = doc.getElementsByTagName("skin-key");
            for (int temp = 0; temp < nList.getLength(); temp++) {
                org.w3c.dom.Element element = (org.w3c.dom.Element) nList.item(temp);
                String skinName = element.getTextContent();
                log.debug("Found skin-key value {}", skinName);
                skins.add(skinName);
            }
        } catch (SAXException | ParserConfigurationException | IOException e) {
            log.error("Error processing skinsFilepath {}", skinsFilepath, e);
        }

        return skins;
    }

}