• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hazendaz / smartsprites / #43

11 Nov 2023 07:47PM UTC coverage: 88.431%. Remained the same
#43

push

github

hazendaz
[tests] Fix tests given they expected order and now license on everything for +36

1437 of 1625 relevant lines covered (88.43%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

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

39
import java.io.BufferedReader;
40
import java.io.BufferedWriter;
41
import java.io.File;
42
import java.io.IOException;
43
import java.util.ArrayList;
44
import java.util.Collection;
45
import java.util.Collections;
46
import java.util.Comparator;
47
import java.util.HashMap;
48
import java.util.HashSet;
49
import java.util.Iterator;
50
import java.util.LinkedHashMap;
51
import java.util.List;
52
import java.util.Map;
53

54
import org.carrot2.labs.smartsprites.message.LevelCounterMessageSink;
55
import org.carrot2.labs.smartsprites.message.Message.MessageType;
56
import org.carrot2.labs.smartsprites.message.MessageLog;
57
import org.carrot2.labs.smartsprites.resource.FileSystemResourceHandler;
58
import org.carrot2.labs.smartsprites.resource.ResourceHandler;
59
import org.carrot2.util.FileUtils;
60
import org.carrot2.util.PathUtils;
61
import org.carrot2.util.StringUtils;
62

63
import com.google.common.base.Strings;
64
import com.google.common.collect.Lists;
65
import com.google.common.collect.Multimap;
66
import com.google.common.collect.Sets;
67
import com.google.common.io.Closeables;
68

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

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

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

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

87
    /** SpriteImageBuilder for this builder */
88
    private final SpriteImageBuilder spriteImageBuilder;
89

90
    /** Resource handler */
91
    private ResourceHandler resourceHandler;
92

93
    /**
94
     * Creates a {@link SpriteBuilder} with the provided parameters and log.
95
     */
96
    public SpriteBuilder(SmartSpritesParameters parameters, MessageLog messageLog)
97
    {
98
        this(parameters, messageLog, new FileSystemResourceHandler(
1✔
99
            parameters.getDocumentRootDir(), parameters.getCssFileEncoding(), messageLog));
1✔
100
    }
1✔
101

102
    /**
103
     * Creates a {@link SpriteBuilder} with the provided parameters and log.
104
     */
105
    public SpriteBuilder(SmartSpritesParameters parameters, MessageLog messageLog,
106
        ResourceHandler resourceHandler)
107
    {
1✔
108
        this.messageLog = messageLog;
1✔
109
        this.parameters = parameters;
1✔
110
        this.resourceHandler = resourceHandler;
1✔
111
        spriteDirectiveOccurrenceCollector = new SpriteDirectiveOccurrenceCollector(
1✔
112
            messageLog, resourceHandler);
113
        spriteImageBuilder = new SpriteImageBuilder(parameters, messageLog,
1✔
114
            resourceHandler);
115
    }
1✔
116

117
    /**
118
     * Performs processing for this builder's parameters. This method resolves all paths
119
     * against the local file system.
120
     */
121
    public void buildSprites() throws IOException
122
    {
123
        if (!parameters.validate(messageLog))
1✔
124
        {
125
            return;
×
126
        }
127

128
        final Collection<String> filePaths;
129
        if (parameters.getCssFiles() != null && !parameters.getCssFiles().isEmpty())
1✔
130
        {
131
            // Take directly provided css fle paths
132
            filePaths = parameters.getCssFiles();
1✔
133

134
            // If root dir is provided, filter out those files that are outside root dir
135
            if (StringUtils.isNotBlank(parameters.getOutputDir()))
1✔
136
            {
137
                filterFilesOutsideRootDir(filePaths);
1✔
138
            }
139

140
            // Make sure the files exist and are really files
141
            for (Iterator<String> it = filePaths.iterator(); it.hasNext();)
1✔
142
            {
143
                final String path = it.next();
1✔
144
                final File file = new File(path);
1✔
145
                if (file.exists())
1✔
146
                {
147
                    if (!file.isFile())
1✔
148
                    {
149
                        messageLog.warning(MessageType.CSS_PATH_IS_NOT_A_FILE, path);
1✔
150
                        it.remove();
1✔
151
                    }
152
                }
153
                else
154
                {
155
                    messageLog.warning(MessageType.CSS_FILE_DOES_NOT_EXIST, path);
1✔
156
                    it.remove();
1✔
157
                }
158
            }
1✔
159
        }
160
        else
161
        {
162
            // Take all css files from the root dir
163
            final List<File> files = Lists.newArrayList(org.apache.commons.io.FileUtils
1✔
164
                .listFiles(parameters.getRootDirFile(), new String []
1✔
165
                {
166
                    "css"
167
                }, true));
168
            Collections.sort(files, new Comparator<File>()
1✔
169
            {
1✔
170
                public int compare(File f1, File f2)
171
                {
172
                    return f1.getAbsolutePath().compareTo(f2.getAbsolutePath());
1✔
173
                }
174
            });
175

176
            filePaths = new ArrayList<>();
1✔
177
            for (File file : files)
1✔
178
            {
179
                filePaths.add(file.getPath());
1✔
180
            }
1✔
181
        }
182

183
        buildSprites(filePaths);
1✔
184
    }
1✔
185

186
    private void filterFilesOutsideRootDir(Collection<String> filePaths)
187
        throws IOException
188
    {
189
        for (Iterator<String> it = filePaths.iterator(); it.hasNext();)
1✔
190
        {
191
            final String filePath = it.next();
1✔
192
            if (!FileUtils
1✔
193
                .isFileInParent(new File(filePath), parameters.getRootDirFile()))
1✔
194
            {
195
                it.remove();
1✔
196
                messageLog.warning(MessageType.IGNORING_CSS_FILE_OUTSIDE_OF_ROOT_DIR,
1✔
197
                    filePath);
198
            }
199
        }
1✔
200
    }
1✔
201

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

213
        final LevelCounterMessageSink levelCounter = new LevelCounterMessageSink();
1✔
214
        messageLog.addMessageSink(levelCounter);
1✔
215

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

220
        // Merge them, checking for duplicates
221
        final Map<String, SpriteImageOccurrence> spriteImageOccurrencesBySpriteId = spriteDirectiveOccurrenceCollector
1✔
222
            .mergeSpriteImageOccurrences(spriteImageOccurrencesByFile);
1✔
223
        final Map<String, SpriteImageDirective> spriteImageDirectivesBySpriteId = new LinkedHashMap<>();
1✔
224
        for (Map.Entry<String, SpriteImageOccurrence> entry : spriteImageOccurrencesBySpriteId
1✔
225
            .entrySet())
1✔
226
        {
227
            spriteImageDirectivesBySpriteId.put(entry.getKey(),
1✔
228
                entry.getValue().spriteImageDirective);
1✔
229
        }
1✔
230

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

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

239
        // Build the sprite images
240
        messageLog.setCssFile(null);
1✔
241
        final Multimap<String, SpriteReferenceReplacement> spriteReplacementsByFile = spriteImageBuilder
1✔
242
            .buildSpriteImages(spriteImageOccurrencesBySpriteId,
1✔
243
                spriteReferenceOccurrencesBySpriteId);
244

245
        // Rewrite the CSS
246
        rewriteCssFiles(spriteImageOccurrencesByFile, spriteReplacementsByFile);
1✔
247

248
        final long stop = System.currentTimeMillis();
1✔
249

250
        if (levelCounter.getWarnCount() > 0)
1✔
251
        {
252
            messageLog.status(MessageType.PROCESSING_COMPLETED_WITH_WARNINGS,
1✔
253
                (stop - start), levelCounter.getWarnCount());
1✔
254
        }
255
        else
256
        {
257
            messageLog.status(MessageType.PROCESSING_COMPLETED, (stop - start));
1✔
258
        }
259
    }
1✔
260

261
    /**
262
     * Rewrites the original files to refer to the generated sprite images.
263
     */
264
    private void rewriteCssFiles(
265
        final Multimap<String, SpriteImageOccurrence> spriteImageOccurrencesByFile,
266
        final Multimap<String, SpriteReferenceReplacement> spriteReplacementsByFile)
267
        throws IOException
268
    {
269
        if (spriteReplacementsByFile.isEmpty())
1✔
270
        {
271
            // If nothing to replace, still, copy the original file, so that there
272
            // is some output file.
273
            for (final Map.Entry<String, Collection<SpriteImageOccurrence>> entry : spriteImageOccurrencesByFile
1✔
274
                .asMap().entrySet())
1✔
275
            {
276
                final String cssFile = entry.getKey();
1✔
277

278
                createProcessedCss(
1✔
279
                    cssFile,
280
                    SpriteImageBuilder
281
                        .getSpriteImageOccurrencesByLineNumber(spriteImageOccurrencesByFile
1✔
282
                            .get(cssFile)),
1✔
283
                    new HashMap<Integer, SpriteReferenceReplacement>());
284
            }
1✔
285
        }
286
        else
287
        {
288
            for (final Map.Entry<String, Collection<SpriteReferenceReplacement>> entry : spriteReplacementsByFile
1✔
289
                .asMap().entrySet())
1✔
290
            {
291
                final String cssFile = entry.getKey();
1✔
292
                final Map<Integer, SpriteReferenceReplacement> spriteReplacementsByLineNumber = SpriteImageBuilder
1✔
293
                    .getSpriteReplacementsByLineNumber(entry.getValue());
1✔
294

295
                createProcessedCss(
1✔
296
                    cssFile,
297
                    SpriteImageBuilder
298
                        .getSpriteImageOccurrencesByLineNumber(spriteImageOccurrencesByFile
1✔
299
                            .get(cssFile)), spriteReplacementsByLineNumber);
1✔
300
            }
1✔
301
        }
302
    }
1✔
303

304
    /**
305
     * Rewrites one CSS file to refer to the generated sprite images.
306
     */
307
    private void createProcessedCss(String originalCssFile,
308
        Map<Integer, SpriteImageOccurrence> spriteImageOccurrencesByLineNumber,
309
        Map<Integer, SpriteReferenceReplacement> spriteReplacementsByLineNumber)
310
        throws IOException
311
    {
312
        final String processedCssFile = getProcessedCssFile(originalCssFile);
1✔
313
        final BufferedReader originalCssReader = new BufferedReader(
1✔
314
            resourceHandler.getResourceAsReader(originalCssFile));
1✔
315
        messageLog.setCssFile(null);
1✔
316
        messageLog.info(MessageType.CREATING_CSS_STYLE_SHEET, processedCssFile);
1✔
317
        messageLog.info(MessageType.READING_CSS, originalCssFile);
1✔
318
        final BufferedWriter processedCssWriter = new BufferedWriter(
1✔
319
            resourceHandler.getResourceAsWriter(processedCssFile));
1✔
320
        messageLog.info(MessageType.WRITING_CSS, processedCssFile);
1✔
321

322
        String originalCssLine;
323
        int originalCssLineNumber = -1;
1✔
324
        int lastReferenceReplacementLine = -1;
1✔
325

326
        boolean markSpriteImages = parameters.isMarkSpriteImages();
1✔
327
        
328
        // Generate UID for sprite file
329
        try
330
        {
331
            messageLog.setCssFile(originalCssFile);
1✔
332

333
            originalCssFile = originalCssFile.replace(File.separatorChar, '/');
1✔
334

335
            while ((originalCssLine = originalCssReader.readLine()) != null)
1✔
336
            {
337
                originalCssLineNumber++;
1✔
338
                messageLog.setLine(originalCssLineNumber);
1✔
339

340
                if (originalCssLine.contains("}"))
1✔
341
                {
342
                    lastReferenceReplacementLine = -1;
1✔
343
                }
344

345
                final SpriteImageOccurrence spriteImageOccurrence = spriteImageOccurrencesByLineNumber
1✔
346
                    .get(originalCssLineNumber);
1✔
347
                final SpriteReferenceReplacement spriteReferenceReplacement = spriteReplacementsByLineNumber
1✔
348
                    .get(originalCssLineNumber);
1✔
349

350
                if (spriteImageOccurrence != null)
1✔
351
                {
352
                    // Ignore line with directive
353
                    continue;
1✔
354
                }
355

356
                if (spriteReferenceReplacement != null)
1✔
357
                {
358
                    final boolean important = spriteReferenceReplacement.spriteReferenceOccurrence.important;
1✔
359
                    lastReferenceReplacementLine = originalCssLineNumber;
1✔
360

361
                    processedCssWriter.write("  background-image: url('"
1✔
362
                        + getRelativeToReplacementLocation(
1✔
363
                            spriteReferenceReplacement.spriteImage.resolvedPath,
364
                            originalCssFile, spriteReferenceReplacement) + "')"
365
                            + (important ? " !important" : "") + ";"+ (markSpriteImages ? " /** sprite:sprite */" :"") + "\n");
1✔
366

367
                    if (spriteReferenceReplacement.spriteImage.hasReducedForIe6)
1✔
368
                    {
369
                        processedCssWriter.write("  -background-image: url('"
1✔
370
                            + getRelativeToReplacementLocation(
1✔
371
                                spriteReferenceReplacement.spriteImage.resolvedPathIe6,
372
                                originalCssFile, spriteReferenceReplacement) + "')"
373
                                + (important ? " !important" : "") + ";"+ (markSpriteImages ? " /** sprite:sprite */" :"") + "\n");
1✔
374
                    }
375

376
                    processedCssWriter.write("  background-position: "
1✔
377
                        + spriteReferenceReplacement.horizontalPositionString + " "
378
                        + spriteReferenceReplacement.verticalPositionString
379
                        + (important ? " !important" : "") + ";\n");
1✔
380

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

390
                    continue;
391
                }
392

393
                if (lastReferenceReplacementLine >= 0)
1✔
394
                {
395
                    for (final String property : OVERRIDING_PROPERTIES)
1✔
396
                    {
397
                        if (originalCssLine.contains(property))
1✔
398
                        {
399
                            messageLog.warning(MessageType.OVERRIDING_PROPERTY_FOUND,
1✔
400
                                property, lastReferenceReplacementLine);
1✔
401
                        }
402
                    }
1✔
403
                }
404

405
                // Just write the original line
406
                processedCssWriter.write(originalCssLine + "\n");
1✔
407
            }
1✔
408

409
            messageLog.setCssFile(null);
1✔
410
        }
411
        finally
412
        {
413
            Closeables.close(originalCssReader, true);
1✔
414
            processedCssWriter.close();
1✔
415
        }
416
    }
1✔
417

418
    /**
419
     * Returns the sprite image's imagePath relative to the CSS in which we're making
420
     * replacements. The imagePath is relative to the CSS which declared the sprite image.
421
     * As it may happen that the image is referenced in another CSS file, we must make
422
     * sure the paths are correctly translated.
423
     */
424
    private String getRelativeToReplacementLocation(String imagePath,
425
        String originalCssFile,
426
        final SpriteReferenceReplacement spriteReferenceReplacement)
427
    {
428
        final String declaringCssPath = spriteReferenceReplacement.spriteImage.spriteImageOccurrence.cssFile
1✔
429
            .replace(File.separatorChar, '/');
1✔
430
        final String declarationReplacementRelativePath = PathUtils.getRelativeFilePath(
1✔
431
            originalCssFile.substring(0, originalCssFile.lastIndexOf('/')),
1✔
432
            declaringCssPath.substring(0, declaringCssPath.lastIndexOf('/'))).replace(
1✔
433
            File.separatorChar, '/');
434
        return FileUtils.canonicalize(
1✔
435
                (Strings.isNullOrEmpty(declarationReplacementRelativePath)
1✔
436
                    || originalCssFile.equals(declaringCssPath) ? ""
1✔
437
                    : declarationReplacementRelativePath + '/')
1✔
438
                    + imagePath, "/");
439
    }
440

441
    /**
442
     * Gets the name of the processed CSS file.
443
     */
444
    String getProcessedCssFile(String originalCssFile)
445
    {
446
        final int lastDotIndex = originalCssFile.lastIndexOf('.');
1✔
447
        final String processedCssFile;
448
        if (lastDotIndex >= 0)
1✔
449
        {
450
            processedCssFile = originalCssFile.substring(0, lastDotIndex)
1✔
451
                + parameters.getCssFileSuffix() + originalCssFile.substring(lastDotIndex);
1✔
452
        }
453
        else
454
        {
455
            processedCssFile = originalCssFile + parameters.getCssFileSuffix();
×
456
        }
457

458
        if (parameters.hasOutputDir())
1✔
459
        {
460
            return FileUtils.changeRoot(processedCssFile, parameters.getRootDir(),
1✔
461
                parameters.getOutputDir());
1✔
462
        }
463
        else
464
        {
465
            return processedCssFile;
1✔
466
        }
467
    }
468
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc