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

com.alee.managers.style.data.SkinInfoConverter Maven / Gradle / Ivy

Go to download

WebLaf is a Java Swing Look and Feel and extended components library for cross-platform applications

There is a newer version: 2.2.1
Show newest version
/*
 * This file is part of WebLookAndFeel library.
 *
 * WebLookAndFeel library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * WebLookAndFeel 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with WebLookAndFeel library.  If not, see .
 */

package com.alee.managers.style.data;

import com.alee.managers.style.StyleException;
import com.alee.managers.style.StyleableComponent;
import com.alee.utils.CompareUtils;
import com.alee.utils.TextUtils;
import com.alee.utils.XmlUtils;
import com.alee.utils.xml.ResourceFile;
import com.alee.utils.xml.ResourceLocation;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.reflection.ReflectionConverter;
import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.mapper.Mapper;

import java.util.*;

/**
 * Custom XStream converter for {@link com.alee.managers.style.data.SkinInfo} class.
 *
 * @author Mikle Garin
 * @see How to use StyleManager
 * @see com.alee.managers.style.StyleManager
 * @see com.alee.managers.style.data.SkinInfo
 */

public final class SkinInfoConverter extends ReflectionConverter
{
    /**
     * todo 1. Create proper object->xml marshalling strategy
     */

    /**
     * Converter constants.
     */
    public static final String ID_NODE = "id";
    public static final String ICON_NODE = "icon";
    public static final String TITLE_NODE = "title";
    public static final String DESCRIPTION_NODE = "description";
    public static final String AUTHOR_NODE = "author";
    public static final String SUPPORTED_SYSTEMS_NODE = "supportedSystems";
    public static final String CLASS_NODE = "class";
    public static final String INCLUDE_NODE = "include";
    public static final String STYLE_NODE = "style";
    public static final String NEAR_CLASS_ATTRIBUTE = "nearClass";

    /**
     * Context variables.
     */
    public static final String SUBSEQUENT_SKIN = "subsequent.skin";
    public static final String SKIN_CLASS = "skin.class";

    /**
     * Skin read lock to avoid concurrent skin loading.
     * todo This should be removed in future and proper concurrent styles load should be available
     */
    private static final Object skinLock = new Object ();

    /**
     * Custom resource map used by StyleEditor to link resources and modified XML files.
     * In other circumstances this map shouldn't be required and will be empty.
     */
    private static final Map> resourceMap = new LinkedHashMap> ();

    /**
     * Skin includes identifier mark.
     * It identifies whether or not current skin is a simple include or a standalone skin.
     * These fields used for a dirty workaround, but it works and there is no better way to provide data into subsequent (included) skins.
     */
    private static boolean subsequentSkin = false;
    private static String skinClass = null;

    /**
     * Constructs SkinInfoConverter with the specified mapper and reflection provider.
     *
     * @param mapper             mapper
     * @param reflectionProvider reflection provider
     */
    public SkinInfoConverter ( final Mapper mapper, final ReflectionProvider reflectionProvider )
    {
        super ( mapper, reflectionProvider );
    }

    /**
     * Adds custom resource that will be used to change the default resources load strategy.
     * If specified skin XML files will be taken from this map instead of being read from the actual file.
     * This was generally added to support quick styles replacement by the {@link com.alee.extended.style.StyleEditor} application.
     *
     * @param nearClass class near which real XML is located
     * @param src       real XML location
     * @param xml       XML to use instead the real one
     */
    public static void addCustomResource ( final String nearClass, final String src, final String xml )
    {
        Map nearClassMap = resourceMap.get ( nearClass );
        if ( nearClassMap == null )
        {
            nearClassMap = new LinkedHashMap ();
            resourceMap.put ( nearClass, nearClassMap );
        }
        nearClassMap.put ( src, xml );
    }

    @Override
    public boolean canConvert ( final Class type )
    {
        return type.equals ( SkinInfo.class );
    }

    @Override
    public Object unmarshal ( final HierarchicalStreamReader reader, final UnmarshallingContext context )
    {
        synchronized ( skinLock )
        {
            // Previous skin class
            // It is also used to reset skin class to {@code null} value
            final String superSkinClass = skinClass;

            // Previous subsequent mark value
            final boolean wasSubsequent = subsequentSkin;

            // Have to perform read in try-catch to properly cleanup skin class
            // Cleanup will only be performed if the skin was read by this specific method call
            try
            {
                // Adding context value displaying whether or not this is a subsequent skin
                context.put ( SUBSEQUENT_SKIN, subsequentSkin );

                // Adding context value representing currently processed skin class
                context.put ( SKIN_CLASS, skinClass );

                // Creating component style
                final SkinInfo skinInfo = new SkinInfo ();
                final List styles = new ArrayList ();
                while ( reader.hasMoreChildren () )
                {
                    // Read next node
                    reader.moveDown ();
                    final String nodeName = reader.getNodeName ();
                    if ( nodeName.equals ( ID_NODE ) )
                    {
                        skinInfo.setId ( reader.getValue () );
                    }
                    else if ( nodeName.equals ( ICON_NODE ) )
                    {
                        // todo
                    }
                    else if ( nodeName.equals ( TITLE_NODE ) )
                    {
                        skinInfo.setTitle ( reader.getValue () );
                    }
                    else if ( nodeName.equals ( DESCRIPTION_NODE ) )
                    {
                        skinInfo.setDescription ( reader.getValue () );
                    }
                    else if ( nodeName.equals ( AUTHOR_NODE ) )
                    {
                        skinInfo.setAuthor ( reader.getValue () );
                    }
                    else if ( nodeName.equals ( SUPPORTED_SYSTEMS_NODE ) )
                    {
                        skinInfo.setSupportedSystems ( reader.getValue () );
                    }
                    else if ( nodeName.equals ( CLASS_NODE ) )
                    {
                        // Reading skin class canonical name
                        skinInfo.setSkinClass ( reader.getValue () );

                        // Adding skin into context, even if this is a subsequent skin
                        // Since it should be defined in the beginning of XML this value will be passed to underlying converters
                        // This way we can provide it into {@link com.alee.managers.style.data.ComponentStyleConverter}
                        skinClass = skinInfo.getSkinClass ();
                        context.put ( SKIN_CLASS, skinClass );
                    }
                    else if ( nodeName.equals ( STYLE_NODE ) )
                    {
                        // Reading component style
                        styles.add ( ( ComponentStyle ) context.convertAnother ( styles, ComponentStyle.class ) );
                    }
                    else if ( nodeName.equals ( INCLUDE_NODE ) )
                    {
                        // Reading included skin file styles
                        final String nearClass = reader.getAttribute ( NEAR_CLASS_ATTRIBUTE );
                        final String file = reader.getValue ();
                        final ResourceFile resourceFile = new ResourceFile ( ResourceLocation.nearClass, file, nearClass );
                        styles.addAll ( readInclude ( context, wasSubsequent, skinInfo, resourceFile ) );
                    }
                    reader.moveUp ();
                }

                // Saving all read styles into the skin
                // At this point there might be more than one style with the same ID
                skinInfo.setStyles ( styles );

                // Building final skin information if skin is not subsequent
                // This basically gathers all skins into a large cache and merges all settings appropriately
                if ( !subsequentSkin )
                {
                    // Creating cache map
                    final Map> stylesCache =
                            new LinkedHashMap> ( StyleableComponent.values ().length );

                    // Merged elements
                    performOverride ( styles );

                    // Building styles which extend some other styles
                    // We have to merge these manually once to create complete styles
                    buildStyles ( styles );

                    // Generating skin info cache
                    // Also merging all styles with the same ID
                    gatherStyles ( styles, stylesCache );

                    // Saving generated cache into skin
                    skinInfo.setStylesCache ( stylesCache );
                }

                return skinInfo;
            }
            finally
            {
                // Restoring previous skin class value
                // This will also reset the skin class back to {@code null} if this is main skin
                skinClass = superSkinClass;
                context.put ( SKIN_CLASS, skinClass );
            }
        }
    }

    /**
     * Reading and returning included skin file styles.
     *
     * @param context       unmarshalling context
     * @param wasSubsequent whether or not this skin was a subsequent one
     * @param skinInfo      sking information
     * @param resourceFile  included resourse file
     * @return included skin file styles
     */
    private List readInclude ( final UnmarshallingContext context, final boolean wasSubsequent, final SkinInfo skinInfo,
                                               final ResourceFile resourceFile )
    {
        // We have to perform includes read operation in try-catch to avoid misbehavior on next read
        // This is required to properly reset static "wasSubsequent" field back to {@code false} afterwards
        try
        {
            // Marking all further skins read as subsequent
            subsequentSkin = true;
            context.put ( SUBSEQUENT_SKIN, subsequentSkin );

            // Reading included file

            // Replacing null relative class with skin class
            if ( resourceFile.getClassName () == null )
            {
                final String skinClass = skinInfo.getSkinClass ();
                if ( skinClass == null )
                {
                    throw new StyleException ( "Included skin file \"" + resourceFile.getSource () +
                            "\" specified but skin \"" + CLASS_NODE + "\" is not set" );
                }
                resourceFile.setClassName ( skinClass );
            }

            // Reading skin part from included file
            final SkinInfo include = loadSkinInfo ( resourceFile );

            // Returning included styles
            return include.getStyles ();
        }
        finally
        {
            // Restoring include mark
            subsequentSkin = wasSubsequent;
            context.put ( SUBSEQUENT_SKIN, subsequentSkin );
        }
    }

    /**
     * Performs style override.
     *
     * @param styles styles to override
     */
    private void performOverride ( final List styles )
    {
        for ( int i = 0; i < styles.size (); i++ )
        {
            final ComponentStyle currentStyle = styles.get ( i );
            for ( int j = i + 1; j < styles.size (); j++ )
            {
                final ComponentStyle style = styles.get ( j );
                if ( style.getType () == currentStyle.getType () && CompareUtils.equals ( style.getId (), currentStyle.getId () ) )
                {
                    styles.set ( i, currentStyle.clone ().merge ( styles.remove ( j-- ) ) );
                }
            }
        }

        for ( int i = 0; i < styles.size (); i++ )
        {
            performOverride ( styles, styles, i );
        }
    }

    /**
     * Performs style override.
     *
     * @param globalStyles all available global styles
     * @param levelStyles  current level styles
     * @param index        index of style we are overriding on current level
     */
    private void performOverride ( final List globalStyles, final List levelStyles, final int index )
    {
        final ComponentStyle style = levelStyles.get ( index );

        // Overriding style children first
        if ( style.getStylesCount () > 0 )
        {
            for ( int i = 0; i < style.getStylesCount (); i++ )
            {
                performOverride ( globalStyles, style.getStyles (), i );
            }
        }

        // Trying to determine style we will extend
        final StyleableComponent type = style.getType ();
        final String completeId = style.getCompleteId ();
        final String defaultStyleId = type.getDefaultStyleId ().getCompleteId ();
        ComponentStyle extendedStyle = null;

        // Searching for extended style
        // This can be a style explicitely specified in style XML as extended one or default one
        if ( !TextUtils.isEmpty ( style.getExtendsId () ) )
        {
            // Style cannot extend itself
            final String extendsId = style.getExtendsId ();
            if ( extendsId.equals ( completeId ) )
            {
                final String msg = "Component style '%s:%s' extends itself";
                throw new StyleException ( String.format ( msg, type, completeId ) );
            }

            // Extended style must exist in loaded skin
            extendedStyle = findStyle ( type, extendsId, style.getId (), levelStyles, globalStyles, index );
            if ( extendedStyle == null )
            {
                final String msg = "Component style '%s:%s' missing style '%s'";
                throw new StyleException ( String.format ( msg, type, completeId, extendsId ) );
            }
        }

        // Searching for overriden style
        // This allows us to provide default or existing styles overrides
        if ( extendedStyle == null )
        {
            // Retrieving possible style with the same ID
            // In case we find one we will use it as an extended style
            extendedStyle = findOverrideStyle ( globalStyles, style );
        }

        // Searching for default style
        // This is made to provide all initial settings properly without leaving any of those empty
        if ( extendedStyle == null && !CompareUtils.equals ( completeId, defaultStyleId ) )
        {
            // Default style must exist in loaded skin
            // Any non-default style extends default one by default even if it is not specified
            extendedStyle = findStyle ( type, defaultStyleId, style.getId (), levelStyles, globalStyles, index );
            if ( extendedStyle == null )
            {
                final String msg = "Component style '%s:%s' missing default style '%s'";
                throw new StyleException ( String.format ( msg, type, completeId, defaultStyleId ) );
            }
        }

        // Processing extended style
        // This will be either extended style, overriden style or default style
        // It might also receive {@code null} in case we are working with default style itself
        if ( extendedStyle != null )
        {
            // Creating a clone of extended style and merging it with current style
            // Result of the merge is stored within the styles list on the current level
            levelStyles.set ( index, extendedStyle.clone ().merge ( style ) );
        }
    }

    /**
     * Returns overriden style if one exists.
     *
     * @param globalStyles all available global styles
     * @param style        style to look overriden one for
     * @return overriden style if one exists
     */
    private ComponentStyle findOverrideStyle ( final List globalStyles, final ComponentStyle style )
    {
        final List componentStyles = new ArrayList ();
        componentStyles.add ( style );
        while ( componentStyles.get ( 0 ).getParent () != null )
        {
            componentStyles.add ( 0, componentStyles.get ( 0 ).getParent () );
        }

        ComponentStyle oldStyle = null;
        while ( !componentStyles.isEmpty () )
        {
            final ComponentStyle currentStyle = componentStyles.remove ( 0 );
            final List styles = oldStyle == null ? globalStyles : oldStyle.getStyles ();
            final int maxIndex = oldStyle == null ? globalStyles.indexOf ( currentStyle ) : Integer.MAX_VALUE;
            if ( ( oldStyle = findStyle ( currentStyle.getType (), currentStyle.getId (), styles, maxIndex ) ) == null &&
                    ( oldStyle = findStyle ( currentStyle.getType (), currentStyle.getExtendsId (), styles, maxIndex ) ) == null &&
                    ( oldStyle = findStyle ( currentStyle.getType (), currentStyle.getType ().toString (), styles, maxIndex ) ) == null )
            {
                break;
            }
        }

        return oldStyle;
    }

    /**
     * Gathers styles into styles cache map.
     *
     * @param styles      styles available on this level
     * @param stylesCache styles cache map
     */
    private void gatherStyles ( final List styles, final Map> stylesCache )
    {
        if ( styles != null )
        {
            for ( final ComponentStyle style : styles )
            {
                // Retrieving styles map for this component type
                final StyleableComponent type = style.getType ();
                Map componentStyles = stylesCache.get ( type );
                if ( componentStyles == null )
                {
                    componentStyles = new LinkedHashMap ( 1 );
                    stylesCache.put ( type, componentStyles );
                }

                // Adding this style into cache
                componentStyles.put ( style.getCompleteId (), style );

                // Adding child styles into cache
                gatherStyles ( style.getStyles (), stylesCache );
            }
        }
    }

    /**
     * Builds specified styles.
     * This will resolve all style dependencies and overrides.
     *
     * @param styles styles to build
     */
    private void buildStyles ( final List styles )
    {
        // Creating built styles IDs map
        final Map> builtStyles = new HashMap> ();
        for ( final StyleableComponent type : StyleableComponent.values () )
        {
            builtStyles.put ( type, new ArrayList ( 1 ) );
        }

        // Special list that will keep only styles which are being built
        final List building = new ArrayList ();

        // Building provided styles into a new list
        for ( int i = 0; i < styles.size (); i++ )
        {
            buildStyle ( styles, i, building, builtStyles );
        }
    }

    /**
     * Builds style at the specified index on the level.
     * This will resolve all dependencies and overrides for the specified style.
     *
     * @param levelStyles all available level styles
     * @param index       index of style we are building on current level
     * @param building    styles which are currently being built, used to determine cyclic references
     * @param builtStyles IDs of styles which were already built
     * @return build style
     */
    private ComponentStyle buildStyle ( final List levelStyles, final int index, final List building,
                                        final Map> builtStyles )
    {
        final ComponentStyle style = levelStyles.get ( index );

        final StyleableComponent type = style.getType ();
        final String completeId = style.getCompleteId ();
        final String uniqueId = type + ":" + completeId;

        // Avoiding cyclic references
        if ( building.contains ( uniqueId ) )
        {
            throw new StyleException ( "Style " + uniqueId + " is used within cyclic references" );
        }

        // Check whether this style was already built
        if ( builtStyles.get ( type ).contains ( completeId ) )
        {
            return style;
        }

        // Adding this style into list of styles we are building right now
        building.add ( uniqueId );

        // Resolving nested styles first
        if ( style.getStylesCount () > 0 )
        {
            for ( int i = 0; i < style.getStylesCount (); i++ )
            {
                buildStyle ( style.getStyles (), i, building, builtStyles );
            }
        }

        // Adding this styles into built list
        builtStyles.get ( type ).add ( completeId );

        // Removing this style from building list upon completion
        building.remove ( uniqueId );

        // Return completed style
        return style;
    }

    /**
     * Returns component style found either on local or global level.
     *
     * @param type        component type
     * @param id          ID of the style to find
     * @param excludeId   style ID that should be excluded on the current level
     * @param levelStyles current level styles
     * @param styles      global styles
     * @param maxIndex    max style index
     * @return component style found either on local or global level
     */
    private ComponentStyle findStyle ( final StyleableComponent type, final String id, final String excludeId,
                                       final List levelStyles, final List styles, final int maxIndex )
    {
        // todo Probably look on some other levels later on?
        if ( levelStyles != null && levelStyles != styles )
        {
            final ComponentStyle style = findStyle ( type, id, levelStyles, maxIndex );
            if ( style != null && !CompareUtils.equals ( style.getId (), excludeId ) )
            {
                return style;
            }
        }
        return findStyle ( type, id, styles, Integer.MAX_VALUE );
    }

    /**
     * Returns component style found in the specified styles list.
     * This method doesn't perform nested styles search for reason.
     *
     * @param type     component type
     * @param id       ID of the style to find
     * @param styles   styles list
     * @param maxIndex max style index
     * @return component style found in the specified styles list
     */
    private ComponentStyle findStyle ( final StyleableComponent type, final String id, final List styles,
                                       final int maxIndex )
    {
        ComponentStyle fstyle = null;
        for ( int i = 0; i < styles.size () && i < maxIndex; i++ )
        {
            final ComponentStyle style = styles.get ( i );
            if ( style.getType () == type && CompareUtils.equals ( style.getId (), id ) )
            {
                fstyle = style;
            }
        }
        return fstyle;
    }

    /**
     * Loads SkinInfo from the specified resource file.
     * It will use an XML from a predefined resources map if it exists there.
     *
     * @param resourceFile XML resource file
     * @return loaded SkinInfo
     */
    protected SkinInfo loadSkinInfo ( final ResourceFile resourceFile )
    {
        try
        {
            final Map nearClassMap = resourceMap.get ( resourceFile.getClassName () );
            if ( nearClassMap != null )
            {
                final String xml = nearClassMap.get ( resourceFile.getSource () );
                if ( xml != null )
                {
                    return XmlUtils.fromXML ( xml );
                }
                else
                {
                    return XmlUtils.fromXML ( resourceFile, false );
                }
            }
            else
            {
                return XmlUtils.fromXML ( resourceFile, false );
            }
        }
        catch ( final Throwable e )
        {
            throw new StyleException ( "Included skin file \"" + resourceFile.getSource () + "\" cannot be read", e );
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy