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.FileInputStream;
22 import java.io.FileNotFoundException;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStreamWriter;
26 import java.io.PrintWriter;
27 import java.io.UnsupportedEncodingException;
28 import java.util.HashMap;
29 import java.util.Iterator;
30 import java.util.Map;
31
32 import org.apache.jasper.JasperException;
33 import org.apache.jasper.JspCompilationContext;
34
35 /**
36 * Contains static utilities for generating SMAP data based on the
37 * current version of Jasper.
38 *
39 * @author Jayson Falkner
40 * @author Shawn Bayern
41 * @author Robert Field (inner SDEInstaller class)
42 * @author Mark Roth
43 * @author Kin-man Chung
44 */
45 public class SmapUtil {
46
47 private org.apache.juli.logging.Log log=
48 org.apache.juli.logging.LogFactory.getLog( SmapUtil.class );
49
50 //*********************************************************************
51 // Constants
52
53 public static final String SMAP_ENCODING = "UTF-8";
54
55 //*********************************************************************
56 // Public entry points
57
58 /**
59 * Generates an appropriate SMAP representing the current compilation
60 * context. (JSR-045.)
61 *
62 * @param ctxt Current compilation context
63 * @param pageNodes The current JSP page
64 * @return a SMAP for the page
65 */
66 public static String[] generateSmap(
67 JspCompilationContext ctxt,
68 Node.Nodes pageNodes)
69 throws IOException {
70
71 // Scan the nodes for presence of Jasper generated inner classes
72 PreScanVisitor psVisitor = new PreScanVisitor();
73 try {
74 pageNodes.visit(psVisitor);
75 } catch (JasperException ex) {
76 }
77 HashMap map = psVisitor.getMap();
78
79 // set up our SMAP generator
80 SmapGenerator g = new SmapGenerator();
81
82 /** Disable reading of input SMAP because:
83 1. There is a bug here: getRealPath() is null if .jsp is in a jar
84 Bugzilla 14660.
85 2. Mappings from other sources into .jsp files are not supported.
86 TODO: fix 1. if 2. is not true.
87 // determine if we have an input SMAP
88 String smapPath = inputSmapPath(ctxt.getRealPath(ctxt.getJspFile()));
89 File inputSmap = new File(smapPath);
90 if (inputSmap.exists()) {
91 byte[] embeddedSmap = null;
92 byte[] subSmap = SDEInstaller.readWhole(inputSmap);
93 String subSmapString = new String(subSmap, SMAP_ENCODING);
94 g.addSmap(subSmapString, "JSP");
95 }
96 **/
97
98 // now, assemble info about our own stratum (JSP) using JspLineMap
99 SmapStratum s = new SmapStratum("JSP");
100
101 g.setOutputFileName(unqualify(ctxt.getServletJavaFileName()));
102
103 // Map out Node.Nodes
104 evaluateNodes(pageNodes, s, map, ctxt.getOptions().getMappedFile());
105 s.optimizeLineSection();
106 g.addStratum(s, true);
107
108 if (ctxt.getOptions().isSmapDumped()) {
109 File outSmap = new File(ctxt.getClassFileName() + ".smap");
110 PrintWriter so =
111 new PrintWriter(
112 new OutputStreamWriter(
113 new FileOutputStream(outSmap),
114 SMAP_ENCODING));
115 so.print(g.getString());
116 so.close();
117 }
118
119 String classFileName = ctxt.getClassFileName();
120 int innerClassCount = map.size();
121 String [] smapInfo = new String[2 + innerClassCount*2];
122 smapInfo[0] = classFileName;
123 smapInfo[1] = g.getString();
124
125 int count = 2;
126 Iterator iter = map.entrySet().iterator();
127 while (iter.hasNext()) {
128 Map.Entry entry = (Map.Entry) iter.next();
129 String innerClass = (String) entry.getKey();
130 s = (SmapStratum) entry.getValue();
131 s.optimizeLineSection();
132 g = new SmapGenerator();
133 g.setOutputFileName(unqualify(ctxt.getServletJavaFileName()));
134 g.addStratum(s, true);
135
136 String innerClassFileName =
137 classFileName.substring(0, classFileName.indexOf(".class")) +
138 '$' + innerClass + ".class";
139 if (ctxt.getOptions().isSmapDumped()) {
140 File outSmap = new File(innerClassFileName + ".smap");
141 PrintWriter so =
142 new PrintWriter(
143 new OutputStreamWriter(
144 new FileOutputStream(outSmap),
145 SMAP_ENCODING));
146 so.print(g.getString());
147 so.close();
148 }
149 smapInfo[count] = innerClassFileName;
150 smapInfo[count+1] = g.getString();
151 count += 2;
152 }
153
154 return smapInfo;
155 }
156
157 public static void installSmap(String[] smap)
158 throws IOException {
159 if (smap == null) {
160 return;
161 }
162
163 for (int i = 0; i < smap.length; i += 2) {
164 File outServlet = new File(smap[i]);
165 SDEInstaller.install(outServlet, smap[i+1].getBytes());
166 }
167 }
168
169 //*********************************************************************
170 // Private utilities
171
172 /**
173 * Returns an unqualified version of the given file path.
174 */
175 private static String unqualify(String path) {
176 path = path.replace('\\', '/');
177 return path.substring(path.lastIndexOf('/') + 1);
178 }
179
180 /**
181 * Returns a file path corresponding to a potential SMAP input
182 * for the given compilation input (JSP file).
183 */
184 private static String inputSmapPath(String path) {
185 return path.substring(0, path.lastIndexOf('.') + 1) + "smap";
186 }
187
188 //*********************************************************************
189 // Installation logic (from Robert Field, JSR-045 spec lead)
190 private static class SDEInstaller {
191
192 private org.apache.juli.logging.Log log=
193 org.apache.juli.logging.LogFactory.getLog( SDEInstaller.class );
194
195 static final String nameSDE = "SourceDebugExtension";
196
197 byte[] orig;
198 byte[] sdeAttr;
199 byte[] gen;
200
201 int origPos = 0;
202 int genPos = 0;
203
204 int sdeIndex;
205
206 public static void main(String[] args) throws IOException {
207 if (args.length == 2) {
208 install(new File(args[0]), new File(args[1]));
209 } else if (args.length == 3) {
210 install(
211 new File(args[0]),
212 new File(args[1]),
213 new File(args[2]));
214 } else {
215 System.err.println(
216 "Usage: <command> <input class file> "
217 + "<attribute file> <output class file name>\n"
218 + "<command> <input/output class file> <attribute file>");
219 }
220 }
221
222 static void install(File inClassFile, File attrFile, File outClassFile)
223 throws IOException {
224 new SDEInstaller(inClassFile, attrFile, outClassFile);
225 }
226
227 static void install(File inOutClassFile, File attrFile)
228 throws IOException {
229 File tmpFile = new File(inOutClassFile.getPath() + "tmp");
230 new SDEInstaller(inOutClassFile, attrFile, tmpFile);
231 if (!inOutClassFile.delete()) {
232 throw new IOException("inOutClassFile.delete() failed");
233 }
234 if (!tmpFile.renameTo(inOutClassFile)) {
235 throw new IOException("tmpFile.renameTo(inOutClassFile) failed");
236 }
237 }
238
239 static void install(File classFile, byte[] smap) throws IOException {
240 File tmpFile = new File(classFile.getPath() + "tmp");
241 new SDEInstaller(classFile, smap, tmpFile);
242 if (!classFile.delete()) {
243 throw new IOException("classFile.delete() failed");
244 }
245 if (!tmpFile.renameTo(classFile)) {
246 throw new IOException("tmpFile.renameTo(classFile) failed");
247 }
248 }
249
250 SDEInstaller(File inClassFile, byte[] sdeAttr, File outClassFile)
251 throws IOException {
252 if (!inClassFile.exists()) {
253 throw new FileNotFoundException("no such file: " + inClassFile);
254 }
255
256 this.sdeAttr = sdeAttr;
257 // get the bytes
258 orig = readWhole(inClassFile);
259 gen = new byte[orig.length + sdeAttr.length + 100];
260
261 // do it
262 addSDE();
263
264 // write result
265 FileOutputStream outStream = new FileOutputStream(outClassFile);
266 outStream.write(gen, 0, genPos);
267 outStream.close();
268 }
269
270 SDEInstaller(File inClassFile, File attrFile, File outClassFile)
271 throws IOException {
272 this(inClassFile, readWhole(attrFile), outClassFile);
273 }
274
275 static byte[] readWhole(File input) throws IOException {
276 FileInputStream inStream = new FileInputStream(input);
277 int len = (int)input.length();
278 byte[] bytes = new byte[len];
279 if (inStream.read(bytes, 0, len) != len) {
280 throw new IOException("expected size: " + len);
281 }
282 inStream.close();
283 return bytes;
284 }
285
286 void addSDE() throws UnsupportedEncodingException, IOException {
287 int i;
288 copy(4 + 2 + 2); // magic min/maj version
289 int constantPoolCountPos = genPos;
290 int constantPoolCount = readU2();
291 if (log.isDebugEnabled())
292 log.debug("constant pool count: " + constantPoolCount);
293 writeU2(constantPoolCount);
294
295 // copy old constant pool return index of SDE symbol, if found
296 sdeIndex = copyConstantPool(constantPoolCount);
297 if (sdeIndex < 0) {
298 // if "SourceDebugExtension" symbol not there add it
299 writeUtf8ForSDE();
300
301 // increment the countantPoolCount
302 sdeIndex = constantPoolCount;
303 ++constantPoolCount;
304 randomAccessWriteU2(constantPoolCountPos, constantPoolCount);
305
306 if (log.isDebugEnabled())
307 log.debug("SourceDebugExtension not found, installed at: " + sdeIndex);
308 } else {
309 if (log.isDebugEnabled())
310 log.debug("SourceDebugExtension found at: " + sdeIndex);
311 }
312 copy(2 + 2 + 2); // access, this, super
313 int interfaceCount = readU2();
314 writeU2(interfaceCount);
315 if (log.isDebugEnabled())
316 log.debug("interfaceCount: " + interfaceCount);
317 copy(interfaceCount * 2);
318 copyMembers(); // fields
319 copyMembers(); // methods
320 int attrCountPos = genPos;
321 int attrCount = readU2();
322 writeU2(attrCount);
323 if (log.isDebugEnabled())
324 log.debug("class attrCount: " + attrCount);
325 // copy the class attributes, return true if SDE attr found (not copied)
326 if (!copyAttrs(attrCount)) {
327 // we will be adding SDE and it isn't already counted
328 ++attrCount;
329 randomAccessWriteU2(attrCountPos, attrCount);
330 if (log.isDebugEnabled())
331 log.debug("class attrCount incremented");
332 }
333 writeAttrForSDE(sdeIndex);
334 }
335
336 void copyMembers() {
337 int count = readU2();
338 writeU2(count);
339 if (log.isDebugEnabled())
340 log.debug("members count: " + count);
341 for (int i = 0; i < count; ++i) {
342 copy(6); // access, name, descriptor
343 int attrCount = readU2();
344 writeU2(attrCount);
345 if (log.isDebugEnabled())
346 log.debug("member attr count: " + attrCount);
347 copyAttrs(attrCount);
348 }
349 }
350
351 boolean copyAttrs(int attrCount) {
352 boolean sdeFound = false;
353 for (int i = 0; i < attrCount; ++i) {
354 int nameIndex = readU2();
355 // don't write old SDE
356 if (nameIndex == sdeIndex) {
357 sdeFound = true;
358 if (log.isDebugEnabled())
359 log.debug("SDE attr found");
360 } else {
361 writeU2(nameIndex); // name
362 int len = readU4();
363 writeU4(len);
364 copy(len);
365 if (log.isDebugEnabled())
366 log.debug("attr len: " + len);
367 }
368 }
369 return sdeFound;
370 }
371
372 void writeAttrForSDE(int index) {
373 writeU2(index);
374 writeU4(sdeAttr.length);
375 for (int i = 0; i < sdeAttr.length; ++i) {
376 writeU1(sdeAttr[i]);
377 }
378 }
379
380 void randomAccessWriteU2(int pos, int val) {
381 int savePos = genPos;
382 genPos = pos;
383 writeU2(val);
384 genPos = savePos;
385 }
386
387 int readU1() {
388 return ((int)orig[origPos++]) & 0xFF;
389 }
390
391 int readU2() {
392 int res = readU1();
393 return (res << 8) + readU1();
394 }
395
396 int readU4() {
397 int res = readU2();
398 return (res << 16) + readU2();
399 }
400
401 void writeU1(int val) {
402 gen[genPos++] = (byte)val;
403 }
404
405 void writeU2(int val) {
406 writeU1(val >> 8);
407 writeU1(val & 0xFF);
408 }
409
410 void writeU4(int val) {
411 writeU2(val >> 16);
412 writeU2(val & 0xFFFF);
413 }
414
415 void copy(int count) {
416 for (int i = 0; i < count; ++i) {
417 gen[genPos++] = orig[origPos++];
418 }
419 }
420
421 byte[] readBytes(int count) {
422 byte[] bytes = new byte[count];
423 for (int i = 0; i < count; ++i) {
424 bytes[i] = orig[origPos++];
425 }
426 return bytes;
427 }
428
429 void writeBytes(byte[] bytes) {
430 for (int i = 0; i < bytes.length; ++i) {
431 gen[genPos++] = bytes[i];
432 }
433 }
434
435 int copyConstantPool(int constantPoolCount)
436 throws UnsupportedEncodingException, IOException {
437 int sdeIndex = -1;
438 // copy const pool index zero not in class file
439 for (int i = 1; i < constantPoolCount; ++i) {
440 int tag = readU1();
441 writeU1(tag);
442 switch (tag) {
443 case 7 : // Class
444 case 8 : // String
445 if (log.isDebugEnabled())
446 log.debug(i + " copying 2 bytes");
447 copy(2);
448 break;
449 case 9 : // Field
450 case 10 : // Method
451 case 11 : // InterfaceMethod
452 case 3 : // Integer
453 case 4 : // Float
454 case 12 : // NameAndType
455 if (log.isDebugEnabled())
456 log.debug(i + " copying 4 bytes");
457 copy(4);
458 break;
459 case 5 : // Long
460 case 6 : // Double
461 if (log.isDebugEnabled())
462 log.debug(i + " copying 8 bytes");
463 copy(8);
464 i++;
465 break;
466 case 1 : // Utf8
467 int len = readU2();
468 writeU2(len);
469 byte[] utf8 = readBytes(len);
470 String str = new String(utf8, "UTF-8");
471 if (log.isDebugEnabled())
472 log.debug(i + " read class attr -- '" + str + "'");
473 if (str.equals(nameSDE)) {
474 sdeIndex = i;
475 }
476 writeBytes(utf8);
477 break;
478 default :
479 throw new IOException("unexpected tag: " + tag);
480 }
481 }
482 return sdeIndex;
483 }
484
485 void writeUtf8ForSDE() {
486 int len = nameSDE.length();
487 writeU1(1); // Utf8 tag
488 writeU2(len);
489 for (int i = 0; i < len; ++i) {
490 writeU1(nameSDE.charAt(i));
491 }
492 }
493 }
494
495 public static void evaluateNodes(
496 Node.Nodes nodes,
497 SmapStratum s,
498 HashMap innerClassMap,
499 boolean breakAtLF) {
500 try {
501 nodes.visit(new SmapGenVisitor(s, breakAtLF, innerClassMap));
502 } catch (JasperException ex) {
503 }
504 }
505
506 static class SmapGenVisitor extends Node.Visitor {
507
508 private SmapStratum smap;
509 private boolean breakAtLF;
510 private HashMap innerClassMap;
511
512 SmapGenVisitor(SmapStratum s, boolean breakAtLF, HashMap map) {
513 this.smap = s;
514 this.breakAtLF = breakAtLF;
515 this.innerClassMap = map;
516 }
517
518 public void visitBody(Node n) throws JasperException {
519 SmapStratum smapSave = smap;
520 String innerClass = n.getInnerClassName();
521 if (innerClass != null) {
522 this.smap = (SmapStratum) innerClassMap.get(innerClass);
523 }
524 super.visitBody(n);
525 smap = smapSave;
526 }
527
528 public void visit(Node.Declaration n) throws JasperException {
529 doSmapText(n);
530 }
531
532 public void visit(Node.Expression n) throws JasperException {
533 doSmapText(n);
534 }
535
536 public void visit(Node.Scriptlet n) throws JasperException {
537 doSmapText(n);
538 }
539
540 public void visit(Node.IncludeAction n) throws JasperException {
541 doSmap(n);
542 visitBody(n);
543 }
544
545 public void visit(Node.ForwardAction n) throws JasperException {
546 doSmap(n);
547 visitBody(n);
548 }
549
550 public void visit(Node.GetProperty n) throws JasperException {
551 doSmap(n);
552 visitBody(n);
553 }
554
555 public void visit(Node.SetProperty n) throws JasperException {
556 doSmap(n);
557 visitBody(n);
558 }
559
560 public void visit(Node.UseBean n) throws JasperException {
561 doSmap(n);
562 visitBody(n);
563 }
564
565 public void visit(Node.PlugIn n) throws JasperException {
566 doSmap(n);
567 visitBody(n);
568 }
569
570 public void visit(Node.CustomTag n) throws JasperException {
571 doSmap(n);
572 visitBody(n);
573 }
574
575 public void visit(Node.UninterpretedTag n) throws JasperException {
576 doSmap(n);
577 visitBody(n);
578 }
579
580 public void visit(Node.JspElement n) throws JasperException {
581 doSmap(n);
582 visitBody(n);
583 }
584
585 public void visit(Node.JspText n) throws JasperException {
586 doSmap(n);
587 visitBody(n);
588 }
589
590 public void visit(Node.NamedAttribute n) throws JasperException {
591 visitBody(n);
592 }
593
594 public void visit(Node.JspBody n) throws JasperException {
595 doSmap(n);
596 visitBody(n);
597 }
598
599 public void visit(Node.InvokeAction n) throws JasperException {
600 doSmap(n);
601 visitBody(n);
602 }
603
604 public void visit(Node.DoBodyAction n) throws JasperException {
605 doSmap(n);
606 visitBody(n);
607 }
608
609 public void visit(Node.ELExpression n) throws JasperException {
610 doSmap(n);
611 }
612
613 public void visit(Node.TemplateText n) throws JasperException {
614 Mark mark = n.getStart();
615 if (mark == null) {
616 return;
617 }
618
619 //Add the file information
620 String fileName = mark.getFile();
621 smap.addFile(unqualify(fileName), fileName);
622
623 //Add a LineInfo that corresponds to the beginning of this node
624 int iInputStartLine = mark.getLineNumber();
625 int iOutputStartLine = n.getBeginJavaLine();
626 int iOutputLineIncrement = breakAtLF? 1: 0;
627 smap.addLineData(iInputStartLine, fileName, 1, iOutputStartLine,
628 iOutputLineIncrement);
629
630 // Output additional mappings in the text
631 java.util.ArrayList extraSmap = n.getExtraSmap();
632
633 if (extraSmap != null) {
634 for (int i = 0; i < extraSmap.size(); i++) {
635 iOutputStartLine += iOutputLineIncrement;
636 smap.addLineData(
637 iInputStartLine+((Integer)extraSmap.get(i)).intValue(),
638 fileName,
639 1,
640 iOutputStartLine,
641 iOutputLineIncrement);
642 }
643 }
644 }
645
646 private void doSmap(
647 Node n,
648 int inLineCount,
649 int outIncrement,
650 int skippedLines) {
651 Mark mark = n.getStart();
652 if (mark == null) {
653 return;
654 }
655
656 String unqualifiedName = unqualify(mark.getFile());
657 smap.addFile(unqualifiedName, mark.getFile());
658 smap.addLineData(
659 mark.getLineNumber() + skippedLines,
660 mark.getFile(),
661 inLineCount - skippedLines,
662 n.getBeginJavaLine() + skippedLines,
663 outIncrement);
664 }
665
666 private void doSmap(Node n) {
667 doSmap(n, 1, n.getEndJavaLine() - n.getBeginJavaLine(), 0);
668 }
669
670 private void doSmapText(Node n) {
671 String text = n.getText();
672 int index = 0;
673 int next = 0;
674 int lineCount = 1;
675 int skippedLines = 0;
676 boolean slashStarSeen = false;
677 boolean beginning = true;
678
679 // Count lines inside text, but skipping comment lines at the
680 // beginning of the text.
681 while ((next = text.indexOf('\n', index)) > -1) {
682 if (beginning) {
683 String line = text.substring(index, next).trim();
684 if (!slashStarSeen && line.startsWith("/*")) {
685 slashStarSeen = true;
686 }
687 if (slashStarSeen) {
688 skippedLines++;
689 int endIndex = line.indexOf("*/");
690 if (endIndex >= 0) {
691 // End of /* */ comment
692 slashStarSeen = false;
693 if (endIndex < line.length() - 2) {
694 // Some executable code after comment
695 skippedLines--;
696 beginning = false;
697 }
698 }
699 } else if (line.length() == 0 || line.startsWith("//")) {
700 skippedLines++;
701 } else {
702 beginning = false;
703 }
704 }
705 lineCount++;
706 index = next + 1;
707 }
708
709 doSmap(n, lineCount, 1, skippedLines);
710 }
711 }
712
713 private static class PreScanVisitor extends Node.Visitor {
714
715 HashMap map = new HashMap();
716
717 public void doVisit(Node n) {
718 String inner = n.getInnerClassName();
719 if (inner != null && !map.containsKey(inner)) {
720 map.put(inner, new SmapStratum("JSP"));
721 }
722 }
723
724 HashMap getMap() {
725 return map;
726 }
727 }
728
729 }