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
015package org.apache.tapestry5.internal.yuicompressor;
016
017import com.yahoo.platform.yui.compressor.JavaScriptCompressor;
018import org.apache.tapestry5.ioc.OperationTracker;
019import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
020import org.apache.tapestry5.ioc.internal.util.InternalUtils;
021import org.apache.tapestry5.services.assets.StreamableResource;
022import org.mozilla.javascript.ErrorReporter;
023import org.mozilla.javascript.EvaluatorException;
024import org.slf4j.Logger;
025
026import java.io.IOException;
027import java.io.LineNumberReader;
028import java.io.Reader;
029import java.io.Writer;
030import java.util.Set;
031import java.util.concurrent.atomic.AtomicInteger;
032
033/**
034 * JavaScript resource minimizer based on the YUI {@link JavaScriptCompressor}.
035 *
036 * @since 5.3
037 */
038public 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            @Override
067            public void run()
068            {
069                if (!sourceIdentified)
070                {
071                    logger.error(String.format("JavaScript compression problems for resource %s:",
072                            resource.getDescription()));
073                    sourceIdentified = true;
074                }
075            }
076        };
077
078        final AtomicInteger warningCount = new AtomicInteger();
079
080        Runnable identifyWarnings = new Runnable()
081        {
082            @Override
083            public void run()
084            {
085                if (warningCount.get() > 0)
086                {
087                    logger.error(String.format("%,d compression warnings; enable warning logging of %s to see details.",
088                            warningCount.get(),
089                            logger.getName()));
090                }
091            }
092        };
093
094        ErrorReporter errorReporter = new ErrorReporter()
095        {
096            private String format(String message, int line, int lineOffset)
097            {
098                if (line < 0)
099                    return message;
100
101                return String.format("(%d:%d): %s", line, lineOffset, message);
102            }
103
104            public void warning(String message, String sourceName, int line, String lineSource, int lineOffset)
105            {
106                for (String ignored : IGNORED_WARNINGS)
107                {
108                    if (message.contains(ignored))
109                    {
110                        return;
111                    }
112                }
113
114                identifySource.run();
115
116                errorLines.add(line);
117
118                if (logger.isWarnEnabled())
119                {
120                    logger.warn(format(message, line, lineOffset));
121                } else
122                {
123                    warningCount.incrementAndGet();
124                }
125            }
126
127            public EvaluatorException runtimeError(String message, String sourceName, int line, String lineSource,
128                                                   int lineOffset)
129            {
130                error(message, sourceName, line, lineSource, lineOffset);
131
132                return new EvaluatorException(message);
133            }
134
135            public void error(String message, String sourceName, int line, String lineSource, int lineOffset)
136            {
137                identifySource.run();
138
139                errorLines.add(line);
140
141                logger.error(format(message, line, lineOffset));
142            }
143
144        };
145
146        Reader reader = toReader(resource);
147
148        try
149        {
150            JavaScriptCompressor compressor = new JavaScriptCompressor(reader, errorReporter);
151            compressor.compress(output, -1, true, true, false, false);
152
153            identifyWarnings.run();
154
155        } catch (EvaluatorException ex)
156        {
157            identifySource.run();
158
159            logInputLines(resource, errorLines);
160
161            recoverFromException(ex, resource, output);
162
163        } catch (Exception ex)
164        {
165            identifySource.run();
166
167            recoverFromException(ex, resource, output);
168        }
169
170        reader.close();
171    }
172
173    private void recoverFromException(Exception ex, StreamableResource resource, Writer output) throws IOException
174    {
175        logger.error(InternalUtils.toMessage(ex), ex);
176
177        streamUnminimized(resource, output);
178    }
179
180    private void streamUnminimized(StreamableResource resource, Writer output) throws IOException
181    {
182        Reader reader = toReader(resource);
183
184        char[] buffer = new char[5000];
185
186        try
187        {
188
189            while (true)
190            {
191                int length = reader.read(buffer);
192
193                if (length < 0)
194                {
195                    break;
196                }
197
198                output.write(buffer, 0, length);
199            }
200        } finally
201        {
202            reader.close();
203        }
204    }
205
206    private void logInputLines(StreamableResource resource, Set<Integer> lines)
207    {
208        int last = -1;
209
210        try
211        {
212            LineNumberReader lnr = new LineNumberReader(toReader(resource));
213
214            while (true)
215            {
216                String line = lnr.readLine();
217
218                if (line == null) break;
219
220                int lineNumber = lnr.getLineNumber();
221
222                Where where = where(lineNumber, lines);
223
224                if (where == Where.FAR)
225                {
226                    continue;
227                }
228
229                // Add a blank line to separate non-consecutive parts of the content.
230                if (last > 0 && last + 1 != lineNumber)
231                {
232                    logger.error("");
233                }
234
235                String formatted = String.format("%s%6d %s",
236                        where == Where.EXACT ? "*" : " ",
237                        lineNumber,
238                        line);
239
240                logger.error(formatted);
241
242                last = lineNumber;
243            }
244
245            lnr.close();
246
247        } catch (IOException ex)
248        { // Ignore.
249        }
250
251    }
252
253    private Where where(int lineNumber, Set<Integer> lines)
254    {
255        if (lines.contains(lineNumber))
256        {
257            return Where.EXACT;
258        }
259
260        for (int line : lines)
261        {
262            if (Math.abs(lineNumber - line) < RANGE)
263            {
264                return Where.NEAR;
265            }
266        }
267
268        return Where.FAR;
269    }
270}