org.opencms.xml.CmsXmlEntityResolver.java Source code

Java tutorial

Introduction

Here is the source code for org.opencms.xml.CmsXmlEntityResolver.java

Source

/*
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com)
 *
 * This library 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 library 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.
 *
 * For further information about Alkacon Software GmbH, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.xml;

import org.opencms.configuration.CmsConfigurationManager;
import org.opencms.db.CmsDriverManager;
import org.opencms.db.CmsPublishedResource;
import org.opencms.file.CmsFile;
import org.opencms.file.CmsObject;
import org.opencms.file.CmsResource;
import org.opencms.file.CmsResourceFilter;
import org.opencms.main.CmsEvent;
import org.opencms.main.CmsException;
import org.opencms.main.CmsLog;
import org.opencms.main.I_CmsEventListener;
import org.opencms.main.OpenCms;
import org.opencms.util.CmsCollectionsGenericWrapper;
import org.opencms.util.CmsFileUtil;
import org.opencms.util.CmsUUID;
import org.opencms.xml.page.CmsXmlPage;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;

import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;

/**
 * Resolves XML entities (e.g. external DTDs) in the OpenCms VFS.<p>
 * 
 * Also provides a cache for XML content schema definitions.<p>
 * 
 * @since 6.0.0 
 */
public class CmsXmlEntityResolver implements EntityResolver, I_CmsEventListener {

    /** The scheme to identify a file in the OpenCms VFS. */
    public static final String OPENCMS_SCHEME = "opencms://";

    /** Scheme for files which should be retrieved from the classpath. */
    public static final String INTERNAL_SCHEME = "internal://";

    /** The log object for this class. */
    private static final Log LOG = CmsLog.getLog(CmsXmlEntityResolver.class);

    /** A temporary cache for XML content definitions. */
    private static Map<String, CmsXmlContentDefinition> m_cacheContentDefinitions;

    /** A permanent cache to avoid multiple readings of often used files from the VFS. */
    private static Map<String, byte[]> m_cachePermanent;

    /** A temporary cache to avoid multiple readings of often used files from the VFS. */
    private static Map<String, byte[]> m_cacheTemporary;

    /** The location of the XML page XML schema. */
    private static final String XMLPAGE_OLD_DTD_LOCATION = "org/opencms/xml/page/xmlpage.dtd";

    /**
     * A list of string pairs used to translate legacy system ids to a new form. The first component of each pair
     * is the prefix which should be replaced by the second component of that pair. 
     */
    private static final String[][] m_legacyTranslations = {
            { "opencms://system/modules/org.opencms.ade.config/schemas/", "internal://org/opencms/xml/adeconfig/" },
            { "opencms://system/modules/org.opencms.ade.containerpage/schemas/",
                    "internal://org/opencms/xml/containerpage/" } };

    /** The (old) DTD address of the OpenCms xmlpage (used in 5.3.5). */
    private static final String XMLPAGE_OLD_DTD_SYSTEM_ID_1 = "http://www.opencms.org/dtd/6.0/xmlpage.dtd";

    /** The (old) DTD address of the OpenCms xmlpage (used until 5.3.5). */
    private static final String XMLPAGE_OLD_DTD_SYSTEM_ID_2 = "/system/shared/page.dtd";

    /** The location of the xmlpage XSD. */
    private static final String XMLPAGE_XSD_LOCATION = "org/opencms/xml/page/xmlpage.xsd";

    /** The cms object to use for VFS access (will be initialized with "Guest" permissions). */
    private CmsObject m_cms;

    /**
     * Creates a new XML entity resolver based on the provided CmsObject.<p>
     * 
     * If the provided CmsObject is null, then the OpenCms VFS is not 
     * searched for XML entities, however the internal cache and 
     * other OpenCms internal entities not in the VFS are still resolved.<p> 
     * 
     * @param cms the cms context to use for resolving XML files from the OpenCms VFS
     */
    public CmsXmlEntityResolver(CmsObject cms) {

        initCaches();
        m_cms = cms;
    }

    /**
     * Adds a system ID URL to to internal permanent cache.<p>
     * 
     * This cache will NOT be cleared automatically.<p>
     * 
     * @param systemId the system ID to add
     * @param content the content of the system id
     */
    public static void cacheSystemId(String systemId, byte[] content) {

        initCaches();
        m_cachePermanent.put(systemId, content);
    }

    /**
     * Checks if a given system ID URL is in the internal permanent cache.<p>
     * 
     * This check is required to see if a XML content is based on a file that actually exists in the OpenCms VFS,
     * or if the schema has been just cached without a VFS file.<p>
     * 
     * @param systemId the system id ID check
     * 
     * @return <code>true</code> if the system ID is in the internal permanent cache, <code>false</code> otherwise
     */
    public static boolean isCachedSystemId(String systemId) {

        if (m_cachePermanent != null) {
            return m_cachePermanent.containsKey(systemId);
        }
        return false;
    }

    /**
     * Checks whether the given schema id is an internal schema id or is translated to an internal schema id.<p>
     * @param schema the schema id 
     * @return true if the given schema id is an internal schema id or translated to an internal schema id  
     */
    public static boolean isInternalId(String schema) {

        String translatedId = translateLegacySystemId(schema);
        if (translatedId.startsWith(INTERNAL_SCHEME)) {
            return true;
        }
        return false;
    }

    /**
     * Initialize the OpenCms XML entity resolver.<p>
     * 
     * @param adminCms an initialized OpenCms user context with "Administrator" role permissions
     * @param typeSchemaBytes the base widget type XML schema definitions
     * 
     * @see CmsXmlContentTypeManager#initialize(CmsObject)
     */
    protected static void initialize(CmsObject adminCms, byte[] typeSchemaBytes) {

        // create the resolver to register as event listener
        CmsXmlEntityResolver resolver = new CmsXmlEntityResolver(adminCms);

        // register this object as event listener
        OpenCms.addCmsEventListener(resolver,
                new int[] { I_CmsEventListener.EVENT_CLEAR_CACHES, I_CmsEventListener.EVENT_PUBLISH_PROJECT,
                        I_CmsEventListener.EVENT_RESOURCE_MODIFIED, I_CmsEventListener.EVENT_RESOURCE_MOVED,
                        I_CmsEventListener.EVENT_RESOURCE_DELETED });

        // cache the base widget type XML schema definitions
        cacheSystemId(CmsXmlContentDefinition.XSD_INCLUDE_OPENCMS, typeSchemaBytes);
    }

    /**
     * Initializes the internal caches for permanent and temporary system IDs.<p>
     */
    private static void initCaches() {

        if (m_cacheTemporary == null) {
            Map<String, byte[]> cacheTemporary = CmsCollectionsGenericWrapper.createLRUMap(1024);
            m_cacheTemporary = Collections.synchronizedMap(cacheTemporary);

            Map<String, byte[]> cachePermanent = new HashMap<String, byte[]>(32);
            m_cachePermanent = Collections.synchronizedMap(cachePermanent);

            Map<String, CmsXmlContentDefinition> cacheContentDefinitions = CmsCollectionsGenericWrapper
                    .createLRUMap(512);
            m_cacheContentDefinitions = Collections.synchronizedMap(cacheContentDefinitions);
        }
        if (OpenCms.getRunLevel() > OpenCms.RUNLEVEL_1_CORE_OBJECT) {
            if ((OpenCms.getMemoryMonitor() != null) && !OpenCms.getMemoryMonitor()
                    .isMonitoring(CmsXmlEntityResolver.class.getName() + ".cacheTemporary")) {
                // reinitialize the caches after the memory monitor is set up                
                Map<String, byte[]> cacheTemporary = CmsCollectionsGenericWrapper.createLRUMap(128);
                cacheTemporary.putAll(m_cacheTemporary);
                m_cacheTemporary = Collections.synchronizedMap(cacheTemporary);
                // map must be of type "LRUMap" so that memory monitor can access all information
                OpenCms.getMemoryMonitor().register(CmsXmlEntityResolver.class.getName() + ".cacheTemporary",
                        cacheTemporary);

                Map<String, byte[]> cachePermanent = new HashMap<String, byte[]>(32);
                cachePermanent.putAll(m_cachePermanent);
                m_cachePermanent = Collections.synchronizedMap(cachePermanent);
                // map must be of type "HashMap" so that memory monitor can access all information
                OpenCms.getMemoryMonitor().register(CmsXmlEntityResolver.class.getName() + ".cachePermanent",
                        cachePermanent);

                Map<String, CmsXmlContentDefinition> cacheContentDefinitions = CmsCollectionsGenericWrapper
                        .createLRUMap(64);
                cacheContentDefinitions.putAll(m_cacheContentDefinitions);
                m_cacheContentDefinitions = Collections.synchronizedMap(cacheContentDefinitions);
                // map must be of type "LRUMap" so that memory monitor can access all information
                OpenCms.getMemoryMonitor().register(
                        CmsXmlEntityResolver.class.getName() + ".cacheContentDefinitions", cacheContentDefinitions);
            }
        }
    }

    /**
     * Translates a legacy system id to a new form.<p>
     * 
     * @param systemId the original system id 
     * @return the new system id 
     */
    private static String translateLegacySystemId(String systemId) {

        String result = systemId;
        for (String[] translation : m_legacyTranslations) {
            if (systemId.startsWith(translation[0])) {
                // replace prefix with second component if it matches the first component
                result = translation[1] + systemId.substring(translation[0].length());
                break;
            }
        }
        if (OpenCms.getRepositoryManager() != null) {
            result = OpenCms.getResourceManager().getXsdTranslator().translateResource(result);
        }
        return result;
    }

    /**
     * Caches an XML content definition based on the given system id and the online / offline status
     * of this entity resolver instance.<p>
     * 
     * @param systemId the system id to use as cache key
     * @param contentDefinition the content definition to cache
     */
    public void cacheContentDefinition(String systemId, CmsXmlContentDefinition contentDefinition) {

        String cacheKey = getCacheKeyForCurrentProject(systemId);
        m_cacheContentDefinitions.put(cacheKey, contentDefinition);
        if (LOG.isDebugEnabled()) {
            LOG.debug(Messages.get().getBundle().key(Messages.LOG_ERR_CACHED_SYSTEM_ID_1, cacheKey));
        }
    }

    /**
     * @see org.opencms.main.I_CmsEventListener#cmsEvent(org.opencms.main.CmsEvent)
     */
    public void cmsEvent(CmsEvent event) {

        CmsResource resource;
        switch (event.getType()) {
        case I_CmsEventListener.EVENT_PUBLISH_PROJECT:
            // only flush cache if a schema definition where published
            CmsUUID publishHistoryId = new CmsUUID((String) event.getData().get(I_CmsEventListener.KEY_PUBLISHID));
            if (isSchemaDefinitionInPublishList(publishHistoryId)) {
                m_cacheTemporary.clear();
                m_cacheContentDefinitions.clear();
                if (LOG.isDebugEnabled()) {
                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_ERR_FLUSHED_CACHES_0));
                }
            }
            break;
        case I_CmsEventListener.EVENT_CLEAR_CACHES:
            // flush cache   
            m_cacheTemporary.clear();
            m_cacheContentDefinitions.clear();
            if (LOG.isDebugEnabled()) {
                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ERR_FLUSHED_CACHES_0));
            }
            break;
        case I_CmsEventListener.EVENT_RESOURCE_MODIFIED:
            Object change = event.getData().get(I_CmsEventListener.KEY_CHANGE);
            if ((change != null) && change.equals(new Integer(CmsDriverManager.NOTHING_CHANGED))) {
                // skip lock & unlock
                return;
            }
            resource = (CmsResource) event.getData().get(I_CmsEventListener.KEY_RESOURCE);
            uncacheSystemId(resource.getRootPath());
            break;
        case I_CmsEventListener.EVENT_RESOURCE_DELETED:
        case I_CmsEventListener.EVENT_RESOURCE_MOVED:
            List<CmsResource> resources = CmsCollectionsGenericWrapper
                    .list(event.getData().get(I_CmsEventListener.KEY_RESOURCES));
            for (int i = 0; i < resources.size(); i++) {
                resource = resources.get(i);
                uncacheSystemId(resource.getRootPath());
            }
            break;
        default:
            // no operation
        }
    }

    /**
     * Looks up the given XML content definition system id in the internal content definition cache.<p> 
     * 
     * @param systemId the system id of the XML content definition to look up
     * 
     * @return the XML content definition found, or null if no definition is cached for the given system id
     */
    public CmsXmlContentDefinition getCachedContentDefinition(String systemId) {

        String cacheKey = getCacheKeyForCurrentProject(systemId);
        CmsXmlContentDefinition result = m_cacheContentDefinitions.get(cacheKey);
        if ((result != null) && LOG.isDebugEnabled()) {
            LOG.debug(Messages.get().getBundle().key(Messages.LOG_CACHE_LOOKUP_SUCCEEDED_1, cacheKey));
        }
        return result;
    }

    /**
     * @see org.xml.sax.EntityResolver#resolveEntity(java.lang.String, java.lang.String)
     */
    public InputSource resolveEntity(String publicId, String systemId) {

        // lookup the system id caches first
        byte[] content;
        systemId = translateLegacySystemId(systemId);
        content = m_cachePermanent.get(systemId);
        if (content != null) {

            // permanent cache contains system id
            return new InputSource(new ByteArrayInputStream(content));
        } else if (systemId.equals(CmsXmlPage.XMLPAGE_XSD_SYSTEM_ID)) {

            // XML page XSD reference
            try {
                InputStream stream = getClass().getClassLoader().getResourceAsStream(XMLPAGE_XSD_LOCATION);
                content = CmsFileUtil.readFully(stream);
                // cache the XML page DTD
                m_cachePermanent.put(systemId, content);
                return new InputSource(new ByteArrayInputStream(content));
            } catch (Throwable t) {
                LOG.error(
                        Messages.get().getBundle().key(Messages.LOG_XMLPAGE_XSD_NOT_FOUND_1, XMLPAGE_XSD_LOCATION),
                        t);
            }

        } else if (systemId.equals(XMLPAGE_OLD_DTD_SYSTEM_ID_1) || systemId.endsWith(XMLPAGE_OLD_DTD_SYSTEM_ID_2)) {

            // XML page DTD reference
            try {
                InputStream stream = getClass().getClassLoader().getResourceAsStream(XMLPAGE_OLD_DTD_LOCATION);
                // cache the XML page DTD
                content = CmsFileUtil.readFully(stream);
                m_cachePermanent.put(systemId, content);
                return new InputSource(new ByteArrayInputStream(content));
            } catch (Throwable t) {
                LOG.error(Messages.get().getBundle().key(Messages.LOG_XMLPAGE_DTD_NOT_FOUND_1,
                        XMLPAGE_OLD_DTD_LOCATION), t);
            }
        } else if ((m_cms != null) && systemId.startsWith(OPENCMS_SCHEME)) {

            // opencms:// VFS reference
            String cacheSystemId = systemId.substring(OPENCMS_SCHEME.length() - 1);
            String cacheKey = getCacheKey(cacheSystemId,
                    m_cms.getRequestContext().getCurrentProject().isOnlineProject());
            // look up temporary cache
            content = m_cacheTemporary.get(cacheKey);
            if (content != null) {
                return new InputSource(new ByteArrayInputStream(content));
            }
            String storedSiteRoot = m_cms.getRequestContext().getSiteRoot();
            try {
                // content not cached, read from VFS
                m_cms.getRequestContext().setSiteRoot("/");
                CmsFile file = m_cms.readFile(cacheSystemId, CmsResourceFilter.IGNORE_EXPIRATION);
                content = file.getContents();
                // store content in cache
                m_cacheTemporary.put(cacheKey, content);
                if (LOG.isDebugEnabled()) {
                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_ERR_CACHED_SYS_ID_1, cacheKey));
                }
                return new InputSource(new ByteArrayInputStream(content));
            } catch (Throwable t) {
                LOG.error(Messages.get().getBundle().key(Messages.LOG_ENTITY_RESOLVE_FAILED_1, systemId), t);
            } finally {
                m_cms.getRequestContext().setSiteRoot(storedSiteRoot);
            }

        } else if (systemId.startsWith(INTERNAL_SCHEME)) {
            String location = systemId.substring(INTERNAL_SCHEME.length());
            try {
                InputStream stream = getClass().getClassLoader().getResourceAsStream(location);
                content = CmsFileUtil.readFully(stream);
                m_cachePermanent.put(systemId, content);
                return new InputSource(new ByteArrayInputStream(content));
            } catch (Throwable t) {
                LOG.error(t.getLocalizedMessage(), t);
            }

        } else if (systemId.substring(0, systemId.lastIndexOf("/") + 1)
                .equalsIgnoreCase(CmsConfigurationManager.DEFAULT_DTD_PREFIX)) {
            // default DTD location in the org.opencms.configuration package
            String location = null;
            try {
                String dtdFilename = systemId.substring(systemId.lastIndexOf("/") + 1);
                location = CmsConfigurationManager.DEFAULT_DTD_LOCATION + dtdFilename;
                InputStream stream = getClass().getClassLoader().getResourceAsStream(location);
                content = CmsFileUtil.readFully(stream);
                // cache the DTD
                m_cachePermanent.put(systemId, content);
                return new InputSource(new ByteArrayInputStream(content));
            } catch (Throwable t) {
                LOG.error(Messages.get().getBundle().key(Messages.LOG_DTD_NOT_FOUND_1, location), t);
            }
        }
        // use the default behaviour (i.e. resolve through external URL)
        return null;
    }

    /**
     * Removes a cached entry for a system id (filename) from the internal offline temporary and content definition caches.<p>
     * 
     * The online resources cached for the online project are only flushed when a project is published.<p>
     * 
     * @param systemId the system id (filename) to remove from the cache
     */
    public void uncacheSystemId(String systemId) {

        Object o;
        o = m_cacheTemporary.remove(getCacheKey(systemId, false));
        if (null != o) {
            // if an object was removed from the temporary cache, all XML content definitions must be cleared
            // because this may be a nested subschema 
            m_cacheContentDefinitions.clear();
            if (LOG.isDebugEnabled()) {
                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ERR_UNCACHED_SYS_ID_1,
                        getCacheKey(systemId, false)));
            }
        } else {
            // check if a cached content definition has to be removed based on the system id
            o = m_cacheContentDefinitions.remove(getCacheKey(systemId, false));
            if ((null != o) && LOG.isDebugEnabled()) {
                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ERR_UNCACHED_CONTENT_DEF_1,
                        getCacheKey(systemId, false)));
            }
        }
    }

    /**
     * Returns a cache key for the given system id (filename) based on the status 
     * of the given project flag.<p>
     * 
     * @param systemId the system id (filename) to get the cache key for
     * @param online indicates if this key is generated for the online project
     * 
     * @return the cache key for the system id
     */
    private String getCacheKey(String systemId, boolean online) {

        if (online) {
            return "online_".concat(systemId);
        }
        return "offline_".concat(systemId);
    }

    /**
     * Returns a cache key for the given system id (filename) based on the status 
     * of the internal CmsObject.<p>
     * 
     * @param systemId the system id (filename) to get the cache key for
     * 
     * @return the cache key for the system id
     */
    private String getCacheKeyForCurrentProject(String systemId) {

        // check the project
        boolean project = (m_cms != null) ? m_cms.getRequestContext().getCurrentProject().isOnlineProject() : false;

        // remove opencms:// prefix
        if (systemId.startsWith(OPENCMS_SCHEME)) {
            systemId = systemId.substring(OPENCMS_SCHEME.length() - 1);
        }

        return getCacheKey(systemId, project);
    }

    /**
     * Proves if there is at least one xsd or dtd file in the list of resources to publish.<p>
     * 
     * @param publishHistoryId the publish history id
     * 
     * @return true, if there is at least one xsd or dtd file in the list of resources to publish, otherwise false
     */
    private boolean isSchemaDefinitionInPublishList(CmsUUID publishHistoryId) {

        if (m_cms == null) {
            // CmsObject not available, assume there may be a schema definition in the publish history
            return true;
        }
        try {
            List<CmsPublishedResource> publishedResources = m_cms.readPublishedResources(publishHistoryId);
            for (CmsPublishedResource cmsPublishedResource : publishedResources) {
                String resourceRootPath = cmsPublishedResource.getRootPath();
                String resourceRootPathLowerCase = resourceRootPath.toLowerCase();
                if (resourceRootPathLowerCase.endsWith(".xsd") || resourceRootPathLowerCase.endsWith(".dtd")
                        || m_cacheTemporary.containsKey(getCacheKey(resourceRootPath, true))) {
                    return true;
                }
            }
        } catch (CmsException e) {
            // error reading published Resources.
            LOG.warn(e.getMessage(), e);
        }
        return false;
    }
}