    001    // Copyright 2011, 2012 The Apache Software Foundation
    002    //
    003    // Licensed under the Apache License, Version 2.0 (the "License");
    004    // you may not use this file except in compliance with the License.
    005    // You may obtain a copy of the License at
    006    //
    007    // http://www.apache.org/licenses/LICENSE-2.0
    008    //
    009    // Unless required by applicable law or agreed to in writing, software
    010    // distributed under the License is distributed on an "AS IS" BASIS,
    011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012    // See the License for the specific language governing permissions and
    013    // limitations under the License.
    014
    015    package org.apache.tapestry5.internal.yuicompressor;
    016
    017    import com.yahoo.platform.yui.compressor.JavaScriptCompressor;
    018    import org.apache.tapestry5.ioc.OperationTracker;
    019    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    020    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    021    import org.apache.tapestry5.services.assets.StreamableResource;
    022    import org.mozilla.javascript.ErrorReporter;
    023    import org.mozilla.javascript.EvaluatorException;
    024    import org.slf4j.Logger;
    025
    026    import java.io.IOException;
    027    import java.io.LineNumberReader;
    028    import java.io.Reader;
    029    import java.io.Writer;
    030    import java.util.Set;
    031    import java.util.concurrent.atomic.AtomicInteger;
    032
    033    /**
    034     * JavaScript resource minimizer based on the YUI {@link JavaScriptCompressor}.
    035     *
    036     * @since 5.3
    037     */
    038    public class JavaScriptResourceMinimizer extends AbstractMinimizer
    039    {
    040        private final static int RANGE = 5;
    041
    042        private enum Where
    043        {
    044            EXACT, NEAR, FAR
    045        }
    046
    047        private static final String[] IGNORED_WARNINGS = {
    048                "Try to use a single 'var' statement per scope.",
    049                "Using 'eval' is not recommended",
    050                "has already been declared in the same scope"
    051        };
    052
    053        public JavaScriptResourceMinimizer(final Logger logger, OperationTracker tracker)
    054        {
    055            super(logger, tracker, "JavaScript");
    056        }
    057
    058        protected void doMinimize(final StreamableResource resource, Writer output) throws IOException
    059        {
    060            final Set<Integer> errorLines = CollectionFactory.newSet();
    061
    062            final Runnable identifySource = new Runnable()
    063            {
    064                boolean sourceIdentified = false;
    065
    066                public void run()
    067                {
    068                    if (!sourceIdentified)
    069                    {
    070                        logger.error(String.format("JavaScript compression problems for resource %s:",
    071                                resource.getDescription()));
    072                        sourceIdentified = true;
    073                    }
    074                }
    075            };
    076
    077            final AtomicInteger warningCount = new AtomicInteger();
    078
    079            Runnable identifyWarnings = new Runnable()
    080            {
    081                public void run()
    082                {
    083                    if (warningCount.get() > 0)
    084                    {
    085                        logger.error(String.format("%,d compression warnings; enable warning logging of %s to see details.",
    086                                warningCount.get(),
    087                                logger.getName()));
    088                    }
    089                }
    090            };
    091
    092            ErrorReporter errorReporter = new ErrorReporter()
    093            {
    094                private String format(String message, int line, int lineOffset)
    095                {
    096                    if (line < 0)
    097                        return message;
    098
    099                    return String.format("(%d:%d): %s", line, lineOffset, message);
    100                }
    101
    102                public void warning(String message, String sourceName, int line, String lineSource, int lineOffset)
    103                {
    104                    for (String ignored : IGNORED_WARNINGS)
    105                    {
    106                        if (message.contains(ignored))
    107                        {
    108                            return;
    109                        }
    110                    }
    111
    112                    identifySource.run();
    113
    114                    errorLines.add(line);
    115
    116                    if (logger.isWarnEnabled())
    117                    {
    118                        logger.warn(format(message, line, lineOffset));
    119                    } else
    120                    {
    121                        warningCount.incrementAndGet();
    122                    }
    123                }
    124
    125                public EvaluatorException runtimeError(String message, String sourceName, int line, String lineSource,
    126                                                       int lineOffset)
    127                {
    128                    error(message, sourceName, line, lineSource, lineOffset);
    129
    130                    return new EvaluatorException(message);
    131                }
    132
    133                public void error(String message, String sourceName, int line, String lineSource, int lineOffset)
    134                {
    135                    identifySource.run();
    136
    137                    errorLines.add(line);
    138
    139                    logger.error(format(message, line, lineOffset));
    140                }
    141
    142            };
    143
    144            Reader reader = toReader(resource);
    145
    146            try
    147            {
    148                JavaScriptCompressor compressor = new JavaScriptCompressor(reader, errorReporter);
    149                compressor.compress(output, -1, true, true, false, false);
    150
    151                identifyWarnings.run();
    152
    153            } catch (EvaluatorException ex)
    154            {
    155                identifySource.run();
    156
    157                logInputLines(resource, errorLines);
    158
    159                recoverFromException(ex, resource, output);
    160
    161            } catch (Exception ex)
    162            {
    163                identifySource.run();
    164
    165                recoverFromException(ex, resource, output);
    166            }
    167
    168            reader.close();
    169        }
    170
    171        private void recoverFromException(Exception ex, StreamableResource resource, Writer output) throws IOException
    172        {
    173            logger.error(InternalUtils.toMessage(ex), ex);
    174
    175            streamUnminimized(resource, output);
    176        }
    177
    178        private void streamUnminimized(StreamableResource resource, Writer output) throws IOException
    179        {
    180            Reader reader = toReader(resource);
    181
    182            char[] buffer = new char[5000];
    183
    184            try
    185            {
    186
    187                while (true)
    188                {
    189                    int length = reader.read(buffer);
    190
    191                    if (length < 0)
    192                    {
    193                        break;
    194                    }
    195
    196                    output.write(buffer, 0, length);
    197                }
    198            } finally
    199            {
    200                reader.close();
    201            }
    202        }
    203
    204        private void logInputLines(StreamableResource resource, Set<Integer> lines)
    205        {
    206            int last = -1;
    207
    208            try
    209            {
    210                LineNumberReader lnr = new LineNumberReader(toReader(resource));
    211
    212                while (true)
    213                {
    214                    String line = lnr.readLine();
    215
    216                    if (line == null) break;
    217
    218                    int lineNumber = lnr.getLineNumber();
    219
    220                    Where where = where(lineNumber, lines);
    221
    222                    if (where == Where.FAR)
    223                    {
    224                        continue;
    225                    }
    226
    227                    // Add a blank line to separate non-consecutive parts of the content.
    228                    if (last > 0 && last + 1 != lineNumber)
    229                    {
    230                        logger.error("");
    231                    }
    232
    233                    String formatted = String.format("%s%6d %s",
    234                            where == Where.EXACT ? "*" : " ",
    235                            lineNumber,
    236                            line);
    237
    238                    logger.error(formatted);
    239
    240                    last = lineNumber;
    241                }
    242
    243                lnr.close();
    244
    245            } catch (IOException ex)
    246            { // Ignore.
    247            }
    248
    249        }
    250
    251        private Where where(int lineNumber, Set<Integer> lines)
    252        {
    253            if (lines.contains(lineNumber))
    254            {
    255                return Where.EXACT;
    256            }
    257
    258            for (int line : lines)
    259            {
    260                if (Math.abs(lineNumber - line) < RANGE)
    261                {
    262                    return Where.NEAR;
    263                }
    264            }
    265
    266            return Where.FAR;
    267        }
    268    }