001    // Copyright 2011 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    
032    /**
033     * JavaScript resource minimizer based on the YUI {@link JavaScriptCompressor}.
034     *
035     * @since 5.3
036     */
037    public class JavaScriptResourceMinimizer extends AbstractMinimizer
038    {
039        private final Logger logger;
040    
041        private final static int RANGE = 5;
042    
043        private enum Where
044        {
045            EXACT, NEAR, FAR
046        }
047    
048        public JavaScriptResourceMinimizer(final Logger logger, OperationTracker tracker)
049        {
050            super(logger, tracker, "JavaScript");
051    
052            this.logger = logger;
053        }
054    
055        protected void doMinimize(StreamableResource resource, Writer output) throws IOException
056        {
057            final Set<Integer> errorLines = CollectionFactory.newSet();
058    
059            ErrorReporter errorReporter = new ErrorReporter()
060            {
061                private String format(String message, int line, int lineOffset)
062                {
063                    if (line < 0)
064                        return message;
065    
066                    return String.format("(%d:%d): %s", line, lineOffset, message);
067                }
068    
069                public void warning(String message, String sourceName, int line, String lineSource, int lineOffset)
070                {
071                    errorLines.add(line);
072    
073                    logger.warn(format(message, line, lineOffset));
074                }
075    
076                public EvaluatorException runtimeError(String message, String sourceName, int line, String lineSource,
077                                                       int lineOffset)
078                {
079                    error(message, sourceName, line, lineSource, lineOffset);
080    
081                    return new EvaluatorException(message);
082                }
083    
084                public void error(String message, String sourceName, int line, String lineSource, int lineOffset)
085                {
086                    errorLines.add(line);
087    
088                    logger.error(format(message, line, lineOffset));
089                }
090    
091            };
092    
093    
094            Reader reader = toReader(resource);
095    
096            try
097            {
098                JavaScriptCompressor compressor = new JavaScriptCompressor(reader, errorReporter);
099                compressor.compress(output, -1, true, false, false, false);
100            } catch (EvaluatorException ex)
101            {
102                logInputLines(resource, errorLines);
103    
104                recoverFromException(ex, resource, output);
105    
106            } catch (Exception ex)
107            {
108                recoverFromException(ex, resource, output);
109            }
110    
111            reader.close();
112        }
113    
114        private void recoverFromException(Exception ex, StreamableResource resource, Writer output) throws IOException
115        {
116            logger.error(String.format("Exception minimizing %s: %s", resource.getDescription(), InternalUtils.toMessage(ex)), ex);
117    
118            streamUnminimized(resource, output);
119        }
120    
121        private void streamUnminimized(StreamableResource resource, Writer output) throws IOException
122        {
123            Reader reader = toReader(resource);
124    
125            char[] buffer = new char[5000];
126    
127            try
128            {
129    
130                while (true)
131                {
132                    int length = reader.read(buffer);
133    
134                    if (length < 0)
135                    {
136                        break;
137                    }
138    
139                    output.write(buffer, 0, length);
140                }
141            } finally
142            {
143                reader.close();
144            }
145        }
146    
147        private void logInputLines(StreamableResource resource, Set<Integer> lines)
148        {
149            logger.error(String.format("Errors in resource %s:", resource.getDescription()));
150    
151            int last = -1;
152    
153            try
154            {
155                LineNumberReader lnr = new LineNumberReader(toReader(resource));
156    
157                while (true)
158                {
159                    String line = lnr.readLine();
160    
161                    if (line == null) break;
162    
163                    int lineNumber = lnr.getLineNumber();
164    
165                    Where where = where(lineNumber, lines);
166    
167                    if (where == Where.FAR)
168                    {
169                        continue;
170                    }
171    
172                    // Add a blank line to separate non-consecutive parts of the content.
173                    if (last > 0 && last + 1 != lineNumber)
174                    {
175                        logger.error("");
176                    }
177    
178                    String formatted = String.format("%s%6d %s",
179                            where == Where.EXACT ? "*" : " ",
180                            lineNumber,
181                            line);
182    
183                    logger.error(formatted);
184    
185                    last = lineNumber;
186                }
187    
188                lnr.close();
189    
190            } catch (IOException ex)
191            { // Ignore.
192            }
193    
194        }
195    
196        private Where where(int lineNumber, Set<Integer> lines)
197        {
198            if (lines.contains(lineNumber))
199            {
200                return Where.EXACT;
201            }
202    
203            for (int line : lines)
204            {
205                if (Math.abs(lineNumber - line) < RANGE)
206                {
207                    return Where.NEAR;
208                }
209            }
210    
211            return Where.FAR;
212        }
213    }