Package org.carrot2.labs.smartsprites

Source Code of org.carrot2.labs.smartsprites.SpriteBuilder

package org.carrot2.labs.smartsprites;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.carrot2.labs.smartsprites.message.LevelCounterMessageSink;
import org.carrot2.labs.smartsprites.message.Message.MessageType;
import org.carrot2.labs.smartsprites.message.MessageLog;
import org.carrot2.labs.smartsprites.resource.FileSystemResourceHandler;
import org.carrot2.labs.smartsprites.resource.ResourceHandler;
import org.carrot2.util.FileUtils;
import org.carrot2.util.PathUtils;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.io.Closeables;

/**
* Performs all stages of sprite building. This class is not thread-safe.
*/
public class SpriteBuilder
{
    /** Properties we need to watch for in terms of overriding the generated ones. */
    private static final HashSet<String> OVERRIDING_PROPERTIES = Sets.newHashSet(
        "background-position", "background-image");

    /** This builder's configuration */
    public final SmartSpritesParameters parameters;

    /** This builder's message log */
    private final MessageLog messageLog;

    /** Directive occurrence collector for this builder */
    private final SpriteDirectiveOccurrenceCollector spriteDirectiveOccurrenceCollector;

    /** SpriteImageBuilder for this builder */
    private final SpriteImageBuilder spriteImageBuilder;

    /** Resource handler */
    private ResourceHandler resourceHandler;

    /**
     * Creates a {@link SpriteBuilder} with the provided parameters and log.
     */
    public SpriteBuilder(SmartSpritesParameters parameters, MessageLog messageLog)
    {
        this(parameters, messageLog, new FileSystemResourceHandler(
            parameters.getDocumentRootDir(), parameters.getCssFileEncoding(), messageLog));
    }

    /**
     * Creates a {@link SpriteBuilder} with the provided parameters and log.
     */
    public SpriteBuilder(SmartSpritesParameters parameters, MessageLog messageLog,
        ResourceHandler resourceHandler)
    {
        this.messageLog = messageLog;
        this.parameters = parameters;
        this.resourceHandler = resourceHandler;
        spriteDirectiveOccurrenceCollector = new SpriteDirectiveOccurrenceCollector(
            messageLog, resourceHandler);
        spriteImageBuilder = new SpriteImageBuilder(parameters, messageLog,
            resourceHandler);
    }

    /**
     * Performs processing for this builder's parameters. This method resolves all paths
     * against the local file system.
     */
    public void buildSprites() throws FileNotFoundException, IOException
    {
        if (!parameters.validate(messageLog))
        {
            return;
        }

        final Collection<String> filePaths;
        if (parameters.getCssFiles() != null && !parameters.getCssFiles().isEmpty())
        {
            // Take directly provided css fle paths
            filePaths = parameters.getCssFiles();

            // If root dir is provided, filter out those files that are outside root dir
            if (StringUtils.isNotBlank(parameters.getOutputDir()))
            {
                filterFilesOutsideRootDir(filePaths);
            }

            // Make sure the files exist and are really files
            for (Iterator<String> it = filePaths.iterator(); it.hasNext();)
            {
                final String path = it.next();
                final File file = new File(path);
                if (file.exists())
                {
                    if (!file.isFile())
                    {
                        messageLog.warning(MessageType.CSS_PATH_IS_NOT_A_FILE, path);
                        it.remove();
                    }
                }
                else
                {
                    messageLog.warning(MessageType.CSS_FILE_DOES_NOT_EXIST, path);
                    it.remove();
                }
            }
        }
        else
        {
            // Take all css files from the root dir
            final List<File> files = Lists.newArrayList(org.apache.commons.io.FileUtils
                .listFiles(parameters.getRootDirFile(), new String []
                {
                    "css"
                }, true));
            Collections.sort(files, new Comparator<File>()
            {
                public int compare(File f1, File f2)
                {
                    return f1.getAbsolutePath().compareTo(f2.getAbsolutePath());
                }
            });

            filePaths = Lists.newArrayList();
            for (File file : files)
            {
                filePaths.add(file.getPath());
            }
        }

        buildSprites(filePaths);
    }

    private void filterFilesOutsideRootDir(Collection<String> filePaths)
        throws IOException
    {
        for (Iterator<String> it = filePaths.iterator(); it.hasNext();)
        {
            final String filePath = it.next();
            if (!FileUtils
                .isFileInParent(new File(filePath), parameters.getRootDirFile()))
            {
                it.remove();
                messageLog.warning(MessageType.IGNORING_CSS_FILE_OUTSIDE_OF_ROOT_DIR,
                    filePath);
            }
        }
    }

    /**
     * Performs processing from the list of file paths for this builder's parameters.
     *
     * @param filePaths paths of CSS files to process. Non-absolute paths will be taken
     *            relative to the current working directory. Both platform-specific and
     *            '/' as the file separator are supported.
     */
    public void buildSprites(Collection<String> filePaths) throws FileNotFoundException,
        IOException
    {
        final long start = System.currentTimeMillis();

        final LevelCounterMessageSink levelCounter = new LevelCounterMessageSink();
        messageLog.addMessageSink(levelCounter);

        // Collect sprite declarations from all css files
        final Multimap<String, SpriteImageOccurrence> spriteImageOccurrencesByFile = spriteDirectiveOccurrenceCollector
            .collectSpriteImageOccurrences(filePaths);

        // Merge them, checking for duplicates
        final Map<String, SpriteImageOccurrence> spriteImageOccurrencesBySpriteId = spriteDirectiveOccurrenceCollector
            .mergeSpriteImageOccurrences(spriteImageOccurrencesByFile);
        final Map<String, SpriteImageDirective> spriteImageDirectivesBySpriteId = Maps
            .newLinkedHashMap();
        for (Map.Entry<String, SpriteImageOccurrence> entry : spriteImageOccurrencesBySpriteId
            .entrySet())
        {
            spriteImageDirectivesBySpriteId.put(entry.getKey(),
                entry.getValue().spriteImageDirective);
        }

        // Collect sprite references from all css files
        final Multimap<String, SpriteReferenceOccurrence> spriteEntriesByFile = spriteDirectiveOccurrenceCollector
            .collectSpriteReferenceOccurrences(filePaths, spriteImageDirectivesBySpriteId);

        // Now merge and regroup all files by sprite-id
        final Multimap<String, SpriteReferenceOccurrence> spriteReferenceOccurrencesBySpriteId = SpriteDirectiveOccurrenceCollector
            .mergeSpriteReferenceOccurrences(spriteEntriesByFile);

        // Build the sprite images
        messageLog.setCssFile(null);
        final Multimap<String, SpriteReferenceReplacement> spriteReplacementsByFile = spriteImageBuilder
            .buildSpriteImages(spriteImageOccurrencesBySpriteId,
                spriteReferenceOccurrencesBySpriteId);

        // Rewrite the CSS
        rewriteCssFiles(spriteImageOccurrencesByFile, spriteReplacementsByFile);

        final long stop = System.currentTimeMillis();

        if (levelCounter.getWarnCount() > 0)
        {
            messageLog.status(MessageType.PROCESSING_COMPLETED_WITH_WARNINGS,
                (stop - start), levelCounter.getWarnCount());
        }
        else
        {
            messageLog.status(MessageType.PROCESSING_COMPLETED, (stop - start));
        }
    }

    /**
     * Rewrites the original files to refer to the generated sprite images.
     */
    private void rewriteCssFiles(
        final Multimap<String, SpriteImageOccurrence> spriteImageOccurrencesByFile,
        final Multimap<String, SpriteReferenceReplacement> spriteReplacementsByFile)
        throws IOException
    {
        if (spriteReplacementsByFile.isEmpty())
        {
            // If nothing to replace, still, copy the original file, so that there
            // is some output file.
            for (final Map.Entry<String, Collection<SpriteImageOccurrence>> entry : spriteImageOccurrencesByFile
                .asMap().entrySet())
            {
                final String cssFile = entry.getKey();

                createProcessedCss(
                    cssFile,
                    SpriteImageBuilder
                        .getSpriteImageOccurrencesByLineNumber(spriteImageOccurrencesByFile
                            .get(cssFile)),
                    new HashMap<Integer, SpriteReferenceReplacement>());
            }
        }
        else
        {
            for (final Map.Entry<String, Collection<SpriteReferenceReplacement>> entry : spriteReplacementsByFile
                .asMap().entrySet())
            {
                final String cssFile = entry.getKey();
                final Map<Integer, SpriteReferenceReplacement> spriteReplacementsByLineNumber = SpriteImageBuilder
                    .getSpriteReplacementsByLineNumber(entry.getValue());

                createProcessedCss(
                    cssFile,
                    SpriteImageBuilder
                        .getSpriteImageOccurrencesByLineNumber(spriteImageOccurrencesByFile
                            .get(cssFile)), spriteReplacementsByLineNumber);
            }
        }
    }

    /**
     * Rewrites one CSS file to refer to the generated sprite images.
     */
    private void createProcessedCss(String originalCssFile,
        Map<Integer, SpriteImageOccurrence> spriteImageOccurrencesByLineNumber,
        Map<Integer, SpriteReferenceReplacement> spriteReplacementsByLineNumber)
        throws IOException
    {
        final String processedCssFile = getProcessedCssFile(originalCssFile);
        final BufferedReader originalCssReader = new BufferedReader(
            resourceHandler.getResourceAsReader(originalCssFile));
        messageLog.setCssFile(null);
        messageLog.info(MessageType.CREATING_CSS_STYLE_SHEET, processedCssFile);
        messageLog.info(MessageType.READING_CSS, originalCssFile);
        final BufferedWriter processedCssWriter = new BufferedWriter(
            resourceHandler.getResourceAsWriter(processedCssFile));
        messageLog.info(MessageType.WRITING_CSS, processedCssFile);

        String originalCssLine;
        int originalCssLineNumber = -1;
        int lastReferenceReplacementLine = -1;

        boolean markSpriteImages = parameters.isMarkSpriteImages();
       
        // Generate UID for sprite file
        try
        {
            messageLog.setCssFile(originalCssFile);

            originalCssFile = originalCssFile.replace(File.separatorChar, '/');

            while ((originalCssLine = originalCssReader.readLine()) != null)
            {
                originalCssLineNumber++;
                messageLog.setLine(originalCssLineNumber);

                if (originalCssLine.contains("}"))
                {
                    lastReferenceReplacementLine = -1;
                }

                final SpriteImageOccurrence spriteImageOccurrence = spriteImageOccurrencesByLineNumber
                    .get(originalCssLineNumber);
                final SpriteReferenceReplacement spriteReferenceReplacement = spriteReplacementsByLineNumber
                    .get(originalCssLineNumber);

                if (spriteImageOccurrence != null)
                {
                    // Ignore line with directive
                    continue;
                }

                if (spriteReferenceReplacement != null)
                {
                    final boolean important = spriteReferenceReplacement.spriteReferenceOccurrence.important;
                    lastReferenceReplacementLine = originalCssLineNumber;

                    processedCssWriter.write("  background-image: url('"
                        + getRelativeToReplacementLocation(
                            spriteReferenceReplacement.spriteImage.resolvedPath,
                            originalCssFile, spriteReferenceReplacement) + "')"
                            + (important ? " !important" : "") + ";"+ (markSpriteImages ? " /** sprite:sprite */" :"") + "\n");

                    if (spriteReferenceReplacement.spriteImage.hasReducedForIe6)
                    {
                        processedCssWriter.write("  -background-image: url('"
                            + getRelativeToReplacementLocation(
                                spriteReferenceReplacement.spriteImage.resolvedPathIe6,
                                originalCssFile, spriteReferenceReplacement) + "')"
                                + (important ? " !important" : "") + ";"+ (markSpriteImages ? " /** sprite:sprite */" :"") + "\n");
                    }

                    processedCssWriter.write("  background-position: "
                        + spriteReferenceReplacement.horizontalPositionString + " "
                        + spriteReferenceReplacement.verticalPositionString
                        + (important ? " !important" : "") + ";\n");

                    // If the sprite scale is not 1, write out a background-size directive
                    final float scale = spriteReferenceReplacement.spriteImage.scaleRatio;
                    if (scale != 1.0f)
                    {
                        processedCssWriter.write("  background-size: "
                            + Math.round(spriteReferenceReplacement.spriteImage.spriteWidth / scale) + "px "
                            + Math.round(spriteReferenceReplacement.spriteImage.spriteHeight / scale) + "px;\n");
                    }

                    continue;
                }

                if (lastReferenceReplacementLine >= 0)
                {
                    for (final String property : OVERRIDING_PROPERTIES)
                    {
                        if (originalCssLine.contains(property))
                        {
                            messageLog.warning(MessageType.OVERRIDING_PROPERTY_FOUND,
                                property, lastReferenceReplacementLine);
                        }
                    }
                }

                // Just write the original line
                processedCssWriter.write(originalCssLine + "\n");
            }

            messageLog.setCssFile(null);
        }
        finally
        {
            Closeables.closeQuietly(originalCssReader);
            processedCssWriter.close();
        }
    }

    /**
     * Returns the sprite image's imagePath relative to the CSS in which we're making
     * replacements. The imagePath is relative to the CSS which declared the sprite image.
     * As it may happen that the image is referenced in another CSS file, we must make
     * sure the paths are correctly translated.
     */
    private String getRelativeToReplacementLocation(String imagePath,
        String originalCssFile,
        final SpriteReferenceReplacement spriteReferenceReplacement)
    {
        final String declaringCssPath = spriteReferenceReplacement.spriteImage.spriteImageOccurrence.cssFile
            .replace(File.separatorChar, '/');
        final String declarationReplacementRelativePath = PathUtils.getRelativeFilePath(
            originalCssFile.substring(0, originalCssFile.lastIndexOf('/')),
            declaringCssPath.substring(0, declaringCssPath.lastIndexOf('/'))).replace(
            File.separatorChar, '/');
        final String imagePathRelativeToReplacement = FileUtils
            .canonicalize(
                (StringUtils.isEmpty(declarationReplacementRelativePath)
                    || StringUtils.indexOfDifference(originalCssFile, declaringCssPath) <= 0 ? ""
                    : declarationReplacementRelativePath + '/')
                    + imagePath, "/");
        return imagePathRelativeToReplacement;
    }

    /**
     * Gets the name of the processed CSS file.
     */
    String getProcessedCssFile(String originalCssFile)
    {
        final int lastDotIndex = originalCssFile.lastIndexOf('.');
        final String processedCssFile;
        if (lastDotIndex >= 0)
        {
            processedCssFile = originalCssFile.substring(0, lastDotIndex)
                + parameters.getCssFileSuffix() + originalCssFile.substring(lastDotIndex);
        }
        else
        {
            processedCssFile = originalCssFile + parameters.getCssFileSuffix();
        }

        if (parameters.hasOutputDir())
        {
            return FileUtils.changeRoot(processedCssFile, parameters.getRootDir(),
                parameters.getOutputDir());
        }
        else
        {
            return processedCssFile;
        }
    }
}
TOP

Related Classes of org.carrot2.labs.smartsprites.SpriteBuilder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.