1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.jasper.compiler;
19
20 import java.io.File;
21 import java.io.FileNotFoundException;
22 import java.io.FileOutputStream;
23 import java.io.OutputStreamWriter;
24 import java.io.PrintWriter;
25 import java.io.UnsupportedEncodingException;
26 import java.net.JarURLConnection;
27 import java.net.URL;
28 import java.net.URLConnection;
29 import java.util.Map;
30 import java.util.Map.Entry;
31
32 import org.apache.jasper.JasperException;
33 import org.apache.jasper.JspCompilationContext;
34 import org.apache.jasper.Options;
35 import org.apache.jasper.servlet.JspServletWrapper;
36 import org.apache.juli.logging.Log;
37 import org.apache.juli.logging.LogFactory;
38 import org.apache.tomcat.Jar;
39 import org.apache.tomcat.util.scan.JarFactory;
40
41 /**
42 * Main JSP compiler class. This class uses Ant for compiling.
43 *
44 * @author Anil K. Vijendran
45 * @author Mandar Raje
46 * @author Pierre Delisle
47 * @author Kin-man Chung
48 * @author Remy Maucherat
49 * @author Mark Roth
50 */
51 public abstract class Compiler {
52
53 private final Log log = LogFactory.getLog(Compiler.class); // must not be static
54
55 // ----------------------------------------------------- Instance Variables
56
57 protected JspCompilationContext ctxt;
58
59 protected ErrorDispatcher errDispatcher;
60
61 protected PageInfo pageInfo;
62
63 protected JspServletWrapper jsw;
64
65 protected TagFileProcessor tfp;
66
67 protected Options options;
68
69 protected Node.Nodes pageNodes;
70
71
72 // ------------------------------------------------------------ Constructor
73
74 public void init(JspCompilationContext ctxt, JspServletWrapper jsw) {
75 this.jsw = jsw;
76 this.ctxt = ctxt;
77 this.options = ctxt.getOptions();
78 }
79
80
81 // --------------------------------------------------------- Public Methods
82
83 public SmapStratum getSmap(String className) {
84
85 Map<String,SmapStratum> smaps = ctxt.getRuntimeContext().getSmaps();
86 SmapStratum smap = smaps.get(className);
87
88 if (smap == null && !options.isSmapSuppressed()) {
89 // Tomcat was restarted so cached SMAP has been lost. However, it
90 // was written to the class file so it can be recovered.
91 smap = SmapUtil.loadSmap(className, ctxt.getJspLoader());
92 if (smap != null) {
93 smaps.put(className, smap);
94 }
95 }
96
97 return smap;
98 }
99
100
101 /**
102 * Compile the jsp file into equivalent servlet in .java file
103 *
104 * @return A map of class names to JSR 045 source maps
105 *
106 * @throws Exception Error generating Java source
107 */
108 protected Map<String,SmapStratum> generateJava() throws Exception {
109
110 long t1, t2, t3, t4;
111
112 t1 = t2 = t3 = t4 = 0;
113
114 if (log.isDebugEnabled()) {
115 t1 = System.currentTimeMillis();
116 }
117
118 // Setup page info area
119 pageInfo = new PageInfo(new BeanRepository(ctxt.getClassLoader(),
120 errDispatcher), ctxt.getJspFile(), ctxt.isTagFile());
121
122 JspConfig jspConfig = options.getJspConfig();
123 JspConfig.JspProperty jspProperty = jspConfig.findJspProperty(ctxt
124 .getJspFile());
125
126 /*
127 * If the current uri is matched by a pattern specified in a
128 * jsp-property-group in web.xml, initialize pageInfo with those
129 * properties.
130 */
131 if (jspProperty.isELIgnored() != null) {
132 pageInfo.setELIgnored(JspUtil.booleanValue(jspProperty
133 .isELIgnored()));
134 }
135 if (jspProperty.isScriptingInvalid() != null) {
136 pageInfo.setScriptingInvalid(JspUtil.booleanValue(jspProperty
137 .isScriptingInvalid()));
138 }
139 if (jspProperty.getIncludePrelude() != null) {
140 pageInfo.setIncludePrelude(jspProperty.getIncludePrelude());
141 }
142 if (jspProperty.getIncludeCoda() != null) {
143 pageInfo.setIncludeCoda(jspProperty.getIncludeCoda());
144 }
145 if (jspProperty.isDeferedSyntaxAllowedAsLiteral() != null) {
146 pageInfo.setDeferredSyntaxAllowedAsLiteral(JspUtil.booleanValue(jspProperty
147 .isDeferedSyntaxAllowedAsLiteral()));
148 }
149 if (jspProperty.isTrimDirectiveWhitespaces() != null) {
150 pageInfo.setTrimDirectiveWhitespaces(JspUtil.booleanValue(jspProperty
151 .isTrimDirectiveWhitespaces()));
152 }
153 // Default ContentType processing is deferred until after the page has
154 // been parsed
155 if (jspProperty.getBuffer() != null) {
156 pageInfo.setBufferValue(jspProperty.getBuffer(), null,
157 errDispatcher);
158 }
159 if (jspProperty.isErrorOnUndeclaredNamespace() != null) {
160 pageInfo.setErrorOnUndeclaredNamespace(
161 JspUtil.booleanValue(
162 jspProperty.isErrorOnUndeclaredNamespace()));
163 }
164 if (ctxt.isTagFile()) {
165 try {
166 double libraryVersion = Double.parseDouble(ctxt.getTagInfo()
167 .getTagLibrary().getRequiredVersion());
168 if (libraryVersion < 2.0) {
169 pageInfo.setIsELIgnored("true", null, errDispatcher, true);
170 }
171 if (libraryVersion < 2.1) {
172 pageInfo.setDeferredSyntaxAllowedAsLiteral("true", null,
173 errDispatcher, true);
174 }
175 } catch (NumberFormatException ex) {
176 errDispatcher.jspError(ex);
177 }
178 }
179
180 ctxt.checkOutputDir();
181 String javaFileName = ctxt.getServletJavaFileName();
182
183 try {
184 /*
185 * The setting of isELIgnored changes the behaviour of the parser
186 * in subtle ways. To add to the 'fun', isELIgnored can be set in
187 * any file that forms part of the translation unit so setting it
188 * in a file included towards the end of the translation unit can
189 * change how the parser should have behaved when parsing content
190 * up to the point where isELIgnored was set. Arghh!
191 * Previous attempts to hack around this have only provided partial
192 * solutions. We now use two passes to parse the translation unit.
193 * The first just parses the directives and the second parses the
194 * whole translation unit once we know how isELIgnored has been set.
195 * TODO There are some possible optimisations of this process.
196 */
197 // Parse the file
198 ParserController parserCtl = new ParserController(ctxt, this);
199
200 // Pass 1 - the directives
201 Node.Nodes directives =
202 parserCtl.parseDirectives(ctxt.getJspFile());
203 Validator.validateDirectives(this, directives);
204
205 // Pass 2 - the whole translation unit
206 pageNodes = parserCtl.parse(ctxt.getJspFile());
207
208 // Leave this until now since it can only be set once - bug 49726
209 if (pageInfo.getContentType() == null &&
210 jspProperty.getDefaultContentType() != null) {
211 pageInfo.setContentType(jspProperty.getDefaultContentType());
212 }
213
214 if (ctxt.isPrototypeMode()) {
215 // generate prototype .java file for the tag file
216 try (ServletWriter writer = setupContextWriter(javaFileName)) {
217 Generator.generate(writer, this, pageNodes);
218 return null;
219 }
220 }
221
222 // Validate and process attributes - don't re-validate the
223 // directives we validated in pass 1
224 Validator.validateExDirectives(this, pageNodes);
225
226 if (log.isDebugEnabled()) {
227 t2 = System.currentTimeMillis();
228 }
229
230 // Collect page info
231 Collector.collect(this, pageNodes);
232
233 // Compile (if necessary) and load the tag files referenced in
234 // this compilation unit.
235 tfp = new TagFileProcessor();
236 tfp.loadTagFiles(this, pageNodes);
237
238 if (log.isDebugEnabled()) {
239 t3 = System.currentTimeMillis();
240 }
241
242 // Determine which custom tag needs to declare which scripting vars
243 ScriptingVariabler.set(pageNodes, errDispatcher);
244
245 // Optimizations by Tag Plugins
246 TagPluginManager tagPluginManager = options.getTagPluginManager();
247 tagPluginManager.apply(pageNodes, errDispatcher, pageInfo);
248
249 // Optimization: concatenate contiguous template texts.
250 TextOptimizer.concatenate(this, pageNodes);
251
252 // Generate static function mapper codes.
253 ELFunctionMapper.map(pageNodes);
254
255 // generate servlet .java file
256 try (ServletWriter writer = setupContextWriter(javaFileName)) {
257 Generator.generate(writer, this, pageNodes);
258 }
259
260 // The writer is only used during the compile, dereference
261 // it in the JspCompilationContext when done to allow it
262 // to be GC'd and save memory.
263 ctxt.setWriter(null);
264
265 if (log.isDebugEnabled()) {
266 t4 = System.currentTimeMillis();
267 log.debug("Generated " + javaFileName + " total=" + (t4 - t1)
268 + " generate=" + (t4 - t3) + " validate=" + (t2 - t1));
269 }
270
271 } catch (RuntimeException e) {
272 // Remove the generated .java file
273 File file = new File(javaFileName);
274 if (file.exists()) {
275 if (!file.delete()) {
276 log.warn(Localizer.getMessage(
277 "jsp.warning.compiler.javafile.delete.fail",
278 file.getAbsolutePath()));
279 }
280 }
281 throw e;
282 }
283
284 Map<String,SmapStratum> smaps = null;
285
286 // JSR45 Support
287 if (!options.isSmapSuppressed()) {
288 smaps = SmapUtil.generateSmap(ctxt, pageNodes);
289 // Add them to the web application wide cache for future lookup in
290 // error handling etc.
291 ctxt.getRuntimeContext().getSmaps().putAll(smaps);
292 }
293
294 // If any proto type .java and .class files was generated,
295 // the prototype .java may have been replaced by the current
296 // compilation (if the tag file is self referencing), but the
297 // .class file need to be removed, to make sure that javac would
298 // generate .class again from the new .java file just generated.
299 tfp.removeProtoTypeFiles(ctxt.getClassFileName());
300
301 return smaps;
302 }
303
304 private ServletWriter setupContextWriter(String javaFileName)
305 throws FileNotFoundException, JasperException {
306 ServletWriter writer;
307 // Setup the ServletWriter
308 String javaEncoding = ctxt.getOptions().getJavaEncoding();
309 OutputStreamWriter osw = null;
310
311 try {
312 osw = new OutputStreamWriter(
313 new FileOutputStream(javaFileName), javaEncoding);
314 } catch (UnsupportedEncodingException ex) {
315 errDispatcher.jspError("jsp.error.needAlternateJavaEncoding",
316 javaEncoding);
317 }
318
319 writer = new ServletWriter(new PrintWriter(osw));
320 ctxt.setWriter(writer);
321 return writer;
322 }
323
324 /**
325 * Servlet compilation. This compiles the generated sources into
326 * Servlets.
327 *
328 * @param smaps The source maps for the class(es) generated from the source
329 * file
330 *
331 * @throws FileNotFoundException Source files not found
332 * @throws JasperException Compilation error
333 * @throws Exception Some other error
334 */
335 protected abstract void generateClass(Map<String,SmapStratum> smaps)
336 throws FileNotFoundException, JasperException, Exception;
337
338 /**
339 * Compile the jsp file from the current engine context.
340 * @throws FileNotFoundException Source files not found
341 * @throws JasperException Compilation error
342 * @throws Exception Some other error
343 */
344 public void compile() throws FileNotFoundException, JasperException,
345 Exception {
346 compile(true);
347 }
348
349 /**
350 * Compile the jsp file from the current engine context. As an side- effect,
351 * tag files that are referenced by this page are also compiled.
352 *
353 * @param compileClass
354 * If true, generate both .java and .class file If false,
355 * generate only .java file
356 * @throws FileNotFoundException Source files not found
357 * @throws JasperException Compilation error
358 * @throws Exception Some other error
359 */
360 public void compile(boolean compileClass) throws FileNotFoundException,
361 JasperException, Exception {
362 compile(compileClass, false);
363 }
364
365 /**
366 * Compile the jsp file from the current engine context. As an side- effect,
367 * tag files that are referenced by this page are also compiled.
368 *
369 * @param compileClass
370 * If true, generate both .java and .class file If false,
371 * generate only .java file
372 * @param jspcMode
373 * true if invoked from JspC, false otherwise
374 * @throws FileNotFoundException Source files not found
375 * @throws JasperException Compilation error
376 * @throws Exception Some other error
377 */
378 public void compile(boolean compileClass, boolean jspcMode)
379 throws FileNotFoundException, JasperException, Exception {
380 if (errDispatcher == null) {
381 this.errDispatcher = new ErrorDispatcher(jspcMode);
382 }
383
384 try {
385 final Long jspLastModified = ctxt.getLastModified(ctxt.getJspFile());
386 Map<String,SmapStratum> smaps = generateJava();
387 File javaFile = new File(ctxt.getServletJavaFileName());
388 if (!javaFile.setLastModified(jspLastModified.longValue())) {
389 throw new JasperException(Localizer.getMessage("jsp.error.setLastModified", javaFile));
390 }
391 if (compileClass) {
392 generateClass(smaps);
393 // Fix for bugzilla 41606
394 // Set JspServletWrapper.servletClassLastModifiedTime after successful compile
395 File targetFile = new File(ctxt.getClassFileName());
396 if (targetFile.exists()) {
397 if (!targetFile.setLastModified(jspLastModified.longValue())) {
398 throw new JasperException(
399 Localizer.getMessage("jsp.error.setLastModified", targetFile));
400 }
401 if (jsw != null) {
402 jsw.setServletClassLastModifiedTime(
403 jspLastModified.longValue());
404 }
405 }
406 }
407 } finally {
408 if (tfp != null && ctxt.isPrototypeMode()) {
409 tfp.removeProtoTypeFiles(null);
410 }
411 // Make sure these object which are only used during the
412 // generation and compilation of the JSP page get
413 // dereferenced so that they can be GC'd and reduce the
414 // memory footprint.
415 tfp = null;
416 errDispatcher = null;
417 pageInfo = null;
418 pageNodes = null;
419
420 if (ctxt.getWriter() != null) {
421 ctxt.getWriter().close();
422 ctxt.setWriter(null);
423 }
424 }
425 }
426
427 /**
428 * This is a protected method intended to be overridden by subclasses of
429 * Compiler. This is used by the compile method to do all the compilation.
430 * @return <code>true</code> if the source generation and compilation
431 * should occur
432 */
433 public boolean isOutDated() {
434 return isOutDated(true);
435 }
436
437 /**
438 * Determine if a compilation is necessary by checking the time stamp of the
439 * JSP page with that of the corresponding .class or .java file. If the page
440 * has dependencies, the check is also extended to its dependents, and so
441 * on. This method can by overridden by a subclasses of Compiler.
442 *
443 * @param checkClass
444 * If true, check against .class file, if false, check against
445 * .java file.
446 * @return <code>true</code> if the source generation and compilation
447 * should occur
448 */
449 public boolean isOutDated(boolean checkClass) {
450
451 if (jsw != null
452 && (ctxt.getOptions().getModificationTestInterval() > 0)) {
453
454 if (jsw.getLastModificationTest()
455 + (ctxt.getOptions().getModificationTestInterval() * 1000) > System
456 .currentTimeMillis()) {
457 return false;
458 }
459 jsw.setLastModificationTest(System.currentTimeMillis());
460 }
461
462 // Test the target file first. Unless there is an error checking the
463 // last modified time of the source (unlikely) the target is going to
464 // have to be checked anyway. If the target doesn't exist (likely during
465 // startup) this saves an unnecessary check of the source.
466 File targetFile;
467 if (checkClass) {
468 targetFile = new File(ctxt.getClassFileName());
469 } else {
470 targetFile = new File(ctxt.getServletJavaFileName());
471 }
472 if (!targetFile.exists()) {
473 return true;
474 }
475 long targetLastModified = targetFile.lastModified();
476 if (checkClass && jsw != null) {
477 jsw.setServletClassLastModifiedTime(targetLastModified);
478 }
479
480 Long jspRealLastModified = ctxt.getLastModified(ctxt.getJspFile());
481 if (jspRealLastModified.longValue() < 0) {
482 // Something went wrong - assume modification
483 return true;
484 }
485
486 if (targetLastModified != jspRealLastModified.longValue()) {
487 if (log.isDebugEnabled()) {
488 log.debug("Compiler: outdated: " + targetFile + " "
489 + targetLastModified);
490 }
491 return true;
492 }
493
494 // determine if source dependent files (e.g. includes using include
495 // directives) have been changed.
496 if (jsw == null) {
497 return false;
498 }
499
500 Map<String,Long> depends = jsw.getDependants();
501 if (depends == null) {
502 return false;
503 }
504
505 for (Entry<String, Long> include : depends.entrySet()) {
506 try {
507 String key = include.getKey();
508 URL includeUrl;
509 long includeLastModified = 0;
510 if (key.startsWith("jar:jar:")) {
511 // Assume we constructed this correctly
512 int entryStart = key.lastIndexOf("!/");
513 String entry = key.substring(entryStart + 2);
514 try (Jar jar = JarFactory.newInstance(new URL(key.substring(4, entryStart)))) {
515 includeLastModified = jar.getLastModified(entry);
516 }
517 } else {
518 if (key.startsWith("jar:") || key.startsWith("file:")) {
519 includeUrl = new URL(key);
520 } else {
521 includeUrl = ctxt.getResource(include.getKey());
522 }
523 if (includeUrl == null) {
524 return true;
525 }
526 URLConnection iuc = includeUrl.openConnection();
527 if (iuc instanceof JarURLConnection) {
528 includeLastModified =
529 ((JarURLConnection) iuc).getJarEntry().getTime();
530 } else {
531 includeLastModified = iuc.getLastModified();
532 }
533 iuc.getInputStream().close();
534 }
535
536 if (includeLastModified != include.getValue().longValue()) {
537 return true;
538 }
539 } catch (Exception e) {
540 if (log.isDebugEnabled())
541 log.debug("Problem accessing resource. Treat as outdated.",
542 e);
543 return true;
544 }
545 }
546
547 return false;
548
549 }
550
551 /**
552 * @return the error dispatcher.
553 */
554 public ErrorDispatcher getErrorDispatcher() {
555 return errDispatcher;
556 }
557
558 /**
559 * @return the info about the page under compilation
560 */
561 public PageInfo getPageInfo() {
562 return pageInfo;
563 }
564
565 public JspCompilationContext getCompilationContext() {
566 return ctxt;
567 }
568
569 /**
570 * Remove generated files
571 */
572 public void removeGeneratedFiles() {
573 removeGeneratedClassFiles();
574
575 try {
576 File javaFile = new File(ctxt.getServletJavaFileName());
577 if (log.isDebugEnabled())
578 log.debug("Deleting " + javaFile);
579 if (javaFile.exists()) {
580 if (!javaFile.delete()) {
581 log.warn(Localizer.getMessage(
582 "jsp.warning.compiler.javafile.delete.fail",
583 javaFile.getAbsolutePath()));
584 }
585 }
586 } catch (Exception e) {
587 // Remove as much as possible, log possible exceptions
588 log.warn(Localizer.getMessage("jsp.warning.compiler.classfile.delete.fail.unknown"),
589 e);
590 }
591 }
592
593 public void removeGeneratedClassFiles() {
594 try {
595 File classFile = new File(ctxt.getClassFileName());
596 if (log.isDebugEnabled())
597 log.debug("Deleting " + classFile);
598 if (classFile.exists()) {
599 if (!classFile.delete()) {
600 log.warn(Localizer.getMessage(
601 "jsp.warning.compiler.classfile.delete.fail",
602 classFile.getAbsolutePath()));
603 }
604 }
605 } catch (Exception e) {
606 // Remove as much as possible, log possible exceptions
607 log.warn(Localizer.getMessage("jsp.warning.compiler.classfile.delete.fail.unknown"),
608 e);
609 }
610 }
611 }
612