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 }