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                @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    }