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 package org.apache.jasper.compiler;
18
19 import java.io.BufferedOutputStream;
20 import java.io.BufferedReader;
21 import java.io.ByteArrayOutputStream;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileNotFoundException;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.InputStreamReader;
29 import java.io.Reader;
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Locale;
34 import java.util.Map;
35 import java.util.StringTokenizer;
36
37 import org.apache.jasper.JasperException;
38 import org.apache.juli.logging.Log;
39 import org.apache.juli.logging.LogFactory;
40 import org.eclipse.jdt.core.compiler.IProblem;
41 import org.eclipse.jdt.internal.compiler.ClassFile;
42 import org.eclipse.jdt.internal.compiler.CompilationResult;
43 import org.eclipse.jdt.internal.compiler.Compiler;
44 import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies;
45 import org.eclipse.jdt.internal.compiler.ICompilerRequestor;
46 import org.eclipse.jdt.internal.compiler.IErrorHandlingPolicy;
47 import org.eclipse.jdt.internal.compiler.IProblemFactory;
48 import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
49 import org.eclipse.jdt.internal.compiler.env.ICompilationUnit;
50 import org.eclipse.jdt.internal.compiler.env.INameEnvironment;
51 import org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer;
52 import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
53 import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
54
55 /**
56  * JDT class compiler. This compiler will load source dependencies from the
57  * context classloader, reducing dramatically disk access during
58  * the compilation process.
59  *
60  * Based on code from Cocoon2.
61  *
62  * @author Remy Maucherat
63  */

64 public class JDTCompiler extends org.apache.jasper.compiler.Compiler {
65
66     private final Log log = LogFactory.getLog(JDTCompiler.class); // must not be static
67
68     /**
69      * Compile the servlet from .java file to .class file
70      */

71     @Override
72     protected void generateClass(Map<String,SmapStratum> smaps)
73         throws FileNotFoundException, JasperException, Exception {
74
75         long t1 = 0;
76         if (log.isDebugEnabled()) {
77             t1 = System.currentTimeMillis();
78         }
79
80         final String sourceFile = ctxt.getServletJavaFileName();
81         final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
82         String packageName = ctxt.getServletPackageName();
83         final String targetClassName =
84             ((packageName.length() != 0) ? (packageName + ".") : "")
85                     + ctxt.getServletClassName();
86         final ClassLoader classLoader = ctxt.getJspLoader();
87         String[] fileNames = new String[] {sourceFile};
88         String[] classNames = new String[] {targetClassName};
89         final List<JavacErrorDetail> problemList = new ArrayList<>();
90
91         class CompilationUnit implements ICompilationUnit {
92
93             private final String className;
94             private final String sourceFile;
95
96             CompilationUnit(String sourceFile, String className) {
97                 this.className = className;
98                 this.sourceFile = sourceFile;
99             }
100
101             @Override
102             public char[] getFileName() {
103                 return sourceFile.toCharArray();
104             }
105
106             @Override
107             public char[] getContents() {
108                 char[] result = null;
109                 try (FileInputStream is = new FileInputStream(sourceFile);
110                         InputStreamReader isr = new InputStreamReader(
111                                 is, ctxt.getOptions().getJavaEncoding());
112                         Reader reader = new BufferedReader(isr)) {
113                     char[] chars = new char[8192];
114                     StringBuilder buf = new StringBuilder();
115                     int count;
116                     while ((count = reader.read(chars, 0,
117                                                 chars.length)) > 0) {
118                         buf.append(chars, 0, count);
119                     }
120                     result = new char[buf.length()];
121                     buf.getChars(0, result.length, result, 0);
122                 } catch (IOException e) {
123                     log.error(Localizer.getMessage("jsp.error.compilation.source", sourceFile), e);
124                 }
125                 return result;
126             }
127
128             @Override
129             public char[] getMainTypeName() {
130                 int dot = className.lastIndexOf('.');
131                 if (dot > 0) {
132                     return className.substring(dot + 1).toCharArray();
133                 }
134                 return className.toCharArray();
135             }
136
137             @Override
138             public char[][] getPackageName() {
139                 StringTokenizer izer =
140                     new StringTokenizer(className, ".");
141                 char[][] result = new char[izer.countTokens()-1][];
142                 for (int i = 0; i < result.length; i++) {
143                     String tok = izer.nextToken();
144                     result[i] = tok.toCharArray();
145                 }
146                 return result;
147             }
148
149             @Override
150             public boolean ignoreOptionalProblems() {
151                 return false;
152             }
153         }
154
155         final INameEnvironment env = new INameEnvironment() {
156
157                 @Override
158                 public NameEnvironmentAnswer
159                     findType(char[][] compoundTypeName) {
160                     StringBuilder result = new StringBuilder();
161                     for (int i = 0; i < compoundTypeName.length; i++) {
162                         if(i > 0)
163                             result.append('.');
164                         result.append(compoundTypeName[i]);
165                     }
166                     return findType(result.toString());
167                 }
168
169                 @Override
170                 public NameEnvironmentAnswer
171                     findType(char[] typeName,
172                              char[][] packageName) {
173                         StringBuilder result = new StringBuilder();
174                         int i=0;
175                         for (; i < packageName.length; i++) {
176                             if(i > 0)
177                                 result.append('.');
178                             result.append(packageName[i]);
179                         }
180                         if(i > 0)
181                             result.append('.');
182                         result.append(typeName);
183                         return findType(result.toString());
184                 }
185
186                 private NameEnvironmentAnswer findType(String className) {
187
188                     if (className.equals(targetClassName)) {
189                         ICompilationUnit compilationUnit =
190                             new CompilationUnit(sourceFile, className);
191                         return
192                             new NameEnvironmentAnswer(compilationUnit, null);
193                     }
194
195                     String resourceName =
196                             className.replace('.', '/') + ".class";
197
198                     try (InputStream is = classLoader.getResourceAsStream(resourceName)) {
199                         if (is != null) {
200                             byte[] classBytes;
201                             byte[] buf = new byte[8192];
202                             ByteArrayOutputStream baos =
203                                 new ByteArrayOutputStream(buf.length);
204                             int count;
205                             while ((count = is.read(buf, 0, buf.length)) > 0) {
206                                 baos.write(buf, 0, count);
207                             }
208                             baos.flush();
209                             classBytes = baos.toByteArray();
210                             char[] fileName = className.toCharArray();
211                             ClassFileReader classFileReader =
212                                 new ClassFileReader(classBytes, fileName,
213                                                     true);
214                             return
215                                 new NameEnvironmentAnswer(classFileReader, null);
216                         }
217                     } catch (IOException exc) {
218                         log.error(Localizer.getMessage("jsp.error.compilation.dependent", className), exc);
219                     } catch (org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException exc) {
220                         log.error(Localizer.getMessage("jsp.error.compilation.dependent", className), exc);
221                     }
222                     return null;
223                 }
224
225                 private boolean isPackage(String result) {
226                     if (result.equals(targetClassName)) {
227                         return false;
228                     }
229                     String resourceName = result.replace('.', '/') + ".class";
230                     try (InputStream is =
231                         classLoader.getResourceAsStream(resourceName)) {
232                         return is == null;
233                     } catch (IOException e) {
234                         // we are here, since close on is failed. That means it was not null
235                         return false;
236                     }
237                 }
238
239                 @Override
240                 public boolean isPackage(char[][] parentPackageName,
241                                          char[] packageName) {
242                     StringBuilder result = new StringBuilder();
243                     int i=0;
244                     if (parentPackageName != null) {
245                         for (; i < parentPackageName.length; i++) {
246                             if(i > 0)
247                                 result.append('.');
248                             result.append(parentPackageName[i]);
249                         }
250                     }
251
252                     if (Character.isUpperCase(packageName[0])) {
253                         if (!isPackage(result.toString())) {
254                             return false;
255                         }
256                     }
257                     if(i > 0)
258                         result.append('.');
259                     result.append(packageName);
260
261                     return isPackage(result.toString());
262                 }
263
264                 @Override
265                 public void cleanup() {
266                 }
267
268             };
269
270         final IErrorHandlingPolicy policy =
271             DefaultErrorHandlingPolicies.proceedWithAllProblems();
272
273         final Map<String,String> settings = new HashMap<>();
274         settings.put(CompilerOptions.OPTION_LineNumberAttribute,
275                      CompilerOptions.GENERATE);
276         settings.put(CompilerOptions.OPTION_SourceFileAttribute,
277                      CompilerOptions.GENERATE);
278         settings.put(CompilerOptions.OPTION_ReportDeprecation,
279                      CompilerOptions.IGNORE);
280         if (ctxt.getOptions().getJavaEncoding() != null) {
281             settings.put(CompilerOptions.OPTION_Encoding,
282                     ctxt.getOptions().getJavaEncoding());
283         }
284         if (ctxt.getOptions().getClassDebugInfo()) {
285             settings.put(CompilerOptions.OPTION_LocalVariableAttribute,
286                          CompilerOptions.GENERATE);
287         }
288
289         // Source JVM
290         if(ctxt.getOptions().getCompilerSourceVM() != null) {
291             String opt = ctxt.getOptions().getCompilerSourceVM();
292             if(opt.equals("1.1")) {
293                 settings.put(CompilerOptions.OPTION_Source,
294                              CompilerOptions.VERSION_1_1);
295             } else if(opt.equals("1.2")) {
296                 settings.put(CompilerOptions.OPTION_Source,
297                              CompilerOptions.VERSION_1_2);
298             } else if(opt.equals("1.3")) {
299                 settings.put(CompilerOptions.OPTION_Source,
300                              CompilerOptions.VERSION_1_3);
301             } else if(opt.equals("1.4")) {
302                 settings.put(CompilerOptions.OPTION_Source,
303                              CompilerOptions.VERSION_1_4);
304             } else if(opt.equals("1.5")) {
305                 settings.put(CompilerOptions.OPTION_Source,
306                              CompilerOptions.VERSION_1_5);
307             } else if(opt.equals("1.6")) {
308                 settings.put(CompilerOptions.OPTION_Source,
309                              CompilerOptions.VERSION_1_6);
310             } else if(opt.equals("1.7")) {
311                 settings.put(CompilerOptions.OPTION_Source,
312                              CompilerOptions.VERSION_1_7);
313             } else if(opt.equals("1.8")) {
314                 settings.put(CompilerOptions.OPTION_Source,
315                              CompilerOptions.VERSION_1_8);
316             // Version format changed from Java 9 onwards.
317             // Support old format that was used in EA implementation as well
318             } else if(opt.equals("9") || opt.equals("1.9")) {
319                 settings.put(CompilerOptions.OPTION_Source,
320                              CompilerOptions.VERSION_9);
321             } else if(opt.equals("10")) {
322                 settings.put(CompilerOptions.OPTION_Source,
323                              CompilerOptions.VERSION_10);
324             } else if(opt.equals("11")) {
325                 settings.put(CompilerOptions.OPTION_Source,
326                              CompilerOptions.VERSION_11);
327             } else if(opt.equals("12")) {
328                 settings.put(CompilerOptions.OPTION_Source,
329                              CompilerOptions.VERSION_12);
330             } else if(opt.equals("13")) {
331                 // Constant not available in latest ECJ version shipped with
332                 // Tomcat. May be supported in a snapshot build.
333                 // This is checked against the actual version below.
334                 settings.put(CompilerOptions.OPTION_Source, "13");
335             } else {
336                 log.warn(Localizer.getMessage("jsp.warning.unknown.sourceVM", opt));
337                 settings.put(CompilerOptions.OPTION_Source,
338                         CompilerOptions.VERSION_1_8);
339             }
340         } else {
341             // Default to 1.8
342             settings.put(CompilerOptions.OPTION_Source,
343                     CompilerOptions.VERSION_1_8);
344         }
345
346         // Target JVM
347         if(ctxt.getOptions().getCompilerTargetVM() != null) {
348             String opt = ctxt.getOptions().getCompilerTargetVM();
349             if(opt.equals("1.1")) {
350                 settings.put(CompilerOptions.OPTION_TargetPlatform,
351                              CompilerOptions.VERSION_1_1);
352             } else if(opt.equals("1.2")) {
353                 settings.put(CompilerOptions.OPTION_TargetPlatform,
354                              CompilerOptions.VERSION_1_2);
355             } else if(opt.equals("1.3")) {
356                 settings.put(CompilerOptions.OPTION_TargetPlatform,
357                              CompilerOptions.VERSION_1_3);
358             } else if(opt.equals("1.4")) {
359                 settings.put(CompilerOptions.OPTION_TargetPlatform,
360                              CompilerOptions.VERSION_1_4);
361             } else if(opt.equals("1.5")) {
362                 settings.put(CompilerOptions.OPTION_TargetPlatform,
363                              CompilerOptions.VERSION_1_5);
364                 settings.put(CompilerOptions.OPTION_Compliance,
365                         CompilerOptions.VERSION_1_5);
366             } else if(opt.equals("1.6")) {
367                 settings.put(CompilerOptions.OPTION_TargetPlatform,
368                              CompilerOptions.VERSION_1_6);
369                 settings.put(CompilerOptions.OPTION_Compliance,
370                         CompilerOptions.VERSION_1_6);
371             } else if(opt.equals("1.7")) {
372                 settings.put(CompilerOptions.OPTION_TargetPlatform,
373                              CompilerOptions.VERSION_1_7);
374                 settings.put(CompilerOptions.OPTION_Compliance,
375                         CompilerOptions.VERSION_1_7);
376             } else if(opt.equals("1.8")) {
377                 settings.put(CompilerOptions.OPTION_TargetPlatform,
378                              CompilerOptions.VERSION_1_8);
379                 settings.put(CompilerOptions.OPTION_Compliance,
380                         CompilerOptions.VERSION_1_8);
381             // Version format changed from Java 9 onwards.
382             // Support old format that was used in EA implementation as well
383             } else if(opt.equals("9") || opt.equals("1.9")) {
384                 settings.put(CompilerOptions.OPTION_TargetPlatform,
385                              CompilerOptions.VERSION_9);
386                 settings.put(CompilerOptions.OPTION_Compliance,
387                         CompilerOptions.VERSION_9);
388             } else if(opt.equals("10")) {
389                 settings.put(CompilerOptions.OPTION_TargetPlatform,
390                         CompilerOptions.VERSION_10);
391                 settings.put(CompilerOptions.OPTION_Compliance,
392                         CompilerOptions.VERSION_10);
393             } else if(opt.equals("11")) {
394                 settings.put(CompilerOptions.OPTION_TargetPlatform,
395                         CompilerOptions.VERSION_11);
396                 settings.put(CompilerOptions.OPTION_Compliance,
397                         CompilerOptions.VERSION_11);
398             } else if(opt.equals("12")) {
399                 settings.put(CompilerOptions.OPTION_TargetPlatform,
400                         CompilerOptions.VERSION_12);
401                 settings.put(CompilerOptions.OPTION_Compliance,
402                         CompilerOptions.VERSION_12);
403             } else if(opt.equals("13")) {
404                 // Constant not available in latest ECJ version shipped with
405                 // Tomcat. May be supported in a snapshot build.
406                 // This is checked against the actual version below.
407                 settings.put(CompilerOptions.OPTION_TargetPlatform, "13");
408                 settings.put(CompilerOptions.OPTION_Compliance, "13");
409             } else {
410                 log.warn(Localizer.getMessage("jsp.warning.unknown.targetVM", opt));
411                 settings.put(CompilerOptions.OPTION_TargetPlatform,
412                         CompilerOptions.VERSION_1_8);
413             }
414         } else {
415             // Default to 1.8
416             settings.put(CompilerOptions.OPTION_TargetPlatform,
417                     CompilerOptions.VERSION_1_8);
418             settings.put(CompilerOptions.OPTION_Compliance,
419                     CompilerOptions.VERSION_1_8);
420         }
421
422         final IProblemFactory problemFactory =
423             new DefaultProblemFactory(Locale.getDefault());
424
425         final ICompilerRequestor requestor = new ICompilerRequestor() {
426                 @Override
427                 public void acceptResult(CompilationResult result) {
428                     try {
429                         if (result.hasProblems()) {
430                             IProblem[] problems = result.getProblems();
431                             for (int i = 0; i < problems.length; i++) {
432                                 IProblem problem = problems[i];
433                                 if (problem.isError()) {
434                                     String name =
435                                         new String(problems[i].getOriginatingFileName());
436                                     try {
437                                         problemList.add(ErrorDispatcher.createJavacError
438                                                 (name, pageNodes, new StringBuilder(problem.getMessage()),
439                                                         problem.getSourceLineNumber(), ctxt));
440                                     } catch (JasperException e) {
441                                         log.error(Localizer.getMessage("jsp.error.compilation.jdtProblemError"), e);
442                                     }
443                                 }
444                             }
445                         }
446                         if (problemList.isEmpty()) {
447                             ClassFile[] classFiles = result.getClassFiles();
448                             for (int i = 0; i < classFiles.length; i++) {
449                                 ClassFile classFile = classFiles[i];
450                                 char[][] compoundName =
451                                     classFile.getCompoundName();
452                                 StringBuilder classFileName = new StringBuilder(outputDir).append('/');
453                                 for (int j = 0;
454                                      j < compoundName.length; j++) {
455                                     if(j > 0)
456                                         classFileName.append('/');
457                                     classFileName.append(compoundName[j]);
458                                 }
459                                 byte[] bytes = classFile.getBytes();
460                                 classFileName.append(".class");
461                                 try (FileOutputStream fout = new FileOutputStream(
462                                         classFileName.toString());
463                                         BufferedOutputStream bos = new BufferedOutputStream(fout)) {
464                                     bos.write(bytes);
465                                 }
466                             }
467                         }
468                     } catch (IOException exc) {
469                         log.error(Localizer.getMessage("jsp.error.compilation.jdt"), exc);
470                     }
471                 }
472             };
473
474         ICompilationUnit[] compilationUnits =
475             new ICompilationUnit[classNames.length];
476         for (int i = 0; i < compilationUnits.length; i++) {
477             String className = classNames[i];
478             compilationUnits[i] = new CompilationUnit(fileNames[i], className);
479         }
480         CompilerOptions cOptions = new CompilerOptions(settings);
481
482         // Check source/target JDK versions as the newest versions are allowed
483         // in Tomcat configuration but may not be supported by the ECJ version
484         // being used.
485         String requestedSource = ctxt.getOptions().getCompilerSourceVM();
486         if (requestedSource != null) {
487             String actualSource = CompilerOptions.versionFromJdkLevel(cOptions.sourceLevel);
488             if (!requestedSource.equals(actualSource)) {
489                 log.warn(Localizer.getMessage("jsp.warning.unsupported.sourceVM", requestedSource, actualSource));
490             }
491         }
492         String requestedTarget = ctxt.getOptions().getCompilerTargetVM();
493         if (requestedTarget != null) {
494             String actualTarget = CompilerOptions.versionFromJdkLevel(cOptions.targetJDK);
495             if (!requestedTarget.equals(actualTarget)) {
496                 log.warn(Localizer.getMessage("jsp.warning.unsupported.targetVM", requestedTarget, actualTarget));
497             }
498         }
499
500         cOptions.parseLiteralExpressionsAsConstants = true;
501         Compiler compiler = new Compiler(env,
502                                          policy,
503                                          cOptions,
504                                          requestor,
505                                          problemFactory);
506         compiler.compile(compilationUnits);
507
508         if (!ctxt.keepGenerated()) {
509             File javaFile = new File(ctxt.getServletJavaFileName());
510             if (!javaFile.delete()) {
511                 throw new JasperException(Localizer.getMessage(
512                         "jsp.warning.compiler.javafile.delete.fail", javaFile));
513             }
514         }
515
516         if (!problemList.isEmpty()) {
517             JavacErrorDetail[] jeds =
518                 problemList.toArray(new JavacErrorDetail[0]);
519             errDispatcher.javacError(jeds);
520         }
521
522         if( log.isDebugEnabled() ) {
523             long t2=System.currentTimeMillis();
524             log.debug("Compiled " + ctxt.getServletJavaFileName() + " "
525                       + (t2-t1) + "ms");
526         }
527
528         if (ctxt.isPrototypeMode()) {
529             return;
530         }
531
532         // JSR45 Support
533         if (! options.isSmapSuppressed()) {
534             SmapUtil.installSmap(smaps);
535         }
536     }
537 }
538