com.manydesigns.portofino.dispatcher.DispatcherLogic.java Source code

Java tutorial

Introduction

Here is the source code for com.manydesigns.portofino.dispatcher.DispatcherLogic.java

Source

/*
 * Copyright (C) 2005-2013 ManyDesigns srl.  All rights reserved.
 * http://www.manydesigns.com/
 *
 * 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 3 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 com.manydesigns.portofino.dispatcher;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.manydesigns.elements.ElementsThreadLocals;
import com.manydesigns.elements.options.DefaultSelectionProvider;
import com.manydesigns.elements.options.SelectionProvider;
import com.manydesigns.elements.util.ElementsFileUtils;
import com.manydesigns.portofino.actions.safemode.SafeModeAction;
import com.manydesigns.portofino.di.Injections;
import com.manydesigns.portofino.pages.Page;
import com.manydesigns.portofino.scripting.ScriptingUtil;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * A collection of methods that operate on {@link Dispatch} instances and related objects.
 *
 * @author Paolo Predonzani     - paolo.predonzani@manydesigns.com
 * @author Angelo Lupo          - angelo.lupo@manydesigns.com
 * @author Giampiero Granatella - giampiero.granatella@manydesigns.com
 * @author Alessio Stalla       - alessio.stalla@manydesigns.com
 */
public class DispatcherLogic {
    public static final String copyright = "Copyright (c) 2005-2013, ManyDesigns srl";

    public static final Logger logger = LoggerFactory.getLogger(DispatcherLogic.class);

    public static SelectionProvider createPagesSelectionProvider(File baseDir, File... excludes) {
        return createPagesSelectionProvider(baseDir, false, false, excludes);
    }

    public static SelectionProvider createPagesSelectionProvider(File baseDir, boolean includeRoot,
            boolean includeDetailChildren, File... excludes) {
        DefaultSelectionProvider selectionProvider = new DefaultSelectionProvider("pages");
        if (includeRoot) {
            Page rootPage;
            try {
                rootPage = getPage(baseDir);
            } catch (Exception e) {
                throw new RuntimeException("Couldn't load root page", e);
            }
            selectionProvider.appendRow("/", rootPage.getTitle() + " (top level)", true);
        }
        appendChildrenToPagesSelectionProvider(baseDir, baseDir, null, selectionProvider, includeDetailChildren,
                excludes);
        return selectionProvider;
    }

    protected static void appendChildrenToPagesSelectionProvider(File baseDir, File parentDir, String breadcrumb,
            DefaultSelectionProvider selectionProvider, boolean includeDetailChildren, File... excludes) {
        FileFilter filter = new FileFilter() {
            public boolean accept(File pathname) {
                return pathname.isDirectory();
            }
        };
        for (File dir : parentDir.listFiles(filter)) {
            try {
                appendToPagesSelectionProvider(baseDir, dir, breadcrumb, selectionProvider, includeDetailChildren,
                        excludes);
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        }
    }

    private static void appendToPagesSelectionProvider(File baseDir, File file, String breadcrumb,
            DefaultSelectionProvider selectionProvider, boolean includeDetailChildren, File... excludes) {
        if (ArrayUtils.contains(excludes, file)) {
            return;
        }
        if (PageInstance.DETAIL.equals(file.getName())) {
            if (includeDetailChildren) {
                breadcrumb += " (detail)"; //TODO I18n
                selectionProvider.appendRow("/" + ElementsFileUtils.getRelativePath(baseDir, file), breadcrumb,
                        true);
                appendChildrenToPagesSelectionProvider(baseDir, file, breadcrumb, selectionProvider,
                        includeDetailChildren, excludes);
            }
        } else {
            Page page;
            try {
                page = getPage(file);
            } catch (Exception e) {
                throw new RuntimeException("Couldn't load page", e);
            }
            if (breadcrumb == null) {
                breadcrumb = page.getTitle();
            } else {
                breadcrumb = String.format("%s > %s", breadcrumb, page.getTitle());
            }
            selectionProvider.appendRow("/" + ElementsFileUtils.getRelativePath(baseDir, file), breadcrumb, true);
            appendChildrenToPagesSelectionProvider(baseDir, file, breadcrumb, selectionProvider,
                    includeDetailChildren, excludes);
        }
    }

    protected static final JAXBContext pagesJaxbContext;

    static {
        try {
            pagesJaxbContext = JAXBContext.newInstance(Page.class.getPackage().getName());
        } catch (JAXBException e) {
            throw new Error("Can't instantiate pages jaxb context", e);
        }
    }

    /**
     * Persists a page to the file system.
     * @param pageInstance the live PageInstance containing the Page to save. 
     * @return the file where the page was saved.
     * @throws Exception in case the save fails.
     */
    public static File savePage(PageInstance pageInstance) throws Exception {
        return savePage(pageInstance.getDirectory(), pageInstance.getPage());
    }

    /**
     * Persists a page to the file system.
     * @param directory the directory where to save the page.xml file.
     * @param page the page to save.
     * @return the file where the page was saved.
     * @throws Exception in case the save fails.
     */
    public static File savePage(File directory, Page page) throws Exception {
        File pageFile = getPageFile(directory);
        Marshaller marshaller = pagesJaxbContext.createMarshaller();
        marshaller.setProperty(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        marshaller.marshal(page, pageFile);
        pageCache.invalidate(pageFile);
        return pageFile;
    }

    //Cache configuration properties
    public static final String PAGE_CACHE_SIZE = "page.cache.size";
    public static final String PAGE_CACHE_CHECK_FREQUENCY = "page.cache.check.frequency";
    public static final String CONFIGURATION_CACHE_SIZE = "configuration.cache.size";
    public static final String CONFIGURATION_CACHE_CHECK_FREQUENCY = "configuration.cache.check.frequency";

    public static void init(Configuration portofinoConfiguration) {
        int maxSize, refreshCheckFrequency;
        maxSize = portofinoConfiguration.getInt(PAGE_CACHE_SIZE, 1000);
        refreshCheckFrequency = portofinoConfiguration.getInt(PAGE_CACHE_CHECK_FREQUENCY, 5);
        initPageCache(maxSize, refreshCheckFrequency);
        maxSize = portofinoConfiguration.getInt(CONFIGURATION_CACHE_SIZE, 1000);
        refreshCheckFrequency = portofinoConfiguration.getInt(CONFIGURATION_CACHE_CHECK_FREQUENCY, 5);
        initConfigurationCache(maxSize, refreshCheckFrequency);
    }

    protected static class FileCacheEntry<T> {
        public final T object;
        public final long lastModified;
        public final boolean error;

        public FileCacheEntry(T object, long lastModified, boolean error) {
            this.object = object;
            this.lastModified = lastModified;
            this.error = error;
        }
    }

    protected static class ConfigurationCacheEntry extends FileCacheEntry<Object> {
        public final Class<?> configurationClass;

        public ConfigurationCacheEntry(Object configuration, Class<?> configurationClass, long lastModified,
                boolean error) {
            super(configuration, lastModified, error);
            this.configurationClass = configurationClass;
        }
    }

    //NB il reload delle cache  _asincrono_ rispetto alla get,  quindi possibile che una get ritorni
    //un valore vecchio anche nel caso in cui sia appena stato rilevato un errore nel reload (es. ho scritto
    //caratteri invalidi all'inizio dell'xml).

    protected static LoadingCache<File, FileCacheEntry<Page>> pageCache;

    public static void initPageCache(int maxSize, int refreshCheckFrequency) {
        pageCache = CacheBuilder.newBuilder().maximumSize(maxSize)
                .refreshAfterWrite(refreshCheckFrequency, TimeUnit.SECONDS)
                .build(new CacheLoader<File, FileCacheEntry<Page>>() {

                    @Override
                    public FileCacheEntry<Page> load(File key) throws Exception {
                        return new FileCacheEntry<Page>(loadPage(key), key.lastModified(), false);
                    }

                    @Override
                    public ListenableFuture<FileCacheEntry<Page>> reload(final File key,
                            FileCacheEntry<Page> oldValue) throws Exception {
                        if (!key.exists()) {
                            //Se la pagina non esiste pi, registro questo fatto nella cache;
                            //a questo livello non  un errore, sar il metodo getPage() a gestire
                            //la entry problematica.
                            return Futures.immediateFuture(new FileCacheEntry<Page>(null, 0, true));
                        } else if (key.lastModified() > oldValue.lastModified) {
                            /*return ListenableFutureTask.create(new Callable<PageCacheEntry>() {
                                public PageCacheEntry call() throws Exception {
                                    return doLoad(key);
                                }
                            });*/
                            //TODO async?
                            try {
                                Page page = loadPage(key);
                                return Futures
                                        .immediateFuture(new FileCacheEntry<Page>(page, key.lastModified(), false));
                            } catch (Throwable t) {
                                logger.error("Could not reload cached page from " + key.getAbsolutePath()
                                        + ", removing from cache", t);
                                return Futures
                                        .immediateFuture(new FileCacheEntry<Page>(null, key.lastModified(), true));
                            }
                        } else {
                            return Futures.immediateFuture(oldValue);
                        }
                    }

                });
    }

    protected static LoadingCache<File, ConfigurationCacheEntry> configurationCache;

    public static void initConfigurationCache(int maxSize, int refreshCheckFrequency) {
        configurationCache = CacheBuilder.newBuilder().maximumSize(maxSize)
                .refreshAfterWrite(refreshCheckFrequency, TimeUnit.SECONDS)
                .build(new CacheLoader<File, ConfigurationCacheEntry>() {

                    @Override
                    public ConfigurationCacheEntry load(File key) throws Exception {
                        throw new UnsupportedOperationException();
                    }

                    @Override
                    public ListenableFuture<ConfigurationCacheEntry> reload(final File key,
                            ConfigurationCacheEntry oldValue) throws Exception {
                        if (!key.exists()) {
                            //Se la conf. non esiste pi, la marco come errata;
                            //il metodo getConfiguration provveder a rimuoverla (contrariamente
                            //alla page, non  possibile lasciare l'oggetto in stato errato nella
                            //cache perch in generale potrebbero mancare le informazioni per ricaricarlo
                            //correttamente... TODO da verificare meglio!!!)
                            return Futures.immediateFuture(new ConfigurationCacheEntry(null, null, 0, true));
                        } else if (key.lastModified() > oldValue.lastModified) {
                            //TODO se oldValue.error non dovrei ricaricare (informazioni incomplete) - ?
                            //TODO async?
                            try {
                                Object newConf = loadConfiguration(key, oldValue.configurationClass);
                                return Futures.immediateFuture(new ConfigurationCacheEntry(newConf,
                                        newConf.getClass(), key.lastModified(), false));
                            } catch (Throwable t) {
                                logger.error("Could not reload cached configuration from " + key.getAbsolutePath()
                                        + ", removing from cache", t);
                                return Futures.immediateFuture(new ConfigurationCacheEntry(null, null, 0, true));
                            }
                        } else {
                            return Futures.immediateFuture(oldValue);
                        }
                    }

                });
    }

    public static void clearConfigurationCache() {
        configurationCache.invalidateAll();
    }

    /**
     * Clears the cache from entries whose class matches exactly with the one passed as a parameter.
     * @param configurationClass the class of the entries to remove.
     */
    public static void clearConfigurationCache(Class configurationClass) {
        Set<Map.Entry<File, ConfigurationCacheEntry>> entries = configurationCache.asMap().entrySet();
        List<File> keysToInvalidate = new ArrayList<File>();
        for (Map.Entry<File, ConfigurationCacheEntry> entry : entries) {
            if (entry.getValue().configurationClass == configurationClass) {
                keysToInvalidate.add(entry.getKey());
            }
        }
        configurationCache.invalidateAll(keysToInvalidate);
    }

    protected static File getPageFile(File directory) {
        return new File(directory, "page.xml");
    }

    public static Page loadPage(File key) throws Exception {
        FileInputStream fileInputStream = new FileInputStream(key);
        try {
            Page page = loadPage(fileInputStream);
            page.init();
            return page;
        } finally {
            IOUtils.closeQuietly(fileInputStream);
        }
    }

    public static Page loadPage(InputStream inputStream) throws JAXBException {
        Unmarshaller unmarshaller = pagesJaxbContext.createUnmarshaller();
        return (Page) unmarshaller.unmarshal(inputStream);
    }

    public static Page getPage(File directory) throws PageNotActiveException {
        File pageFile = getPageFile(directory);
        /*if(!pageFile.exists()) {
        pageCache.invalidate(pageFile);
        return null;
        }*/
        try {
            FileCacheEntry<Page> entry = pageCache.get(pageFile);
            if (!entry.error) {
                return entry.object;
            } else {
                throw new PageNotActiveException(pageFile.getAbsolutePath());
            }
        } catch (ExecutionException e) {
            throw new PageNotActiveException(pageFile.getAbsolutePath(), e);
        }
    }

    public static File saveConfiguration(File directory, Object configuration) throws Exception {
        String configurationPackage = configuration.getClass().getPackage().getName();
        JAXBContext jaxbContext = JAXBContext.newInstance(configurationPackage);
        Marshaller marshaller = jaxbContext.createMarshaller();
        marshaller.setProperty(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        File configurationFile = new File(directory, "configuration.xml");
        marshaller.marshal(configuration, configurationFile);
        configurationCache.invalidate(configurationFile);
        return configurationFile;
    }

    public static <T> T getConfiguration(File configurationFile, Class<? extends T> configurationClass)
            throws Exception {
        if (configurationClass == null) {
            return null;
        }
        ConfigurationCacheEntry entry = configurationCache.getIfPresent(configurationFile);
        if (entry == null || !configurationClass.isInstance(entry.object) || entry.error) {
            if (entry != null && entry.error) {
                logger.warn("Cached configuration for {} is in error state, forcing a reload",
                        configurationFile.getAbsolutePath());
            } else if (entry != null && !configurationClass.isInstance(entry.object)) {
                logger.warn("Cached configuration for {} is an instance of the wrong class, forcing a reload",
                        configurationFile.getAbsolutePath());
            }
            T configuration = loadConfiguration(configurationFile, configurationClass);
            entry = new ConfigurationCacheEntry(configuration, configurationClass, configurationFile.lastModified(),
                    false);
            configurationCache.put(configurationFile, entry);
        }
        return (T) entry.object;
    }

    public static <T> T loadConfiguration(File configurationFile, Class<? extends T> configurationClass)
            throws Exception {
        if (configurationClass == null) {
            return null;
        }
        InputStream inputStream = new FileInputStream(configurationFile);
        try {
            return loadConfiguration(inputStream, configurationClass);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
    }

    public static <T> T loadConfiguration(InputStream inputStream, Class<? extends T> configurationClass)
            throws Exception {
        if (configurationClass == null) {
            return null;
        }
        Object configuration;
        String configurationPackage = configurationClass.getPackage().getName();
        JAXBContext jaxbContext = JAXBContext.newInstance(configurationPackage);
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
        configuration = unmarshaller.unmarshal(inputStream);
        if (!configurationClass.isInstance(configuration)) {
            logger.error("Invalid configuration: expected " + configurationClass + ", got " + configuration);
            return null;
        }
        Injections.inject(configuration, ElementsThreadLocals.getServletContext(),
                ElementsThreadLocals.getHttpServletRequest());
        if (configuration instanceof PageActionConfiguration) {
            ((PageActionConfiguration) configuration).init();
        }
        return (T) configuration;
    }

    public static Class<? extends PageAction> getActionClass(Configuration configuration, File directory) {
        return getActionClass(configuration, directory, true);
    }

    public static Class<? extends PageAction> getActionClass(Configuration configuration, File directory,
            boolean fallback) {
        File scriptFile = ScriptingUtil.getGroovyScriptFile(directory, "action");
        Class<? extends PageAction> actionClass;
        try {
            actionClass = (Class<? extends PageAction>) ScriptingUtil.getGroovyClass(scriptFile);
        } catch (Exception e) {
            logger.error("Couldn't load action class for " + directory + ", returning safe-mode action", e);
            return fallback ? SafeModeAction.class : null;
        }
        if (isValidActionClass(actionClass)) {
            return actionClass;
        } else {
            logger.error("Invalid action class for " + directory + ": " + actionClass);
            return fallback ? SafeModeAction.class : null;
        }
    }

    public static boolean isValidActionClass(Class<?> actionClass) {
        if (actionClass == null) {
            return false;
        }
        if (!PageAction.class.isAssignableFrom(actionClass)) {
            logger.error("Action " + actionClass + " must implement " + PageAction.class);
            return false;
        }
        return true;
    }
}