com.thruzero.common.jsf.support.beans.DynamicContentBean.java Source code

Java tutorial

Introduction

Here is the source code for com.thruzero.common.jsf.support.beans.DynamicContentBean.java

Source

/*
 *   Copyright 2012 George Norman
 *
 *   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 com.thruzero.common.jsf.support.beans;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.lang3.StringUtils;
import org.jdom.JDOMException;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;

import com.thruzero.common.core.infonode.InfoNodeElement;
import com.thruzero.common.core.locator.ProviderLocator;
import com.thruzero.common.core.locator.ServiceLocator;
import com.thruzero.common.core.provider.ResourceProvider;
import com.thruzero.common.core.support.ContainerPath;
import com.thruzero.common.core.support.EntityPath;
import com.thruzero.common.core.utils.PerformanceTimerUtils.PerformanceLoggerHelper;
import com.thruzero.common.jsf.support.ContentQuery;
import com.thruzero.common.jsf.support.content.XmlRootNodeCache;
import com.thruzero.common.jsf.utils.FacesUtils;
import com.thruzero.common.web.model.container.ErrorHtmlPanel;
import com.thruzero.common.web.model.container.PanelGrid;
import com.thruzero.common.web.model.container.PanelSet;
import com.thruzero.common.web.model.nav.MenuBar;
import com.thruzero.common.web.model.nav.MenuNode;
import com.thruzero.domain.model.DataStoreInfo;
import com.thruzero.domain.provider.DataStoreInfoProvider;
import com.thruzero.domain.service.InfoNodeService;

/**
 * Reads content from the data store and builds component models used to render UI components on a page. The data-store may reside in a relational database, the
 * file system or be a remote private service (e.g., Dropbox). The relational database and file system choices are mutually exclusive, however, the private
 * data-store is optional and unique for each individual user.
 * 
 * @author George Norman
 */
@javax.faces.bean.ManagedBean(name = "dynamicContentBean")
@javax.faces.bean.RequestScoped
public class DynamicContentBean implements Serializable {
    private static final long serialVersionUID = 1L;

    private static final ErrorBuilder errorBuilder = new ErrorBuilder();
    private static final ContentQueryBuilder contentQueryBuilder = new ContentQueryBuilder();

    // BUG Fix: http://stackoverflow.com/questions/2968876/final-transient-fields-and-serialization
    private transient DataStoreCache dataStoreCache;

    // ------------------------------------------------------
    // ContentException
    // ------------------------------------------------------

    public static class ContentException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        public ContentException(String message) {
            super(message);
        }

        public ContentException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    // ------------------------------------------------------
    // DataStoreCache
    // ------------------------------------------------------

    /**
     * A cache of root nodes as well as the list of component models built from the root node (see RootNodeCache).
     */
    public static class DataStoreCache {
        private String requestUri;

        // TODO-p0(george). ConcurrentHashMap may not be the best for this (investigate this vs rw lock)
        private final Map<String, RootNodeCache> cache = new ConcurrentHashMap<String, RootNodeCache>();

        public RootNodeCache getRootNodeCache(String key) {
            return cache.get(key);
        }

        public void putRootNodeCache(String key, RootNodeCache value) {
            if (!StringUtils.equals(requestUri, FacesUtils.getRequest().getRequestURI())) {
                cache.clear();
                requestUri = FacesUtils.getRequest().getRequestURI();
            }
            cache.put(key, value);
        }

        public void clear() {
            cache.clear();
        }

        public void clear(String key) {
            cache.remove(key);
        }
    }

    // ------------------------------------------------------
    // RootNodeCacheBuilder
    // ------------------------------------------------------

    public static interface RootNodeCacheBuilder {
        RootNodeCache createRootNodeCache(EntityPath entityPath, DataStoreInfo dataStoreInfo);
    }

    // ------------------------------------------------------
    // ContentQueryBuilder
    // ------------------------------------------------------

    public static class ContentQueryBuilder {

        /**
         * Create the query associated with the given key, for the logged in user (or default, database context, if user isn't logged in).
         */
        public ContentQuery buildFromKey(String key, DataStoreInfo dataStoreInfo) {
            ResourceProvider resourceProvider = ProviderLocator.locate(ResourceProvider.class);
            ContentQuery result = buildFromQuerySpec(resourceProvider.getResource(key), dataStoreInfo);

            return result;
        }

        /**
         * Create the query associated with the given key, for the logged in user (or default, database context, if user isn't logged in).
         * <br>----------------------------- <br>
         * Construct a new ContentQuery using a querySpec that contains a full path for the entity segment (entity path begins with a
         * '/'). The format of the querySpec is a pipe-separated pair, where the first segment defines the full entity path and the second segment defines the
         * xpath. The following example shows a query spec with a full entity path: "/jcat3/home/index.xml|/home/listPanel[@id='quickReference']"
         * <br>----------------------------- <br>
         * Construct a new ContentQuery using a querySpec that contains a partial path for the entity segment (entity path does not
         * begin with a '/'). The format of the querySpec is a pipe-separated pair, where the first segment defines the partial entity path and the second segment
         * defines the xpath. The following example shows a query spec with a partial entity path: "home/index.xml|/home/listPanel[@id='quickReference']"
         */
        public ContentQuery buildFromQuerySpec(String querySpec, DataStoreInfo dataStoreInfo) {
            ContentQuery result;
            String[] specPaths = StringUtils.split(querySpec, "|");

            if (specPaths.length != 2) {
                throw new ContentException(
                        "ERROR with querySpec (may not be defined in resources.properties file): " + querySpec);
            }

            if (querySpec.startsWith(ContainerPath.CONTAINER_PATH_SEPARATOR)) {
                // fully qualified entity path (does not consider the logged in user)
                result = new ContentQuery(new EntityPath(specPaths[0]), specPaths[1]);
            } else {
                // partial entity path, so use the DataStore context, for this user
                String dataStoreContext = dataStoreInfo.getDataStoreContext();

                result = new ContentQuery(new EntityPath(ContainerPath.CONTAINER_PATH_SEPARATOR + dataStoreContext
                        + ContainerPath.CONTAINER_PATH_SEPARATOR + specPaths[0]), specPaths[1]);
            }

            return result;
        }
    }

    // ------------------------------------------------------
    // ErrorBuilder
    // ------------------------------------------------------

    public static class ErrorBuilder {
        public PanelSet buildMissingPanelSetError(String xPath) {
            PanelSet result = new PanelSet("error");

            result.addPanel(new ErrorHtmlPanel("error", "PanelSet Error", "ERROR: Missing PanelSet for " + xPath));

            return result;
        }

        public MenuBar buildMissingMenuBarError(String xPath) {
            MenuBar result = new MenuBar();

            result.addMenu("errorMenuNode",
                    new MenuNode(null, "errorMenuNode", "Menu Error - XPath not found: " + xPath, "", null));

            return result;
        }
    }

    // ------------------------------------------------------
    // RootNodeCache
    // ------------------------------------------------------

    public static abstract class RootNodeCache {
        private final InfoNodeElement rootNode;
        // TODO-p0(george). ConcurrentHashMap may not be the best for this (investigate this vs rw lock)
        private final Map<String, InfoNodeElement> contentNodeCache = new ConcurrentHashMap<String, InfoNodeElement>();
        private final Map<String, PanelSet> panelSetCache = new ConcurrentHashMap<String, PanelSet>();
        private final Map<String, List<PanelGrid>> panelGridListCache = new ConcurrentHashMap<String, List<PanelGrid>>();
        private final Map<String, List<PanelSet>> panelSetListCache = new ConcurrentHashMap<String, List<PanelSet>>();
        private final Map<String, MenuBar> menuBarCache = new ConcurrentHashMap<String, MenuBar>();

        public RootNodeCache(InfoNodeElement rootNode) {
            this.rootNode = rootNode;
        }

        public InfoNodeElement getRootNode() {
            return rootNode;
        }

        /**
         * Return the content node, for the given xPath, by returning it from the cache or finding it in the RootNode and then caching it for future access).
         */
        public InfoNodeElement getContentNode(String xPath) {
            InfoNodeElement result = contentNodeCache.get(xPath);

            if (result == null) {
                try {
                    result = getRootNode().findElement(xPath);
                } catch (JDOMException e) {
                    // return null
                }

                // if found, then cache it; otherwise, return null
                if (result != null) {
                    contentNodeCache.put(xPath, result);
                }
            }

            return result;
        }

        /**
         * Return the PanelSet, for the given xPath, by returning it from the cache or finding its model in the RootNode, then building it and finally caching it
         * for future access.
         */
        public PanelSet getPanelSet(String xPath) throws ContentException {
            PanelSet result = panelSetCache.get(xPath);

            if (result == null) {
                InfoNodeElement panelSetNode = getContentNode(xPath);

                if (panelSetNode != null) {
                    result = buildPanelSet(panelSetNode);
                    panelSetCache.put(xPath, result);
                }
            }

            return result;
        }

        protected abstract PanelGrid buildGridRow(InfoNodeElement panelGridNode) throws ContentException;

        public List<PanelGrid> getPanelGridList(String xPath) throws ContentException {
            List<PanelGrid> result = panelGridListCache.get(xPath);

            if (result == null) {
                InfoNodeElement panelGridListNode = getContentNode(xPath);

                if (panelGridListNode != null) {
                    result = new ArrayList<PanelGrid>();

                    @SuppressWarnings("unchecked")
                    List<InfoNodeElement> panelGridNodes = panelGridListNode.getChildren();

                    for (InfoNodeElement panelGridNode : panelGridNodes) {
                        PanelGrid panelGrid = buildGridRow(panelGridNode);

                        result.add(panelGrid);
                    }

                    panelGridListCache.put(xPath, result);
                }
            }

            return result;
        }

        protected abstract PanelSet buildPanelSet(InfoNodeElement panelSetNode) throws ContentException;

        public List<PanelSet> getPanelSetList(String xPath) throws ContentException {
            List<PanelSet> result = panelSetListCache.get(xPath);

            if (result == null) {
                InfoNodeElement panelSetListNode = getContentNode(xPath);

                if (panelSetListNode != null) {
                    result = new ArrayList<PanelSet>();

                    @SuppressWarnings("unchecked")
                    List<InfoNodeElement> panelSetNodes = panelSetListNode.getChildren();

                    for (InfoNodeElement panelSetNode : panelSetNodes) {
                        PanelSet panelSet = buildPanelSet(panelSetNode);

                        result.add(panelSet);
                    }

                    panelSetListCache.put(xPath, result);
                }
            }

            return result;
        }

        public MenuBar getMenuBar(String xPath) throws ContentException {
            MenuBar result = menuBarCache.get(xPath);

            if (result == null) {
                InfoNodeElement menuBarNode = getContentNode(xPath);

                if (menuBarNode != null) {
                    result = buildMenuBar(menuBarNode);
                    menuBarCache.put(xPath, result);
                }
            }

            return result;
        }

        protected abstract MenuBar buildMenuBar(InfoNodeElement menuBarNode) throws ContentException;

        // Cache management ////////////////////////////////

        /** Delete all content that has been cached by this instance. */
        public void clearContentCache() {
            contentNodeCache.clear();
            panelSetCache.clear();
            menuBarCache.clear();
        }

        /** Delete only the content that has been cached by this instance for the given xPath. */
        public void clearContentCacheFor(String xPath) {
            contentNodeCache.remove(xPath);
            panelSetCache.remove(xPath);
            menuBarCache.remove(xPath);
        }
    }

    // ============================================================================
    // DynamicContentBean
    // ============================================================================

    /**
     * Lazy init transients due to dependencies on Locator.
     * <p/>
     * TODO-p1(george). REVISIT. This issue can happen again with another class unless a better solution is found. Issue: On Tomcat startup, serialization appears
     * to happen before the InitFilter is called. This breaks locator as follows. First, the config locator requires the InitFilter to give it the path to the
     * config file. Second, if readObject is used to init the transients, the locators are accessed causing the Registry to be created and interfaces registered.
     * Then, when InitFilter is called, it causes the Locators to be loaded/initialized again, which generates an error (RegistryLocatorStrategy:57 -
     * InterfaceBindingRegistry ERROR - Binding already exists for given interface: com.thruzero.common.core.config.Config).
     */
    private void ensureTransients() {
        if (dataStoreCache == null) {
            dataStoreCache = new DataStoreCache();
        }
    }

    public String getText(String key) {
        InfoNodeElement resultNode = getContentNode(key);
        if (resultNode == null) {
            return null;
        } else {
            return resultNode.getText();
        }
    }

    public String getSafeText(String key) {
        String result = getText(key);

        if (StringUtils.isNotEmpty(result)) {
            result = Jsoup.clean(result, Whitelist.relaxed());
        }

        return result;
    }

    public InfoNodeElement getContentNode(String key) throws ContentException {
        ensureTransients();

        ContentQuery contentQuery = contentQueryBuilder.buildFromKey(key, getDataStoreInfo());
        InfoNodeElement result = loadContentNode(contentQuery);

        return result;
    }

    // TODO-p1(george). Investigate why JSF EL can't distinguish between ContentQuery and String.
    // Renamed from getContentNode to loadContentNode for now.
    public InfoNodeElement loadContentNode(ContentQuery contentQuery) throws ContentException {
        ensureTransients();

        PerformanceLoggerHelper performanceLoggerHelper = new PerformanceLoggerHelper();
        RootNodeCache rootNodeCache = getRootNodeCache(contentQuery.getEntityPath());

        assertRootNodeCacheFound(rootNodeCache, "Content Node", contentQuery.toString());

        InfoNodeElement result = rootNodeCache.getContentNode(contentQuery.getXPath());
        performanceLoggerHelper.debug("loadContentNode");

        return result;
    }

    public List<PanelGrid> loadPanelGridList(ContentQuery contentQuery) throws ContentException {
        ensureTransients();

        PerformanceLoggerHelper performanceLoggerHelper = new PerformanceLoggerHelper();
        List<PanelGrid> result;
        EntityPath entityPath = contentQuery.getEntityPath();
        String xPath = contentQuery.getXPath();

        if (entityPath == null) {
            result = new ArrayList<PanelGrid>();
        } else {
            RootNodeCache rootNodeCache = getRootNodeCache(entityPath);

            assertRootNodeCacheFound(rootNodeCache, "Row Set List", contentQuery.toString());

            try {
                result = rootNodeCache.getPanelGridList(xPath);
            } catch (Exception e) {
                throw new ContentException("ERROR loading PanelGrid list with: " + xPath, e);
            }
        }
        performanceLoggerHelper.debug("loadPanelGridList");

        return result;
    }

    public PanelSet getPanelSet(String key) throws Exception {
        ensureTransients();

        PerformanceLoggerHelper performanceLoggerHelper = new PerformanceLoggerHelper();
        ContentQuery contentQuery = contentQueryBuilder.buildFromKey(key, getDataStoreInfo());
        RootNodeCache rootNodeCache = getRootNodeCache(contentQuery.getEntityPath());

        assertRootNodeCacheFound(rootNodeCache, "PanelSet", contentQuery.toString());

        PanelSet result = rootNodeCache.getPanelSet(contentQuery.getXPath());

        if (result == null) {
            result = errorBuilder.buildMissingPanelSetError(contentQuery.getXPath());

            // remove bad node
            dataStoreCache.clear(contentQuery.getEntityPath().toString());
        }
        performanceLoggerHelper.debug("getPanelSet {" + key + "}");

        return result;
    }

    public List<PanelSet> getPanelSetList(String key) throws ContentException {
        ensureTransients();

        PerformanceLoggerHelper performanceLoggerHelper = new PerformanceLoggerHelper();
        ContentQuery contentQuery = contentQueryBuilder.buildFromKey(key, getDataStoreInfo());
        List<PanelSet> result = loadPanelSetList(contentQuery);

        if (result == null) {
            result = new ArrayList<PanelSet>();
            result.add(errorBuilder.buildMissingPanelSetError(contentQuery.getXPath()));

            // remove bad node
            dataStoreCache.clear(contentQuery.getEntityPath().toString());
        }
        performanceLoggerHelper.debug("getPanelSetList {" + key + "}");

        return result;
    }

    public List<PanelSet> loadPanelSetList(ContentQuery contentQuery) throws ContentException {
        ensureTransients();

        PerformanceLoggerHelper performanceLoggerHelper = new PerformanceLoggerHelper();
        List<PanelSet> result;
        EntityPath entityPath = contentQuery.getEntityPath();
        String xPath = contentQuery.getXPath();

        if (entityPath == null) {
            result = new ArrayList<PanelSet>();
        } else {
            RootNodeCache rootNodeCache = getRootNodeCache(entityPath);

            assertRootNodeCacheFound(rootNodeCache, "Panel Set List", contentQuery.toString());

            try {
                result = rootNodeCache.getPanelSetList(xPath);
            } catch (Exception e) {
                throw new ContentException("ERROR loading PanelSet list with: " + xPath, e);
            }
        }
        performanceLoggerHelper.debug("loadPanelSetList");

        return result;
    }

    public MenuBar getMenuBar(String key) throws ContentException {
        ensureTransients();

        PerformanceLoggerHelper performanceLoggerHelper = new PerformanceLoggerHelper();
        ContentQuery contentQuery = contentQueryBuilder.buildFromKey(key, getDataStoreInfo());
        RootNodeCache rootNodeCache = getRootNodeCache(contentQuery.getEntityPath());

        assertRootNodeCacheFound(rootNodeCache, "Menu Bar", key);

        MenuBar result = rootNodeCache.getMenuBar(contentQuery.getXPath());

        if (result == null) {
            result = errorBuilder.buildMissingMenuBarError(contentQuery.getXPath());

            // remove bad node
            dataStoreCache.clear(contentQuery.getEntityPath().toString());
        }
        performanceLoggerHelper.debug("getMenuBar {" + key + "}");

        return result;
    }

    public void clearCache() {
        ensureTransients();

        dataStoreCache.clear();
    }

    // Support methods /////////////////////////////////////////////////////////

    protected RootNodeCache getRootNodeCache(EntityPath entityPath) {
        RootNodeCache result = dataStoreCache.getRootNodeCache(entityPath.toString());

        // if not found in cache, then load it from the data store and cache it
        if (result == null) {
            InfoNodeService infoNodeService = ServiceLocator.locate(InfoNodeService.class);
            InfoNodeElement rootNode = infoNodeService.getInfoNode(entityPath, getDataStoreInfo());

            if (rootNode != null) {
                rootNode.enableRootNode();
                result = new XmlRootNodeCache(rootNode);
                dataStoreCache.putRootNodeCache(entityPath.toString(), result);
            }
        }

        return result;
    }

    public DataStoreInfo getDataStoreInfo() {
        DataStoreInfo result = ProviderLocator.locate(DataStoreInfoProvider.class).getDataStoreInfo();

        return result;
    }

    protected void assertRootNodeCacheFound(RootNodeCache rootNodeCache, String contentType, String contentKey) {
        if (rootNodeCache == null) {
            throw new ContentException("ERROR: " + contentType + " not found for key: " + contentKey);
        }
    }

}