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 }