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 }