SpriteImageDirective.java

/*
 * SmartSprites Project
 *
 * Copyright (C) 2007-2009, Stanisław Osiński.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * - Redistributions of  source code must  retain the above  copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice, this
 *   list of conditions and the following  disclaimer in  the documentation  and/or
 *   other materials provided with the distribution.
 *
 * - Neither the name of the SmartSprites Project nor the names of its contributors
 *   may  be used  to endorse  or  promote  products derived   from  this  software
 *   without specific prior written permission.
 *
 * - We kindly request that you include in the end-user documentation provided with
 *   the redistribution and/or in the software itself an acknowledgement equivalent
 *   to  the  following: "This product includes software developed by the SmartSprites
 *   Project."
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"  AND
 * ANY EXPRESS OR  IMPLIED WARRANTIES, INCLUDING,  BUT NOT LIMITED  TO, THE IMPLIED
 * WARRANTIES  OF  MERCHANTABILITY  AND  FITNESS  FOR  A  PARTICULAR  PURPOSE   ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE  FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL,  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL  DAMAGES
 * (INCLUDING, BUT  NOT LIMITED  TO, PROCUREMENT  OF SUBSTITUTE  GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS;  OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND  ON
 * ANY  THEORY  OF  LIABILITY,  WHETHER  IN  CONTRACT,  STRICT  LIABILITY,  OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE)  ARISING IN ANY WAY  OUT OF THE USE  OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.carrot2.labs.smartsprites;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import java.awt.Color;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.carrot2.labs.smartsprites.css.CssProperty;
import org.carrot2.labs.smartsprites.css.CssSyntaxUtils;
import org.carrot2.labs.smartsprites.message.Message.MessageType;
import org.carrot2.labs.smartsprites.message.MessageLog;
import org.carrot2.util.CollectionUtils;
import org.carrot2.util.StringUtils;

/**
 * Represents a directive that declares an individual sprite image.
 */
public class SpriteImageDirective {

    /** The Constant PROPERTY_SPRITE_ID. */
    public static final String PROPERTY_SPRITE_ID = "sprite";

    /** The Constant PROPERTY_SPRITE_IMAGE_LAYOUT. */
    public static final String PROPERTY_SPRITE_IMAGE_LAYOUT = "sprite-layout";

    /** The Constant PROPERTY_SPRITE_IMAGE_URL. */
    public static final String PROPERTY_SPRITE_IMAGE_URL = "sprite-image";

    /** The Constant PROPERTY_SPRITE_IMAGE_UID_SUFFIX. */
    public static final String PROPERTY_SPRITE_IMAGE_UID_SUFFIX = "sprite-image-uid";

    /** The Constant PROPERTY_SPRITE_MATTE_COLOR. */
    public static final String PROPERTY_SPRITE_MATTE_COLOR = "sprite-matte-color";

    /** The Constant PROPERTY_SPRITE_SCALE. */
    public static final String PROPERTY_SPRITE_SCALE = "sprite-scale";

    /** A set of allowed properties. */
    private static final Set<String> ALLOWED_PROPERTIES = ImmutableSet.of(PROPERTY_SPRITE_ID,
            PROPERTY_SPRITE_IMAGE_LAYOUT, PROPERTY_SPRITE_IMAGE_URL, PROPERTY_SPRITE_MATTE_COLOR, PROPERTY_SPRITE_SCALE,
            PROPERTY_SPRITE_IMAGE_UID_SUFFIX);

    /**
     * Defines the layout of this sprite.
     */
    public enum SpriteImageLayout {
        /**
         * Vertical layout, images stacked on each other.
         */
        VERTICAL,

        /**
         * Horizontal layout, images next to each other.
         */
        HORIZONTAL;

        /** The value. */
        private final String value;

        /**
         * Instantiates a new sprite image layout.
         */
        SpriteImageLayout() {
            this.value = name().toLowerCase(Locale.ENGLISH);
        }

        @Override
        public String toString() {
            return value;
        }

        /**
         * Values as string.
         *
         * @return the string
         */
        public static String valuesAsString() {
            final String list = Lists.newArrayList(values()).toString();
            return list.substring(1, list.length() - 1);
        }
    }

    /**
     * Defines the UID Generation Mode of this sprite.
     */
    public enum SpriteUidType {
        /**
         * No UID extension.
         */
        NONE,

        /**
         * Append current timestamp as long.
         */
        DATE,

        /**
         * Append sha512 of the sprites file.
         */
        SHA512;

        /** The value. */
        private final String value;

        /** The pattern. */
        public final Pattern pattern;

        /**
         * Instantiates a new sprite uid type.
         */
        SpriteUidType() {
            this.value = name().toLowerCase(Locale.ENGLISH);
            this.pattern = Pattern.compile("${" + value + "}", Pattern.LITERAL);
        }

        @Override
        public String toString() {
            return value;
        }

        /**
         * Values as string.
         *
         * @return the string
         */
        public static String valuesAsString() {
            final String list = Lists.newArrayList(values()).toString();
            return list.substring(1, list.length() - 1);
        }
    }

    /**
     * Defines supported image file formats.
     */
    public enum SpriteImageFormat {

        /** The png. */
        PNG,
        /** The gif. */
        GIF,
        /** The jpg. */
        JPG;

        /** The value. */
        private final String value;

        /**
         * Instantiates a new sprite image format.
         */
        SpriteImageFormat() {
            this.value = name().toLowerCase(Locale.ENGLISH);
        }

        @Override
        public String toString() {
            return value;
        }

        /**
         * Gets the value.
         *
         * @param value
         *            the value
         *
         * @return the value
         */
        public static SpriteImageFormat getValue(String value) {
            return valueOf(value.toUpperCase(Locale.ENGLISH));
        }

        /**
         * Values as string.
         *
         * @return the string
         */
        public static String valuesAsString() {
            final String list = Lists.newArrayList(values()).toString();
            return list.substring(1, list.length() - 1);
        }
    }

    /**
     * Unique identified of this sprite.
     */
    public final String spriteId;

    /**
     * CSS file relative path for this sprite image.
     */
    public final String imagePath;

    /**
     * Non-file-name extension after the sprite image path to force a cache update on change, prefixed by '?'.
     */
    public final SpriteUidType uidType;

    /**
     * Layout of this sprite image.
     */
    public final SpriteImageLayout layout;

    /**
     * Format of this sprite image.
     */
    public final SpriteImageFormat format;

    /**
     * Matte color to be used when reducing true alpha channel.
     */
    public final Color matteColor;

    /**
     * Scaling ratio to apply to background; default is 1.
     */
    public final float scaleRatio;

    /**
     * Sprite layout properties defined at the sprite image directive level. The defaults provided here can be
     * overridden at the sprite reference directive level.
     */
    public final SpriteLayoutProperties spriteLayoutProperties;

    /**
     * Pattern for a simple syntactic check of the image path.
     */
    private static final Pattern IMAGE_PATH_PATTERN = Pattern.compile("([^${}]*|\\$\\{[^}]*\\})*");

    /**
     * Pattern for extracting variables from image path.
     */
    private static final Pattern IMAGE_PATH_VARIABLE_PATTERN = Pattern.compile("\\$\\{([a-z]*)\\}");

    /**
     * Variable names allowed in image path.
     */
    private static final Set<String> ALLOWED_VARIABLES = ImmutableSet.of(PROPERTY_SPRITE_ID,
            SpriteUidType.DATE.toString(), SpriteUidType.SHA512.toString());

    /**
     * Instantiates a new sprite image directive.
     *
     * @param id
     *            the id
     * @param imageUrl
     *            the image url
     * @param layout
     *            the layout
     * @param format
     *            the format
     * @param matteColor
     *            the matte color
     * @param uidType
     *            the uid type
     * @param scale
     *            the scale
     */
    public SpriteImageDirective(String id, String imageUrl, SpriteImageLayout layout, SpriteImageFormat format,
            Color matteColor, SpriteUidType uidType, float scale) {
        this(id, imageUrl, layout, format, matteColor, uidType, scale, new SpriteLayoutProperties(layout));
    }

    /**
     * Instantiates a new sprite image directive.
     *
     * @param id
     *            the id
     * @param imageUrl
     *            the image url
     * @param layout
     *            the layout
     * @param format
     *            the format
     * @param matteColor
     *            the matte color
     * @param uidType
     *            the uid type
     * @param scale
     *            the scale
     * @param spriteLayoutProperties
     *            the sprite layout properties
     */
    public SpriteImageDirective(String id, String imageUrl, SpriteImageLayout layout, SpriteImageFormat format,
            Color matteColor, SpriteUidType uidType, float scale, SpriteLayoutProperties spriteLayoutProperties) {
        this.spriteId = id;
        this.imagePath = imageUrl;
        this.layout = layout;
        this.format = format;
        this.matteColor = matteColor;
        this.uidType = uidType;
        this.scaleRatio = scale;
        this.spriteLayoutProperties = spriteLayoutProperties;
    }

    /**
     * Parses a string into a {@link SpriteImageDirective}, logging messages to the provided {@link MessageLog}s.
     *
     * @param directiveString
     *            the directive string
     * @param messageCollector
     *            the message collector
     *
     * @return the sprite image directive
     */
    public static SpriteImageDirective parse(String directiveString, MessageLog messageCollector) {
        final Map<String, CssProperty> rules = CssSyntaxUtils
                .propertiesAsMap(CssSyntaxUtils.extractRules(directiveString, messageCollector));

        final Set<String> properties = Sets.newLinkedHashSet(rules.keySet());
        properties.removeAll(ALLOWED_PROPERTIES);
        properties.removeAll(SpriteLayoutProperties.ALLOWED_PROPERTIES);
        if (!properties.isEmpty()) {
            messageCollector.warning(MessageType.UNSUPPORTED_PROPERTIES_FOUND, CollectionUtils.toString(properties));
        }

        if (!CssSyntaxUtils.hasNonBlankValue(rules, PROPERTY_SPRITE_ID)) {
            messageCollector.warning(MessageType.SPRITE_ID_NOT_FOUND);
            return null;
        }

        if (!CssSyntaxUtils.hasNonBlankValue(rules, PROPERTY_SPRITE_IMAGE_URL)) {
            messageCollector.warning(MessageType.SPRITE_IMAGE_URL_NOT_FOUND);
            return null;
        }

        final String id = rules.get(PROPERTY_SPRITE_ID).value;

        final SpriteUidType uidGenerator = valueOf(CssSyntaxUtils.getValue(rules, PROPERTY_SPRITE_IMAGE_UID_SUFFIX),
                SpriteUidType.class, SpriteUidType.NONE, messageCollector, MessageType.UNSUPPORTED_UID_TYPE);
        if (uidGenerator != SpriteUidType.NONE) {
            messageCollector.deprecation(MessageType.DEPRECATED_SPRITE_IMAGE_UID, uidGenerator.toString());
        }

        // Image path. If the path does not match a regular expression, issue a warning.
        final String imagePath = CssSyntaxUtils.unpackUrl(rules.get(PROPERTY_SPRITE_IMAGE_URL).value);
        if (IMAGE_PATH_PATTERN.matcher(imagePath).matches()) {
            // Check variable names
            final Matcher variableMatcher = IMAGE_PATH_VARIABLE_PATTERN.matcher(imagePath);
            while (variableMatcher.find()) {
                if (variableMatcher.groupCount() == 1 && !ALLOWED_VARIABLES.contains(variableMatcher.group(1))) {
                    messageCollector.warning(MessageType.UNSUPPORTED_VARIABLE_IN_SPRITE_IMAGE_PATH,
                            variableMatcher.group(1));
                }
            }
        } else {
            // Just issue a warning
            messageCollector.warning(MessageType.MALFORMED_SPRITE_IMAGE_PATH, imagePath);
        }

        // Layout is optional
        final SpriteImageLayout layout = valueOf(CssSyntaxUtils.getValue(rules, PROPERTY_SPRITE_IMAGE_LAYOUT),
                SpriteImageLayout.class, SpriteImageLayout.VERTICAL, messageCollector, MessageType.UNSUPPORTED_LAYOUT);

        // Infer format from image path
        SpriteImageFormat format;
        final int lastDotIndex = imagePath.lastIndexOf('.');
        if (lastDotIndex < 0 || lastDotIndex == imagePath.length() - 1) {
            messageCollector.warning(MessageType.CANNOT_DETERMINE_IMAGE_FORMAT, imagePath);
            format = SpriteImageFormat.PNG;
        } else {
            final int questionMarkIndex = imagePath.indexOf('?', lastDotIndex);
            final String formatValue = questionMarkIndex >= 0 ? imagePath.substring(lastDotIndex + 1, questionMarkIndex)
                    : imagePath.substring(lastDotIndex + 1);
            try {
                format = SpriteImageFormat.getValue(formatValue);
            } catch (final IllegalArgumentException e) {
                messageCollector.warning(MessageType.UNSUPPORTED_SPRITE_IMAGE_FORMAT, formatValue);
                format = SpriteImageFormat.PNG;
            }
        }

        // Matte color
        final Color matteColor;
        if (CssSyntaxUtils.hasNonBlankValue(rules, PROPERTY_SPRITE_MATTE_COLOR)) {
            matteColor = CssSyntaxUtils.parseColor(rules.get(PROPERTY_SPRITE_MATTE_COLOR).value, messageCollector,
                    null);
        } else {
            matteColor = null;
        }

        final float scale;
        if (CssSyntaxUtils.hasNonBlankValue(rules, PROPERTY_SPRITE_SCALE)) {
            scale = Float.parseFloat(rules.get(PROPERTY_SPRITE_SCALE).value);
        } else {
            scale = 1.0f;
        }

        return new SpriteImageDirective(id, imagePath, layout, format, matteColor, uidGenerator, scale,
                SpriteLayoutProperties.parse(directiveString, layout, messageCollector));
    }

    /**
     * Value of.
     *
     * @param <T>
     *            the generic type
     * @param stringValue
     *            the string value
     * @param enumClass
     *            the enum class
     * @param defaultValue
     *            the default value
     * @param messageCollector
     *            the message collector
     * @param messageType
     *            the message type
     *
     * @return the t
     */
    private static <T extends Enum<T>> T valueOf(String stringValue, Class<T> enumClass, T defaultValue,
            MessageLog messageCollector, MessageType messageType) {
        if (!StringUtils.isNotBlank(stringValue)) {
            return defaultValue;
        }
        try {
            return Enum.valueOf(enumClass, stringValue.toUpperCase(Locale.ENGLISH));
        } catch (IllegalArgumentException e) {
            messageCollector.warning(messageType, stringValue);
            return defaultValue;
        }
    }
}