SmartSpritesParameters.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 java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;

import org.carrot2.labs.smartsprites.message.Message.MessageLevel;
import org.carrot2.labs.smartsprites.message.Message.MessageType;
import org.carrot2.labs.smartsprites.message.MessageLog;
import org.carrot2.util.FileUtils;
import org.carrot2.util.StringUtils;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;

/**
 * Contains invocation parameters for SmartSprites, provides methods for validating the parameters.
 */
public final class SmartSpritesParameters {
    /**
     * Path to the directory that contains the css files to be processed. Directories containing CSS and image files
     * must be writable, unless output.dir.path is provided. The root.dir.path can be either absolute, e.g.
     * c:/myproject/web or relative to the directory in which this script is run.
     */
    @Option(name = "--root-dir-path", required = false, metaVar = "DIR")
    private String rootDir;

    /**
     * Paths to individual CSS files to process. Either {@link #rootDir} or {@link #cssFiles} must be not empty. If only
     * {@link #cssFiles} is not empty, {@link #outputDir} must be blank. If both {@link #rootDir} and {@link #cssFiles}
     * are not empty, {@link #outputDir} is supported but only {@link #cssFiles} from {@link #rootDir} are processed.
     */
    @Argument(metaVar = "CSS-FILES")
    @Option(name = "--css-files", required = false, metaVar = "FILES")
    private List<String> cssFiles;

    /**
     * Output directory for processed CSS files and CSS-relative sprite images. The directory structure relative to
     * root.dir.path will be preserved in the output directory. E.g. if CSS files are contained in the css/base
     * directory of root.dir.path, the processed results will be written to output.dir.path/css/base. Also, CSS-relative
     * sprite images will be written to the output directory. Sprite images with document-root-relative URLs will be
     * written relative to the document.root.dir.path.
     * <p>
     * If the output.dir.path directory does not exist, it will be created.
     * <p>
     * You can leave this property empty, in which case the CSS files will be written next to the original CSS files
     * with css.file.suffix, and sprite images will be written relative to CSS files.
     * <p>
     * If you are using a non-empty output.dir.path, you might want to use an empty css.file.suffix.
     */
    @Option(name = "--output-dir-path", metaVar = "DIR")
    private String outputDir;

    /**
     * Document root path for document-root-relative (starting with '/') image urls in CSS. All such image URLs will be
     * taken relative to document.root.dir.path. Also document-root-relative sprite URLs will be written relative to
     * document.root.dir.path. You can leave this property empty if your CSS uses only CSS-relative image URLs. *
     */
    @Option(name = "--document-root-dir-path", metaVar = "DIR")
    private String documentRootDir;

    /**
     * Message logging level. If you're getting lots of INFO messages and want to see only warnings, set this option to
     * WARN.
     */
    @Option(name = "--log-level")
    private MessageLevel logLevel;

    /**
     * The encoding to assume for input and output CSS files.
     */
    @Option(name = "--css-file-encoding")
    private String cssFileEncoding;

    /**
     * Suffix to be appended to the processed CSS file name.
     */
    @Option(name = "--css-file-suffix")
    private String cssFileSuffix;

    /**
     * The required depth of the generated PNG sprites.
     */
    @Option(name = "--sprite-png-depth")
    private PngDepth spritePngDepth;

    /**
     * If <code>true</code>, SmartSprites will generate the sprite directive indicating that the image is a sprite
     * image.
     */
    @Option(name = "--mark-sprite-images")
    private boolean markSpriteImages;

    /** The default suffix to be added to the generated CSS files. */
    public static final String DEFAULT_CSS_FILE_SUFFIX = "-sprite";

    /** By default, we use full color only when necessary. */
    public static final PngDepth DEFAULT_SPRITE_PNG_DEPTH = PngDepth.AUTO;

    /** By default, we'll assume CSS files are UTF-8 encoded. */
    public static final String DEFAULT_CSS_FILE_ENCODING = StandardCharsets.UTF_8.name();

    /** The default logging level. */
    public static final MessageLevel DEFAULT_LOGGING_LEVEL = MessageLevel.INFO;

    /** By default, we don't generate sprite directive in output css. */
    public static final boolean DEFAULT_MARK_SPRITE_IMAGES = false;

    /**
     * The Enum PngDepth.
     */
    public enum PngDepth {

        /** The auto. */
        AUTO,
        /** The indexed. */
        INDEXED,
        /** The direct. */
        DIRECT;
    }

    /**
     * Creates the parameters with default options and null root dir, before root dir is set, the parameters are
     * invalid.
     */
    public SmartSpritesParameters() {
        this(null);
    }

    /**
     * Creates the parameters with most default values.
     *
     * @param rootDir
     *            the root dir
     */
    public SmartSpritesParameters(String rootDir) {
        this(rootDir, null, null, null, MessageLevel.INFO, DEFAULT_CSS_FILE_SUFFIX, DEFAULT_SPRITE_PNG_DEPTH,
                DEFAULT_CSS_FILE_ENCODING, DEFAULT_MARK_SPRITE_IMAGES);
    }

    /**
     * Creates the parameters.
     *
     * @param rootDir
     *            the root dir
     * @param cssFiles
     *            the css files
     * @param outputDir
     *            the output dir
     * @param documentRootDir
     *            the document root dir
     * @param logLevel
     *            the log level
     * @param cssFileSuffix
     *            the css file suffix
     * @param spritePngDepth
     *            the sprite png depth
     * @param cssEncoding
     *            the css encoding
     */
    public SmartSpritesParameters(String rootDir, List<String> cssFiles, String outputDir, String documentRootDir,
            MessageLevel logLevel, String cssFileSuffix, PngDepth spritePngDepth, String cssEncoding) {
        this(rootDir, cssFiles, outputDir, documentRootDir, logLevel, cssFileSuffix, spritePngDepth, cssEncoding,
                DEFAULT_MARK_SPRITE_IMAGES);
    }

    /**
     * Creates the parameters.
     *
     * @param rootDir
     *            the root dir
     * @param cssFiles
     *            the css files
     * @param outputDir
     *            the output dir
     * @param documentRootDir
     *            the document root dir
     * @param logLevel
     *            the log level
     * @param cssFileSuffix
     *            the css file suffix
     * @param spritePngDepth
     *            the sprite png depth
     * @param cssEncoding
     *            the css encoding
     * @param markSpriteImages
     *            the mark sprite images
     */
    public SmartSpritesParameters(String rootDir, List<String> cssFiles, String outputDir, String documentRootDir,
            MessageLevel logLevel, String cssFileSuffix, PngDepth spritePngDepth, String cssEncoding,
            boolean markSpriteImages) {
        this.rootDir = rootDir;
        this.cssFiles = cssFiles;
        this.outputDir = outputDir;
        this.documentRootDir = documentRootDir;
        this.logLevel = logLevel;
        this.cssFileEncoding = cssEncoding;
        this.cssFileSuffix = getCssFileSuffix(cssFileSuffix);
        this.spritePngDepth = spritePngDepth;
        this.markSpriteImages = markSpriteImages;
    }

    /**
     * Validates the provided parameters. All resource paths are resolved against the local file system.
     *
     * @param log
     *            the log
     *
     * @return <code>true</code> if the parameters are valid
     */
    public boolean validate(MessageLog log) {
        boolean valid = true;

        // Either root dir or css files are required
        if (!hasRootDir() && !hasCssFiles()) {
            log.error(MessageType.EITHER_ROOT_DIR_OR_CSS_FILES_IS_REQUIRED);
            return false;
        }

        // If there is no output dir, we can't have both root dir or css files
        if (!hasOutputDir() && hasRootDir() && hasCssFiles()) {
            log.error(MessageType.ROOT_DIR_AND_CSS_FILES_CANNOT_BE_BOTH_SPECIFIED_UNLESS_WITH_OUTPUT_DIR);
            return false;
        }

        // Check root dir if provided
        if (hasRootDir()) {
            final File rootDir = FileUtils.getCanonicalOrAbsoluteFile(this.rootDir);
            if (!rootDir.exists() || !rootDir.isDirectory()) {
                log.error(MessageType.ROOT_DIR_DOES_NOT_EXIST_OR_IS_NOT_DIRECTORY, this.rootDir);
                valid = false;
            }
        }

        // Check output dir if provided
        if (hasOutputDir()) {
            // For output dir, we need root dir
            if (!hasRootDir()) {
                log.error(MessageType.ROOT_DIR_IS_REQUIRED_FOR_OUTPUT_DIR);
                return false;
            }

            final File outputDir = FileUtils.getCanonicalOrAbsoluteFile(this.outputDir);
            if (outputDir.exists() && !outputDir.isDirectory()) {
                log.error(MessageType.OUTPUT_DIR_IS_NOT_DIRECTORY, this.outputDir);
                valid = false;
            }
        }

        if (!hasOutputDir() && StringUtils.isBlank(cssFileSuffix)) {
            log.error(MessageType.CSS_FILE_SUFFIX_IS_REQUIRED_IF_NO_OUTPUT_DIR);
            valid = false;
        }

        if (hasDocumentRootDir()) {
            final File documentRootDir = FileUtils.getCanonicalOrAbsoluteFile(this.documentRootDir);
            if (!documentRootDir.exists() || !documentRootDir.isDirectory()) {
                log.error(MessageType.DOCUMENT_ROOT_DIR_DOES_NOT_EXIST_OR_IS_NOT_DIRECTORY, this.documentRootDir);
                valid = false;
            }
        }

        return valid;
    }

    /**
     * Gets the css file suffix.
     *
     * @param suffix
     *            the suffix
     *
     * @return the css file suffix
     */
    private String getCssFileSuffix(String suffix) {
        if (suffix != null) {
            return suffix;
        }
        if (!hasOutputDir()) {
            // If there is no output dir, we must have some suffix
            return DEFAULT_CSS_FILE_SUFFIX;
        }
        // If we have an output dir, we can have an empty suffix
        return "";
    }

    /**
     * Gets the root dir.
     *
     * @return the root dir
     */
    public String getRootDir() {
        return rootDir;
    }

    /**
     * Gets the root dir file.
     *
     * @return the root dir file
     *
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    public File getRootDirFile() throws IOException {
        return rootDir.startsWith("..") ? new File(rootDir).getCanonicalFile() : new File(rootDir);
    }

    /**
     * Checks for root dir.
     *
     * @return true, if successful
     */
    public boolean hasRootDir() {
        return StringUtils.isNotBlank(rootDir);
    }

    /**
     * Gets the css files.
     *
     * @return the css files
     */
    public List<String> getCssFiles() {
        return cssFiles;
    }

    /**
     * Checks for css files.
     *
     * @return true, if successful
     */
    public boolean hasCssFiles() {
        return cssFiles != null && !cssFiles.isEmpty();
    }

    /**
     * Gets the output dir.
     *
     * @return the output dir
     */
    public String getOutputDir() {
        return outputDir;
    }

    /**
     * Checks for output dir.
     *
     * @return true, if successful
     */
    public boolean hasOutputDir() {
        return StringUtils.isNotBlank(outputDir);
    }

    /**
     * Gets the document root dir.
     *
     * @return the document root dir
     */
    public String getDocumentRootDir() {
        return documentRootDir;
    }

    /**
     * Checks for document root dir.
     *
     * @return true, if successful
     */
    public boolean hasDocumentRootDir() {
        return StringUtils.isNotBlank(documentRootDir);
    }

    /**
     * Gets the log level.
     *
     * @return the log level
     */
    public MessageLevel getLogLevel() {
        return logLevel;
    }

    /**
     * Gets the css file suffix.
     *
     * @return the css file suffix
     */
    public String getCssFileSuffix() {
        return cssFileSuffix;
    }

    /**
     * Gets the sprite png depth.
     *
     * @return the sprite png depth
     */
    public PngDepth getSpritePngDepth() {
        return spritePngDepth;
    }

    /**
     * Checks if is mark sprite images.
     *
     * @return true, if is mark sprite images
     */
    public boolean isMarkSpriteImages() {
        return markSpriteImages;
    }

    /**
     * Gets the css file encoding.
     *
     * @return the css file encoding
     */
    public String getCssFileEncoding() {
        return cssFileEncoding;
    }
}