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 }