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