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    }