All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.randombits.confluence.metadata.impl.DefaultMetadataManager Maven / Gradle / Ivy

There is a newer version: 7.4.1
Show newest version
package org.randombits.confluence.metadata.impl;

import com.atlassian.cache.Cache;
import com.atlassian.cache.CacheManager;
import com.atlassian.confluence.core.ContentEntityManager;
import com.atlassian.confluence.core.ContentEntityObject;
import com.atlassian.confluence.core.ContentPropertyManager;
import com.atlassian.confluence.core.VersionHistorySummary;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.plugin.PluginAccessor;
import com.atlassian.plugin.event.PluginEventManager;
import com.atlassian.plugin.tracker.DefaultPluginModuleTracker;
import com.atlassian.plugin.tracker.PluginModuleTracker;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.BaseException;
import org.apache.log4j.Logger;
import org.randombits.confluence.metadata.*;

import org.randombits.confluence.metadata.event.MetadataUpdatedEvent;
import org.randombits.confluence.metadata.event.MetadataUpdatedEventEmitter;
import org.randombits.confluence.metadata.impl.handler.DirectTypeHandler;
import org.randombits.confluence.metadata.FieldNotFoundException;
import org.randombits.confluence.metadata.indexing.IndexManager;
import org.randombits.confluence.metadata.xstream.MetadataXStream;
import org.randombits.storage.Aliasable;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Qualifier;
import com.opensymphony.xwork.ActionContext;

import java.io.InvalidClassException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;

import static org.apache.log4j.Level.WARN;

public class DefaultMetadataManager implements MetadataManager, DisposableBean, MetadataUpdatedEventEmitter {

    private static final Logger LOG = Logger.getLogger( MetadataManager.class );

    private static final String METADATA_PREFIX = "~metadata";
    private static final String METADATA_CACHE_KEY = MetadataManager.class.getName();

    private static final String WORKFLOWS_PUBLISHED_ACTION_NAME = "releaseview";

    private final CacheManager cacheManager;
    private final ContentPropertyManager contentPropertyManager;
    private final ContentEntityManager contentEntityManager;
    private final PluginModuleTracker typeHandlerTracker;
    private final EventPublisher eventPublisher;

    private XStream xStream;
    private List typeHandlers;
    private IndexManager indexManager;

    public DefaultMetadataManager(
            CacheManager cacheManager,
            PluginAccessor pluginAccessor,
            PluginEventManager pluginEventManager,
            ContentPropertyManager contentPropertyManager,
            @Qualifier("contentEntityManager") ContentEntityManager contentEntityManager,
            IndexManager indexManager,
            EventPublisher eventPublisher) {
        this.cacheManager = cacheManager;
        this.contentPropertyManager = contentPropertyManager;
        this.contentEntityManager = contentEntityManager;
        this.indexManager = indexManager;

        this.eventPublisher = eventPublisher;

        typeHandlers = new ArrayList();

        xStream = new MetadataXStream();
        xStream.setClassLoader( getClass().getClassLoader() );
        applyAliases( this );

        initTypeHandlers();

        typeHandlerTracker = new DefaultPluginModuleTracker( pluginAccessor, pluginEventManager, TypeHandlerModuleDescriptor.class,
                new PluginModuleTracker.Customizer() {
                    @Override
                    public TypeHandlerModuleDescriptor adding( TypeHandlerModuleDescriptor moduleDescriptor ) {
                        addTypeHandler( moduleDescriptor.getModule() );
                        return moduleDescriptor;
                    }

                    @Override
                    public void removed( TypeHandlerModuleDescriptor moduleDescriptor ) {
                        removeTypeHandler( moduleDescriptor.getModule() );
                    }
                } );
    }

    protected void initTypeHandlers() {
        addDirectTypeHandler(
                String.class, Character.class, char.class,
                Byte.class, byte.class, Short.class, short.class, Integer.class, int.class, Long.class, long.class,
                Float.class, float.class, Double.class, double.class, BigDecimal.class, BigInteger.class,
                Boolean.class, boolean.class,
                Date.class);
    }

    private void addDirectTypeHandler( Class... type ) {
        addTypeHandler(new DirectTypeHandler(type));
    }

    public void addTypeHandler( TypeHandler handler ) {
        typeHandlers.add(handler);
        if ( handler instanceof HasAlias)
            ( (HasAlias) handler ).applyAliases( this );
    }

    public void removeTypeHandler( TypeHandler handler ) {
        typeHandlers.remove(handler);
    }

    /**
     * Returns the provided 'storable' value as the original value.
     *
     * @param storable The stored value.
     * @return The original, or null if it is not supported.
     */
    public Object fromStorable( Object storable ) throws TypeConversionException {
        if ( storable == null )
            return null;

        for ( TypeHandler handler : typeHandlers ) {
            if ( handler.supportsStorable(storable) )
                return handler.getOriginal(storable);
        }
        return null;
    }

    public Object toStorable( Object original ) throws TypeConversionException {
        if ( original == null )
            return null;

        for ( TypeHandler handler : typeHandlers ) {
            if ( handler.supportsOriginal(original) )
                return handler.getStorable(original);
        }
        return null;
    }

    public void applyAliases( XStream xStream ) {
        applyAliases( new AliasableXStream( xStream ) );
    }

    /**
     * Applies the standard metadata aliases to the {@link org.randombits.storage.Aliasable} instance.
     *
     * @param aliasable The {@link org.randombits.storage.Aliasable} instance to alias.
     */
    public void applyAliases( Aliasable aliasable ) {
        // Alias the standard reference classes.
        for ( TypeHandler handler : typeHandlers ) {
            if ( handler instanceof HasAlias)
                ( (HasAlias) handler ).applyAliases( aliasable );
        }
    }

    /**
     * Returns a content property value for the specified content/key combo.
     *
     * @param content The content object.
     * @param key     The key to retrieve.
     * @return The property value.
     */
    private String getProperty( ContentEntityObject content, String key ) {
        return contentPropertyManager.getTextProperty(content, key);
    }

    private ContentEntityObject findVersion( ContentEntityObject content, int version ) {
        if ( content.getVersion() == version )
            return content;

        if ( !content.isLatestVersion() )
            content = (ContentEntityObject) content.getLatestVersion();

        return findVersionNoChecks(content, version);
    }

    private ContentEntityObject findVersionNoChecks( ContentEntityObject content, int version ) {

        List summaries = contentEntityManager.getVersionHistorySummaries( content );
        for ( VersionHistorySummary summary : summaries ) {
            if ( summary.getVersion() == version )
                return contentEntityManager.getById( summary.getId() );
        }

        return null;
    }

    private Cache getCache() throws InvalidClassException {
        return cacheManager.getCache(METADATA_CACHE_KEY);
    }

    /**
     * Loads a read-only, cached copy of the current data for the specified
     * content. This is much more efficient than
     * {@link #loadWritableData(ContentEntityObject)} when you only need read
     * access to the current data.
     *
     * @param content The content object.
     * @return The current metadata.
     */
    public MetadataStorage loadReadableData( ContentEntityObject content ) {
        Map data = null;
        Cache cache = null;
        Long id = null;
        MetadataStorage metadata;

        // Comala Workflows page version override if viewing the published version of a page.
        if (ActionContext.getContext() != null && WORKFLOWS_PUBLISHED_ACTION_NAME.equals( ActionContext.getContext().getName() ) ) {
            ContentEntityObject publishedContent = findVersionNoChecks(content, content.getVersion());
            if( publishedContent != null ){
                content = publishedContent;
            }
        }

        if ( content != null ) {
            try {
                id = content.getId();
                cache =  getCache();
                data = (Map) cache.get( id );

            } catch ( InvalidClassException ice ) {
                // InvalidClassException could happen when there is serialiation problem.
                // Users in clustered environment had reported this problem before.
                if (LOG.isEnabledFor(WARN)) {
                    LOG.warn("Could not find cache for ID '" + id + "': " + ice.getMessage(), ice);
                }
            }
        }

        if ( null == content ) {
            // TODO [20140217 YCL] What's the reason content is null? Someone please answer.  This is causing NPE at loadDataXML().
        }

        if ( data == null ) {
            data = loadDataMap( content );
            cache.put( id, data );
        }

        if ( data != null ) {
            metadata = new StandardMetadataStorage( this, content, data, true );
        } else {
            metadata = new EmptyMetadataStorage();
        }

        return metadata;
    }

    /**
     * Loads the data for the specified content object. If the version number is
     * not valid, null is returned.
     *
     * @param content The content object.
     * @param version The version to retrieve.
     * @return the data for the specified version of the content object.
     */
    public MetadataStorage loadWritableData( ContentEntityObject content, int version ) {
        content = findVersion( content, version );
        if ( content != null )
            return loadWritableData( content );
        return null;
    }

    /**
     * Loads the data into a MetadataStorage instance. The storage will load any
     * legacy data, if it exists.
     *
     * @param content The content object.
     * @return The metadata storage.
     */
    public MetadataStorage loadWritableData( ContentEntityObject content ) {
        return loadNewWritableData(content, content);
    }

    /**
     * Loads the data from an existing content to a new content
     * into a MetadataStorage instance. The storage will load any
     * legacy data, if it exists.
     *
     * @param contentFrom The content object.
     * @param contentTo The content object.
     * @return The metadata storage.
     */
    public MetadataStorage loadNewWritableData( ContentEntityObject contentFrom, ContentEntityObject contentTo ) {
        MetadataStorage metadata = null;

        if ( contentFrom != null ) {
            Map data = loadDataMap( contentFrom );

            if ( data == null )
                data = new HashMap();

            metadata = new StandardMetadataStorage( this, contentTo, data, false );
        } else {
            metadata = new EmptyMetadataStorage();
        }

        return metadata;
    }

    private Map loadDataMap( ContentEntityObject content ) {
        // look for the data.
        Map data = null;
        String xml = loadDataXML( content );

        if ( xml != null ) {
            try {
                data = (Map) xStream.fromXML( xml );
            } catch ( BaseException e ) {
                LOG.error( "Conversion issue while loading metadata for content: " + content + "; version: "
                        + content.getVersion(), e );
                data = new MetadataMap();
            }
        }
        if (data == null) {
            data = new MetadataMap();
        }
        return data;
    }

    /**
     * Loads the data for the specified content object, returning it as an XML
     * string. This method will not load any legacy data.
     *
     * @param content The content object.
     * @return The XML String, or null if no data has been saved.
     */
    public String loadDataXML( ContentEntityObject content ) {
        int version = content.getVersion();
        ContentEntityObject latest = (ContentEntityObject) content.getLatestVersion();

        for ( int i = version; i >= 1; i-- ) {
            String xml = getProperty( latest, METADATA_PREFIX + "." + i );
            if ( xml != null ) {
                return xml;
            }
        }

        // If we get this far, we couldn't find any metadata.
        return null;
    }

    /**
     * Saves the data as the next version. This is useful for pre-saving data
     * when other processes will manage the actual creation of the next content
     * version.
     *
     * @param data The data to save.
     */
    public void saveNextData( MetadataStorage data ) {
        this.saveData( data, false );
    }

    /**
     * Saves the metadata. If overwriteCurrent is
     * true, the current set of existing data will be
     * overwritten. If not, it will be saved to a version one number greater
     * than the current version for the content on the assumption that the
     * content object will be saved to a new version immediately afterwards.
     *
     * @param data                  The metadata to save.
     * @param currentContentVersion If true the previous version will be
     *                              overwritten.
     */
    public void saveData( MetadataStorage data, boolean currentContentVersion ) {
        saveData(data, currentContentVersion, true);
    }

    /**
     * Saves the metadata. If overwriteCurrent is
     * true, the current set of existing data will be
     * overwritten. If not, it will be saved to a version one number greater
     * than the current version for the content on the assumption that the
     * content object will be saved to a new version immediately afterwards.
     *
     * @param data                  The metadata to save.
     * @param currentContentVersion If true the previous version will be
     *                              overwritten.
     * @param buildIndex               If true will trigger buildIndex()
     */
    public void saveData(MetadataStorage data, boolean currentContentVersion, boolean buildIndex) {
        ContentEntityObject content = data.getContent();

        if (content != null) {
            ContentEntityObject latest = (ContentEntityObject) content.getLatestVersion();
            updateMetadata(data, latest, latest.getVersion(), currentContentVersion, buildIndex);
        }
    }

    /**
     * Save the metadata for {set-data} macro. It will compare the version of metadata
     * to be saved with the latest version of metadata. It will not update the
     * metadata if the version is not match. This is to prevent {set-data} macro
     * to override metadata version when user view the page history.
     * @see https://tools.servicerocket.com/browse/AN22-358
     *
     * @param data The metadata to be saved
     */
    @Deprecated
    public void saveSetData(MetadataStorage data) {
        ContentEntityObject content = data.getContent();
        if (content != null) {
            ContentEntityObject latestEntityObject = (ContentEntityObject) content.getLatestVersion();
            int latestVersion = latestEntityObject.getVersion();

            if(content.getVersion() == latestVersion) {
                updateMetadata(data, latestEntityObject, latestVersion, true, true);
            }
        }
    }

    private void updateMetadata(MetadataStorage metadata, ContentEntityObject latestEntityObject, int latestVersion, boolean currentContentVersion, boolean buildIndex) {
        if (latestVersion == 0 || !currentContentVersion) {
            latestVersion++;
        }

        String xml = xStream.toXML(toMetadataMap(metadata.getBaseMap()));
        contentPropertyManager.setTextProperty(latestEntityObject, METADATA_PREFIX + "." + latestVersion, xml);
        eventPublisher.publish(new MetadataUpdatedEvent(this, metadata, buildIndex));
        clearCache(latestEntityObject);
    }

    /**
     * Clears the cache for the specified content.
     *
     * @param content The content to clear the metadata cache for.
     */
    private void clearCache( ContentEntityObject content ) {
        try {
            Cache readThroughCache = getCache();
            Long id = content.getId();
            readThroughCache.remove( id );
        } catch(InvalidClassException ice) {
            if (LOG.isEnabledFor(WARN)) {
                LOG.warn("Could not clear cache for content '" + content.getIdAsString() + "': " + ice.getMessage(), ice);
            }
        }
    }

    /**
     * By default, the metadata manager will cache up to 1000 data items for 60
     * minutes. This will clear any cached data from memory.
     */
    public void clearCache() {
        try {
            getCache().removeAll();
        } catch(InvalidClassException ice) {
            if (LOG.isEnabledFor(WARN)) {
                LOG.warn("Could not clear cache: " + ice.getMessage(), ice);
            }
        }
    }

    /**
     * Clears the data storage area for the next version of the content object.
     * This is useful when data has been pre-saved and the overall page-save
     * fails.
     *
     * @param content The content object.
     */
    public void clearNextData( ContentEntityObject content ) {
        if ( !content.isLatestVersion() )
            content = (ContentEntityObject) content.getLatestVersion();

        contentPropertyManager.setTextProperty(content, METADATA_PREFIX + "." + ( content.getVersion() + 1 ), null);
    }

    /**
     * Converts a regular map structure into a MetadataMap structure for
     * storage.
     *
     * @param baseMap The Map to convert.
     * @return the map as a {@link org.randombits.confluence.metadata.MetadataMap}.
     */
    private MetadataMap toMetadataMap( Map baseMap ) {
        if ( baseMap == null )
            return null;

        if ( baseMap instanceof MetadataMap)
            return (MetadataMap) baseMap;

        MetadataMap metadataMap = new MetadataMap();

        for ( Map.Entry e : baseMap.entrySet() ) {
            Object value = e.getValue();
            if ( value instanceof Map )
                metadataMap.put( e.getKey(), toMetadataMap( (Map) value ) );
            else
                metadataMap.put( e.getKey(), e.getValue() );
        }

        return metadataMap;
    }

    /**
     * Adds an alias for the specified class. Aliases are used in the
     * serialization/deserialization process.
     *
     * @param alias        The alias to add.
     * @param aliasedClass The class being aliased.
     */
    public void addAlias( String alias, Class aliasedClass ) {
        xStream.alias( alias, aliasedClass );
    }

    /**
     * Returns a read-only {@link org.randombits.confluence.metadata.MetadataStorage} instance based on the
     * provided {@link Map}. Trying to save the map will fail, since it is not
     * associated with a particular piece of content.
     *
     * @param map The map to use.
     * @return The {@link org.randombits.confluence.metadata.MetadataStorage} instance.
     */
    public MetadataStorage asMetadataStorage( Map map ) {
        return new StandardMetadataStorage( this, null, map, true );
    }


    public boolean isStorable( Object value ) {
        for ( TypeHandler handler : typeHandlers ) {
            if ( handler.supportsStorable( value ) )
                return true;
        }
        return false;
    }

    @Override
    public String toXML( Object value ) throws TypeConversionException {
        value = toStorable( value );
        if ( value != null ) {
            try {
                return xStream.toXML( value );
            } catch ( Exception e ) {
                throw new TypeConversionException( e.getMessage(), e );
            }
        } else {
            return null;
        }
    }

    @Override
    public Object fromXML( String xml ) throws TypeConversionException {
        if ( xml != null ) {
            try {
                Object value = xStream.fromXML( xml );
                return fromStorable( value );
            } catch ( Exception e ) {
                throw new TypeConversionException( e.getMessage(), e );
            }
        }
        return null;
    }

    private static class AliasableXStream implements Aliasable {

        XStream xStream;

        AliasableXStream( XStream xStream ) {
            this.xStream = xStream;
        }

        /**
         * Adds the specified alias for the class.
         *
         * @param alias        The alias.
         * @param aliasedClass The class to alias.
         */
        public void addAlias( String alias, Class aliasedClass ) {
            xStream.alias( alias, aliasedClass );
        }
    }

    @Override
    public void destroy() throws Exception {
        typeHandlers.clear();
        typeHandlerTracker.close();
    }

    @Override
    public  T queryIndex(String contentId, String fieldName, Class clazz) throws IllegalArgumentException, FieldNotFoundException {
        return indexManager.query(contentId, fieldName, clazz);
    }

    @Override
    public  T queryIndex(String contentId, List path, String fieldName, Class clazz) throws IllegalArgumentException, FieldNotFoundException {
        return indexManager.query(contentId, path, fieldName, clazz);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy