001 package fc.web.page;
002
003 import java.io.*;
004 import java.util.*;
005 import java.util.regex.*;
006 import fc.io.*;
007 import fc.util.*;
008
009 //Code blocks of the form
010 // [...]
011 //cause problems with java arrays
012 //String[] or foo[4] etc.., craps out. So we need to use
013 // [[...]]
014 //for the molly code blocks
015
016 // 1. If you are hacking this file, start with parseText()
017 //
018 // 2. Turn the dbg flag to true to see how the parser works
019 //
020 // 3. Keep in mind that the order of switch'es in a case statement in various
021 // methods is not always arbitrary (the order matters in this sort
022 // of recursive descent parsing)
023 //
024 // 4. Read www.mollypages.org/page/grammar/index.mp for a intro
025 // to parsing
026 //
027 // 5. This parser as shipped has a set of regression tests in the
028 // fc/web/page/test directory. These consist of a bunch of *.mp
029 // files and corresponding *.java files, each of which is known
030 // to be generated properly. If you change stuff around, run these
031 // regression tests again by invoking "java fc.web.page.PageParserTest"
032 // Note, if you change things such that the .java output of the parser
033 // is different, then the tests will fail (since the new .java files
034 // of your parser will be different to the test ones shipped in
035 // fc/web/page/test. In this case, once you know that your parser works
036 // as you like it, then you should create a new baseline for your parser
037 // by invoking "java fc.web.page.PageParserTest -generateExpected" and
038 // then you can use *that* as the new baseline for further changes in
039 // your parser (you may have to modify the *.mp files in /fc/web/page/test
040 // to use your new page syntax).
041
042 /**
043 Parses a page and writes out the corresponding java file to the specified
044 output. The parser and scanner is combined into one class here for
045 simplicity (a seperate scanner is overkill for a simple LL(1) grammar
046 such as molly pages).
047
048 @author hursh jain
049 */
050 public final class PageParser
051 {
052 private static final boolean dbg = false;
053 private static final int EOF = -1;
054 private int dbgtab = 0;
055
056 String classname;
057 String packagename = Page.PACKAGE_NAME;
058 PageReader in;
059 PrintWriter out;
060 Log log;
061 File inputFile;
062 File outputFile;
063 File contextRoot;
064 boolean includeMode = false;
065 String src_encoding;
066
067 //Read data
068 //we use these since stringbuffer/builders do not have a clear/reset function
069 CharArrayWriter buf = new CharArrayWriter(4096);
070 CharArrayWriter wsbuf = new CharArrayWriter(32); // ^(whitespace)*
071 int c = EOF;
072 //PageData
073 List decl = new ArrayList(); //declarations
074 List inc_decl = new ArrayList(); //external included declarations
075 List imps = new ArrayList(); //imports
076 List tree = new ArrayList(); //code, exp, text etc.
077 Map directives = new HashMap(); //page options
078
079 /**
080 The name ("mimetype") of the[@ mimetype=....] directive. The value of
081 <tt>none</tt> or an empty string will turn off writing any mimetype
082 entirely (the user can then write a mimetype via the {@link
083 javax.servlet.ServletResponse.setContentType} method manually).
084 <p>
085 Note, from {@link
086 javax.servlet.ServletResponse.setContentType ServletResponse}
087 <pre>
088 Note that the character encoding cannot be communicated via HTTP headers
089 if the servlet does not specify a content type; however, it is still used
090 to encode text written via the servlet response's writer.
091 </pre>
092 */
093 public static String d_mimetype = "mimetype";
094
095 /*
096 this value (or an empty string) for mimetype means no mimetype
097 will be specified (not even the default mimetype)
098 */
099 public static String mimetype_none = "none";
100
101 /**
102 The name ("encoding") of the [page encoding=....] directive.
103 */
104 public static String d_encoding = "encoding";
105
106 /**
107 The name ("src-encoding") of the [page src-encoding=....] directive.
108 */
109 public static String d_src_encoding = "src-encoding";
110
111 /** The name ("buffersize") of the [page buffersize=....] directive */
112 public static String d_buffersize = "buffersize";
113
114 /** The name ("out") of the [page out=....] directive */
115 public static String d_out = "out";
116 /** A value ("outputstream") of the [page out=outputstream] directive */
117 public static String d_out_stream1 = "outputstream";
118 /** A value ("outputstream") of the [page out=stream] directive */
119 public static String d_out_stream2 = "stream";
120 /** A value ("writer") of the [page out=writer] directive */
121 public static String d_out_writer = "writer";
122
123 /*
124 This constructor for internal use.
125
126 The parser can be invoked recursively to parse included files as
127 well..that's what the includeMode() does (and this construtor is invoked
128 when including). When including, we already have a output writer
129 created, we use that writer (instead of creating a new one based on
130 src_encoding as we do for in normal page parsing mode).
131 */
132 private PageParser(
133 File contextRoot, File input, PrintWriter outputWriter, String classname, Log log)
134 throws IOException
135 {
136 this.contextRoot = contextRoot;
137 this.inputFile = input;
138 this.in = new PageReader(input);
139 this.out = outputWriter;
140 this.classname = classname;
141 this.log = log;
142 }
143
144 /**
145 Creates a new page parser that will use the default log obtained by
146 {@link Log#getDefault}
147
148 @param contextRoot absolute path to the webapp context root directory
149 @param input absolute path to the input page file
150 @param input absolute path to the output file (to be written to).
151 @param classname classname to give to the generated java class.
152 */
153 public PageParser(File contextRoot, File input, File output, String classname)
154 throws IOException
155 {
156 this(contextRoot, input, output, classname, Log.getDefault());
157 }
158
159 /**
160 Creates a new page parser.
161
162 @param contextRoot absolute path to the webapp context root directory
163 @param input absolute path to the input page file
164 @param output absolute path to the output file (to be written to).
165 @param classname classname to give to the generated java class.
166 @log log destination for internal logging output.
167 */
168 public PageParser(
169 File contextRoot, File input, File output, String classname, Log log)
170 throws IOException
171 {
172 this.contextRoot = contextRoot;
173 this.inputFile = input;
174 this.in = new PageReader(input);
175 this.outputFile = output;
176 this.classname = classname;
177 this.log = log;
178 }
179
180 void append(final int c)
181 {
182 Argcheck.istrue(c >= 0, "Internal error: recieved c=" + c);
183 buf.append((char)c);
184 }
185
186 void append(final char c)
187 {
188 buf.append(c);
189 }
190
191 void append(final String str)
192 {
193 buf.append(str);
194 }
195
196 PageParser includeMode()
197 {
198 includeMode = true;
199 return this;
200 }
201
202 /**
203 Parses the page. If the parse is successful, the java source will be
204 generated.
205
206 @throws IOException a parse failure occurred. The java source file
207 may or may not be properly generated or written
208 in this case.
209 */
210 public void parse() throws IOException
211 {
212 parseText();
213 writePage();
214 if (! includeMode) {
215 out.close();
216 }
217 out.flush();
218 }
219
220 //util method for use in the case '[' branch of parseText below.
221 private Text newTextNode()
222 {
223 Text text = new Text(buf);
224 tree.add(text);
225 buf.reset();
226 return text;
227 }
228
229 void parseText() throws IOException
230 {
231 if (dbg) dbgenter();
232
233 while (true)
234 {
235 c = in.read();
236
237 if (c == EOF) {
238 tree.add(new Text(buf));
239 buf.reset();
240 break;
241 }
242
243 switch (c)
244 {
245 //Escape start tags
246 case '\\':
247 /* we don't need to do this: previously, expressions
248 were [...] but now they are [=...], previously we needed
249 to escape \[[ entirely (since if we escaped \[ the second
250 [ would start an expression
251 */
252 /*
253 if (in.match("[["))
254 append("[[");
255 */
256 //escape only \[... otherwise leave \ alone
257 if (in.match("["))
258 append("[");
259 else
260 append(c);
261 break;
262
263 case '[':
264 /* suppose we have
265 \[[
266 escape handling above will capture \[
267 then the second '[' drops down here. Good so far.
268 But we must not create a new text object here by
269 default...only if we see another [[ or [= or [include or
270 whatever.
271 */
272 /*
273 But creating a text object at the top is easier
274 then repeating this code at every if..else branch below
275 but this creates superfluous line breaks.
276
277 hello[haha]world
278 -->prints as-->
279 hello (text node 1)
280 [haha] (text node 2)
281 world (text node 3)
282 --> we want
283 hello[haha]world (text node 1)
284 */
285
286 if (in.match('[')) {
287 newTextNode();
288 parseCode();
289 }
290 else if (in.match('=')) {
291 Text text = newTextNode();
292 parseExpression(text);
293 }
294 else if (in.match('!')) {
295 newTextNode();
296 parseDeclaration();
297 }
298 else if (in.match("/*")) {
299 newTextNode();
300 parseComment();
301 }
302 else if (in.matchIgnoreCase("page")) {
303 newTextNode();
304 parseDirective();
305 }
306 //longest match: "include-file" etc., last: "include"
307 else if (in.matchIgnoreCase("include-file")) {
308 newTextNode();
309 parseIncludeFile();
310 }
311 else if (in.matchIgnoreCase("include-decl")) {
312 newTextNode();
313 parseIncludeDecl();
314 }
315 else if (in.matchIgnoreCase("include")) {
316 newTextNode();
317 parseInclude();
318 }
319 else if (in.matchIgnoreCase("forward")) {
320 newTextNode();
321 parseForward();
322 }
323 else if (in.matchIgnoreCase("import")) {
324 newTextNode();
325 parseImport();
326 }
327 else {
328 //System.out.println("c1=" + (char)c);
329 append(c);
330 }
331 break;
332
333 default:
334 //System.out.println("c2=" + (char)c);
335 append(c);
336
337 } //switch
338 } //while
339
340 if (dbg) dbgexit();
341 }
342
343 void parseCode() throws IOException
344 {
345 if (dbg) dbgenter();
346
347 int startline = in.getLine();
348 int startcol = in.getCol();
349
350 while (true)
351 {
352 c = in.read();
353
354 switch (c) /* the order of case tags is important. */
355 {
356 case EOF:
357 unclosed("code", startline, startcol);
358 if (dbg) dbgexit();
359 return;
360
361 case '/': //Top level: // and /* comments
362 append(c);
363 c = in.read();
364 append(c);
365 if (c == '/')
366 appendCodeSlashComment();
367 else if (c == '*')
368 appendCodeStarComment();
369 break;
370
371 case '"': //strings outside of any comment
372 append(c);
373 appendCodeString();
374 break;
375
376 case '\'':
377 append(c);
378 appendCodeCharLiteral();
379 break;
380
381 case ']':
382 if (in.match(']')) {
383 tree.add(new Code(buf));
384 buf.reset();
385 if (dbg) dbgexit();
386 return;
387 }
388 else {
389 append(c);
390 }
391 break;
392
393 /*
394 a hash by itself on a line starts a hash section.
395 whitespace before the # on that line is used as an
396 printing 'out' statements for that hash.
397
398 for (int n = 0; n < ; n++) {
399 ....# foo #
400 | }
401 |=> 4 spaces
402 so nice if generated code looked like:
403
404 for (int n = 0; n < ; n++) {
405 out.print(" foo ");
406 }
407 */
408 case '\n':
409 case '\r':
410 append(c); //the \n or \r just read
411 readToFirstNonWS(); //won't read past more newlines
412 //is '#' is first non-ws on this line ?
413 c = in.read();
414 if (c == '#') {
415 tree.add(new Code(buf));
416 buf.reset();
417 //whitespace provides indentation offset
418 parseHash(wsbuf.toString());
419 }
420 else{
421 append(wsbuf.toString()); //wsbuf contains codetext
422 //let other cases also handle first non-ws or EOF
423 in.unread();
424 }
425 break;
426
427 /* in this case, hash does not start on a new line, like:
428 for (...) { #
429 */
430 case '#':
431 tree.add(new Code(buf));
432 buf.reset();
433 parseHash(null);
434 break;
435
436 default:
437 append(c);
438 } //switch
439 } //while
440 }
441
442 void parseHash(String offset) throws IOException
443 {
444 if (dbg) dbgenter();
445
446 int startline = in.getLine();
447 int startcol = in.getCol();
448
449 while (true)
450 {
451 c = in.read();
452
453 switch (c)
454 {
455 case EOF:
456 unclosed("hash", startline, startcol);
457 if (dbg) dbgexit();
458 return;
459
460 //special case: very common and would be a drag to escape
461 //this every time:
462 // # <table bgcolor="#ffffff">.... #
463 //Now, all of:
464 // bgcolor="#xxx"
465 // bgcolor='#xxx'
466 // bgcolor="\#xxx"
467 //will work the same and give: bgcolor="#xxx"
468 //1)
469 //However to get a:
470 // bgcolor=#xxx (no quoted around #xxx)
471 //we still have to say:
472 // bgcolor=\#xxx
473 //2)
474 //Of course, since we special case this, then:
475 // #"bar"#
476 // that ending # is lost and we end up with
477 // #"bar" with no closing hash
478 //So we need to make sure that we write:
479 // #"bar" #
480 // instead
481
482 case '\'':
483 case '"':
484 append(c);
485 if (in.match('#'))
486 append('#');
487 break;
488
489 case '\\':
490 if (in.match('['))
491 append('[');
492 else if (in.match('#'))
493 append('#');
494 else
495 append(c);
496 break;
497
498 case '[':
499 if (in.match('=')) {
500 Hash hash = new Hash(offset, buf);
501 tree.add(hash);
502 buf.reset();
503 parseExpression(hash);
504 }
505 else{
506 append(c);
507 }
508 break;
509
510 /*
511 this case is not needed but is a bit of a optimization
512 for (int n = 0; n < 1; n++) {
513 #
514 foo
515 ....#...NL
516 }
517 avoids printing the dots (spaces) and NL in this case
518 (the newline after foo is still printed)
519 */
520 case '\n':
521 case '\r':
522 append(c);
523 readToFirstNonWS();
524 c = in.read();
525 //'#' is first non-ws on the line
526 if (c == '#') {
527 tree.add(new Hash(offset, buf));
528 buf.reset();
529 //skipIfWhitespaceToEnd();
530 if (dbg) dbgexit();
531 return;
532 }
533 else {
534 append(wsbuf.toString());
535 in.unread(); //let other cases also handle first non-ws
536 }
537 break;
538
539 case '#':
540 tree.add(new Hash(offset, buf));
541 //skipIfWhitespaceToEnd();
542 buf.reset();
543 if (dbg) dbgexit();
544 return;
545
546 default:
547 append(c);
548 } //switch
549 } //while
550 }
551
552 /**
553 [page <<<FOO]
554 ...as-is..no parse, no interpolation..
555 FOO
556 */
557 void parseHeredoc(StringBuilder directives_buf) throws IOException
558 {
559 if (dbg) dbgenter();
560
561 int startline = in.getLine();
562 int startcol = in.getCol();
563
564 int i = directives_buf.indexOf("<<<"); /* "<<<".length = 3 */
565 CharSequence subseq = directives_buf.substring(
566 i+3,
567 /*directives_buf does not have a ending ']' */
568 directives_buf.length()
569 );
570
571 final String heredoc = subseq.toString().trim();
572 final int heredoc_len = heredoc.length();
573 final CharArrayWriter heredoc_buf = new CharArrayWriter(2048);
574
575 /*
576 the ending heredoc after newline speeds things up a bit
577 which is why is traditionally used i guess, otherwise
578 we have to try a full match every first match. this
579 implementation doesn't care where the ending heredoc
580 appears (can be anywhere)...simplifies the implementation.
581 */
582
583 while (true)
584 {
585 c = in.read();
586
587 if (c == EOF) {
588 unclosed("heredoc: <<<"+heredoc, startline, startcol);
589 break;
590 }
591
592 if (c == heredoc.charAt(0))
593 {
594 boolean matched = true;
595 if (heredoc_len > 1) {
596 matched = in.match(heredoc.substring(1));
597 }
598 if (matched) {
599 tree.add(new Heredoc(heredoc_buf));
600 break;
601 }
602 }
603
604 //default action
605 heredoc_buf.append((char)c);
606 } //while
607
608 if (dbg) dbgexit();
609 }
610
611 /*
612 Text is the parent node for the expression. A new expression is parsed,
613 created and added to the text object by this method
614 */
615 void parseExpression(Element parent) throws IOException
616 {
617 if (dbg) dbgenter();
618
619 int startline = in.getLine();
620 int startcol = in.getCol();
621
622 while (true)
623 {
624 c = in.read();
625
626 switch (c)
627 {
628 case EOF:
629 unclosed("expression", startline, startcol);
630 if (dbg) dbgexit();
631 return;
632
633 case '\\':
634 if (in.match(']'))
635 append(']');
636 else
637 append(c);
638 break;
639
640 case ']':
641 if (buf.toString().trim().length() == 0)
642 error("Empty expression not allowed", startline, startcol);
643 parent.addExp(new Exp(buf));
644 buf.reset();
645 if (dbg) dbgexit();
646 return;
647
648 default:
649 append(c);
650 }
651 }
652 }
653
654 void parseComment() throws IOException
655 {
656 if (dbg) dbgenter();
657
658 int startline = in.getLine();
659 int startcol = in.getCol();
660
661 while (true)
662 {
663 c = in.read();
664
665 switch (c)
666 {
667 case EOF:
668 unclosed("comment", startline, startcol);
669 if (dbg) dbgexit();
670 return;
671
672 case '*':
673 if (in.match("/]"))
674 {
675 tree.add(new Comment(buf));
676 buf.reset();
677 if (dbg) dbgexit();
678 return;
679 }
680 else
681 append(c);
682 break;
683
684 default:
685 append(c);
686 }
687 }
688 }
689
690 void parseDeclaration() throws IOException
691 {
692 if (dbg) dbgenter();
693 int startline = in.getLine();
694 int startcol = in.getCol();
695
696 while (true)
697 {
698 c = in.read();
699
700 switch (c)
701 {
702 case EOF:
703 unclosed("declaration", startline, startcol);
704 if (dbg) dbgexit();
705 return;
706
707 case '!':
708 if (in.match(']')) {
709 decl.add(new Decl(buf));
710 buf.reset();
711 if (dbg) dbgexit();
712 return;
713 }
714 else{
715 append(c);
716 }
717 break;
718
719 //top level // and /* comments, ']' (close decl tag)
720 //is ignored within them
721 case '/':
722 append(c);
723 c = in.read();
724 append(c);
725 if (c == '/')
726 appendCodeSlashComment();
727 else if (c == '*')
728 appendCodeStarComment();
729 break;
730
731 //close tags are ignored within them
732 case '"': //strings outside of any comment
733 append(c);
734 appendCodeString();
735 break;
736
737 case '\'':
738 append(c);
739 appendCodeCharLiteral();
740 break;
741
742 default:
743 append(c);
744 }
745 }
746
747 }
748
749 void parseDirective() throws IOException
750 {
751 if (dbg) dbgenter();
752
753 int startline = in.getLine();
754 int startcol = in.getCol();
755
756 StringBuilder directives_buf = new StringBuilder(1024);
757
758 while (true)
759 {
760 c = in.read();
761
762 switch (c)
763 {
764 case EOF:
765 unclosed("directive", startline, startcol);
766 if (dbg) dbgexit();
767 return;
768
769 case ']':
770 if (directives_buf.indexOf("<<<") >= 0) {
771 parseHeredoc(directives_buf);
772 }
773 else{/* other directives used at page-generation time */
774 addDirectives(directives_buf);
775 }
776
777 if (dbg) dbgexit();
778 return;
779
780 default:
781 directives_buf.append((char)c);
782 }
783 }
784
785 }
786
787 //[a-zA-Z_\-0-9] == ( \w | - )
788 static final Pattern directive_pat = Pattern.compile(
789 //foo = "bar baz" (embd. spaces)
790 "\\s*([a-zA-Z_\\-0-9]+)\\s*=\\s*\"((?:.|\r|\n)+?)\""
791 + "|"
792 //foo = "bar$@#$" (no spaces) OR foo = bar (quotes optional)
793 + "\\s*([a-zA-Z_\\-0-9]+)\\s*=\\s*(\\S+)"
794 );
795
796
797 void addDirectives(StringBuilder directives_buf) throws ParseException
798 {
799 if (dbg) {
800 dbgenter();
801 System.out.println("-------directives section--------");
802 System.out.println(directives_buf.toString());
803 System.out.println("-------end directives-------");
804 }
805
806 String name, value;
807 try {
808 Matcher m = directive_pat.matcher(directives_buf);
809 while (m.find())
810 {
811 if (dbg) System.out.println(">>>>[0]->" + m.group()
812 + "; [1]->" + m.group(1)
813 + " [2]->" + m.group(2)
814 + " [3]->" + m.group(3)
815 + " [4]->" + m.group(4));
816
817 name = m.group(1) != null ? m.group(1).toLowerCase() :
818 m.group(3).toLowerCase();
819 value = m.group(2) != null ? m.group(2).toLowerCase() :
820 m.group(4).toLowerCase();
821
822 if (name.equals(d_buffersize))
823 {
824 //can throw parse exception
825 directives.put(name,
826 IOUtil.stringToFileSize(value.replace("\"|'","")));
827 }
828 else if (name.equals(d_encoding)) {
829 directives.put(name, value.replace("\"|'",""));
830 }
831 else if (name.equals(d_src_encoding)) {
832 directives.put(name, value.replace("\"|'",""));
833 }
834 else if (name.equals(d_mimetype)) {
835 directives.put(name, value.replace("\"|'",""));
836 }
837 else if (name.equals(d_out)) {
838 directives.put(name, value.replace("\"|'",""));
839 }
840 //else if .... other directives here as needed....
841 else
842 throw new Exception("Do not understand directive: " + m.group());
843 }
844 if (dbg) System.out.println("Added directives: " + directives);
845 }
846 catch (Exception e) {
847 throw new ParseException("File: " + inputFile.getAbsolutePath()
848 + ";\n" + e.toString());
849 }
850
851 if (dbg) dbgexit();
852 }
853
854 void parseIncludeFile() throws IOException
855 {
856 if (dbg) dbgenter();
857
858 int startline = in.getLine();
859 int startcol = in.getCol();
860 String option = null;
861
862 while (true)
863 {
864 c = in.read();
865
866 switch (c)
867 {
868 case EOF:
869 unclosed("include-file", startline, startcol);
870 if (dbg) dbgexit();
871 return;
872
873 case '[':
874 if (in.match('=')) {
875 //log.warn("Expressions cannot exist in file includes. Ignoring \"[=\"
876 //in [include-file... section starting at:", startline, startcol);
877 //instead of warn, we will error out. failing early is better.
878 //this does preclude having '[=' in the file name, but it's a good
879 //tradeoff
880 error("Expressions cannot exist in file includes. The offending static-include section starts at:", startline, startcol);
881 }
882 append(c);
883 break;
884
885 case ']':
886 IncludeFile i = new IncludeFile(buf);
887 if (option != null)
888 i.setOption(option);
889 tree.add(i);
890 buf.reset();
891 if (dbg) dbgexit();
892 return;
893
894 case 'o':
895 if (! in.match("ption"))
896 append(c);
897 else{
898 skipWS();
899 if (! in.match("=")) {
900 error("bad option parameter in file include: ", startline, startcol);
901 }
902 skipWS();
903
904 int c2;
905 StringBuilder optionbuf = new StringBuilder();
906 while (true) {
907 c2 = in.read();
908 if (c2 == ']' || c2 == EOF || Character.isWhitespace(c2)) {
909 in.unread();
910 break;
911 }
912 optionbuf.append((char)c2);
913 }
914
915 option = optionbuf.toString();
916 //System.out.println(option);
917 } //else
918 break;
919
920 default:
921 append(c);
922 }
923 }
924 }
925
926 void parseIncludeDecl() throws IOException
927 {
928 if (dbg) dbgenter();
929
930 int startline = in.getLine();
931 int startcol = in.getCol();
932 String option = null;
933
934 while (true)
935 {
936 c = in.read();
937
938 switch (c)
939 {
940 case EOF:
941 unclosed("include-decl", startline, startcol);
942 if (dbg) dbgexit();
943 return;
944
945 case '[':
946 if (in.match('=')) {
947 //log.warn("Expressions cannot exist in file includes. Ignoring \"[=\" in [include-static... section starting at:", startline, startcol);
948 //we will throw an exception. failing early is better. this
949 //does preclude having '[=' in the file name, but it's a good tradeoff
950 error("Expressions cannot exist in include-decl. The offending static-include section starts at:", startline, startcol);
951 }
952 append(c);
953 break;
954
955 case ']':
956 IncludeDecl i = new IncludeDecl(buf);
957 if (option != null)
958 i.setOption(option);
959 inc_decl.add(i);
960 buf.reset();
961 if (dbg) dbgexit();
962 return;
963
964 case 'o':
965 if (! in.match("ption"))
966 append(c);
967 else{
968 skipWS();
969 if (! in.match("=")) {
970 error("bad option parameter in include-code: ", startline, startcol);
971 }
972 skipWS();
973
974 int c2;
975 StringBuilder optionbuf = new StringBuilder();
976 while (true) {
977 c2 = in.read();
978 if (c2 == ']' || c2 == EOF || Character.isWhitespace(c2)) {
979 in.unread();
980 break;
981 }
982 optionbuf.append((char)c2);
983 }
984
985 option = optionbuf.toString();
986 //System.out.println(option);
987 } //else
988 break;
989
990 default:
991 append(c);
992 }
993 }
994 }
995
996 //the filename/url can be optionally double quoted. leading/trailing
997 //double quotes (if any) are ignored when an include is rendered...
998 //this way there isn't any additional parsing needed here...I could
999 //ignore the optional quote here (and that's the formal proper way)
1000 //and then not move the ignore quote logic into the render() method but
1001 //this way is good too...and simpler..
1002 //same goes for the other parseIncludeXX/ForwardXX functions.
1003 void parseInclude() throws IOException
1004 {
1005 if (dbg) dbgenter();
1006
1007 int startline = in.getLine();
1008 int startcol = in.getCol();
1009 Include include = new Include();
1010 while (true)
1011 {
1012 c = in.read();
1013
1014 switch (c)
1015 {
1016 case EOF:
1017 unclosed("include", startline, startcol);
1018 if (dbg) dbgexit();
1019 return;
1020
1021 case '[':
1022 if (in.match('=')) {
1023 include.add(buf);
1024 buf.reset();
1025 parseExpression(include);
1026 }
1027 else{
1028 append(c);
1029 }
1030 break;
1031
1032 case ']':
1033 include.add(buf);
1034 tree.add(include);
1035 buf.reset();
1036 if (dbg) dbgexit();
1037 return;
1038
1039 default:
1040 append(c);
1041 }
1042 }
1043 }
1044
1045 void parseForward() throws IOException
1046 {
1047 if (dbg) dbgenter();
1048
1049 int startline = in.getLine();
1050 int startcol = in.getCol();
1051
1052 Forward forward = new Forward();
1053 while (true)
1054 {
1055 c = in.read();
1056
1057 switch (c)
1058 {
1059 case EOF:
1060 unclosed("forward", startline, startcol);
1061 if (dbg) dbgexit();
1062 return;
1063
1064 case '[':
1065 if (in.match('=')) {
1066 forward.add(buf);
1067 buf.reset();
1068 parseExpression(forward);
1069 }
1070 else{
1071 append(c);
1072 }
1073 break;
1074
1075 case ']':
1076 forward.add(buf);
1077 tree.add(forward);
1078 buf.reset();
1079 if (dbg) dbgexit();
1080 return;
1081
1082 default:
1083 append(c);
1084 }
1085 }
1086 }
1087
1088 //we need to parse imports seperately because they go outside
1089 //a class declaration (and [!...!] goes inside a class)
1090 //import XXX.*;
1091 //class YYY {
1092 //[!....stuff from here ....!]
1093 //...
1094 void parseImport() throws IOException
1095 {
1096 if (dbg) dbgenter();
1097
1098 int startline = in.getLine();
1099 int startcol = in.getCol();
1100
1101 while (true)
1102 {
1103 c = in.read();
1104
1105 switch (c)
1106 {
1107 case EOF:
1108 unclosed("import", startline, startcol);
1109 if (dbg) dbgexit();
1110 return;
1111
1112 case '\n':
1113 imps.add(new Import(buf));
1114 buf.reset();
1115 break;
1116
1117 case ']':
1118 imps.add(new Import(buf));
1119 buf.reset();
1120 if (dbg) dbgexit();
1121 return;
1122
1123 default:
1124 append(c);
1125 }
1126 }
1127 }
1128
1129 /*
1130 Called when // was read at the top level inside a code block. Appends
1131 the contents of a // comment to the buffer (not including the trailing
1132 newline)
1133 */
1134 void appendCodeSlashComment() throws IOException
1135 {
1136 if (dbg) dbgenter();
1137
1138 while (true)
1139 {
1140 c = in.read();
1141
1142 if (c == EOF)
1143 break;
1144
1145 //do not append \r, \r\n, or \n, that finishes the // comment
1146 //we need that newline to figure out if the next line is a hash
1147 //line
1148 if (c == '\r') {
1149 in.unread();
1150 break;
1151 }
1152
1153 if (c == '\n') {
1154 in.unread();
1155 break;
1156 }
1157
1158 append(c);
1159 }
1160
1161 if (dbg) dbgread("CodeSLASHComment Finished: Buffer=" + StringUtil.viewableAscii(buf.toString()));
1162 if (dbg) dbgexit();
1163 }
1164
1165 /*
1166 Called when /* was read at the top level inside a code block. Appends
1167 the contents of a /*comment to the buffer. (not including any trailing
1168 newline or spaces)
1169 */
1170 void appendCodeStarComment() throws IOException
1171 {
1172 if (dbg) dbgenter();
1173
1174 while (true)
1175 {
1176 c = in.read();
1177
1178 if (c == EOF)
1179 break;
1180
1181 append(c);
1182
1183 if (c == '*')
1184 {
1185 if (in.match('/')) {
1186 append('/');
1187 break;
1188 }
1189 }
1190 }
1191
1192 if (dbg) dbgread("CodeSTARComment Finished: Buffer=" + StringUtil.viewableAscii(buf.toString()));
1193 if (dbg) dbgexit();
1194 }
1195
1196 /*
1197 Called (outside of any comments in the code block) when:
1198 --> parseCode()
1199 ... "
1200 ^ (we are here)
1201 */
1202 void appendCodeString() throws IOException
1203 {
1204 if (dbg) dbgenter();
1205
1206 int startline = in.getLine();
1207 int startcol = in.getCol();
1208
1209 while (true)
1210 {
1211 c = in.read();
1212
1213 if (c == EOF || c == '\r' || c == '\n')
1214 unclosed("string literal", startline, startcol);
1215
1216 append(c);
1217
1218 if (c == '\\') {
1219 c = in.read();
1220 if (c == EOF)
1221 unclosed("string literal", startline, startcol);
1222 else {
1223 append(c);
1224 continue; //so \" does not hit the if below and break
1225 }
1226 }
1227
1228 if (c == '"')
1229 break;
1230 }
1231
1232 if (dbg) dbgread("appendCodeString Finished: Buffer=" + StringUtil.viewableAscii(buf.toString()));
1233 if (dbg) dbgexit();
1234 }
1235
1236
1237 /*
1238 Called (outside of any comments in the code block) when:
1239 --> parseCode()
1240 ... '
1241 ^ (we are here)
1242 */
1243 void appendCodeCharLiteral() throws IOException
1244 {
1245 if (dbg) dbgenter();
1246
1247 int startline = in.getLine();
1248 int startcol = in.getCol();
1249
1250 while (true)
1251 {
1252 c = in.read();
1253
1254 if (c == EOF || c == '\r' || c == '\n')
1255 unclosed("char literal", startline, startcol);
1256
1257 append(c);
1258
1259 if (c == '\\') {
1260 c = in.read();
1261 if (c == EOF)
1262 unclosed("char literal", startline, startcol);
1263 else {
1264 append(c);
1265 continue; //so \' does not hit the if below and break
1266 }
1267 }
1268
1269 if (c == '\'')
1270 break;
1271 }
1272
1273 if (dbg) dbgread("appendCodeCharLiteral Finished: Buffer=" + StringUtil.viewableAscii(buf.toString()));
1274 if (dbg) dbgexit();
1275 }
1276
1277
1278 /*
1279 Reads from the current position till the first nonwhitespace char, EOF or
1280 newline is encountered. Reads are into the whitespace buffer. does not
1281 consume the character past the non-whitespace character and does
1282 NOT read multiple lines of whitespace.
1283 */
1284 void readToFirstNonWS() throws IOException
1285 {
1286 wsbuf.reset();
1287
1288 while (true)
1289 {
1290 c = in.read();
1291
1292 if (c == '\r' || c == '\n')
1293 break;
1294
1295 if (c == EOF || ! Character.isWhitespace(c))
1296 break;
1297
1298 wsbuf.append((char)c);
1299 }
1300
1301 in.unread();
1302 }
1303
1304 //skip till end of whitespace or EOF. does not consume any chars past
1305 //the whitespace.
1306 void skipWS() throws IOException
1307 {
1308 int c2 = EOF;
1309 while (true) {
1310 c2 = in.read();
1311 if (c2 == EOF || ! Character.isWhitespace(c2)) {
1312 in.unread();
1313 break;
1314 }
1315 }
1316 }
1317
1318 //skips to the end of line if the rest of the line is (from the current
1319 //position), all whitespace till the end. otherwise, does not change
1320 //current position. consumes trailing newlines (if present) when reading
1321 //whitespace.
1322 void skipIfWhitespaceToEnd() throws IOException
1323 {
1324 int count = 0;
1325
1326 while (true)
1327 {
1328 c = in.read();
1329 count++;
1330
1331 if (c == '\r') {
1332 in.match('\n');
1333 return;
1334 }
1335
1336 if (c == '\n' || c == EOF)
1337 return;
1338
1339 if (! Character.isWhitespace(c))
1340 break;
1341 }
1342
1343 in.unread(count);
1344 }
1345
1346 //not used anymore but left here for potential future use. does not
1347 //consume the newline (if present)
1348 void skipToLineEnd() throws IOException
1349 {
1350 while (true)
1351 {
1352 int c = in.read();
1353 if (c == EOF) {
1354 in.unread();
1355 break;
1356 }
1357 if (c == '\n' || c == '\r') {
1358 in.unread();
1359 break;
1360 }
1361 }
1362 }
1363
1364 String quote(final char c)
1365 {
1366 switch (c)
1367 {
1368 case '\r':
1369 return "\\r";
1370
1371 case '\n':
1372 return "\\n";
1373
1374 case '\"':
1375 //can also say: new String(new char[] {'\', '"'})
1376 return "\\\""; //--> \"
1377
1378 case '\\':
1379 return "\\\\";
1380
1381 default:
1382 return String.valueOf(c);
1383 }
1384 }
1385
1386 //======= util and debug methods ==========================
1387 String methodName(int framenum)
1388 {
1389 StackTraceElement ste[] = new Exception().getStackTrace();
1390 //get method that called us, we are ste[0]
1391 StackTraceElement st = ste[framenum];
1392 String file = st.getFileName();
1393 int line = st.getLineNumber();
1394 String method = st.getMethodName();
1395 String threadname = Thread.currentThread().getName();
1396 return method + "()";
1397 }
1398
1399 void dbgenter() {
1400 System.out.format("%s-->%s\n", StringUtil.repeat('\t', dbgtab++), methodName(2));
1401 }
1402
1403 void dbgexit() {
1404 System.out.format("%s<--%s\n", StringUtil.repeat('\t', --dbgtab), methodName(2));
1405 }
1406
1407 void dbgread(String str) {
1408 System.out.format("%s %s\n", StringUtil.repeat('\t', dbgtab), StringUtil.viewableAscii(str));
1409 }
1410
1411 void dbgread(String str, List list) {
1412 System.out.format("%s %s: ", StringUtil.repeat('\t', dbgtab), StringUtil.viewableAscii(str));
1413 for (int n = 0; n < list.size(); n++) {
1414 System.out.print( StringUtil.viewableAscii( (String)list.get(n) ) );
1415 }
1416 System.out.println("");
1417 }
1418
1419 void dbgread(char c) {
1420 System.out.format("%s %s\n", StringUtil.repeat('\t', dbgtab), StringUtil.viewableAscii(c));
1421 }
1422
1423 void dbgread(CharArrayWriter buf) {
1424 System.out.format("%s %s\n", StringUtil.repeat('\t', dbgtab), StringUtil.viewableAscii(buf.toString()));
1425 }
1426
1427 void unclosed(String blockname, int startline, int startcol) throws IOException
1428 {
1429 throw new IOException(blockname + " tag not closed.\nThis tag was possibly opened in: \nFile:"
1430 + inputFile + ", line:"
1431 + startline + " column:" + startcol +
1432 ".\nCurrent line:" + in.getLine() + " column:" + in.getCol());
1433 }
1434
1435 void error(String msg, int line, int col) throws IOException
1436 {
1437 throw new IOException("Error in File:" + inputFile + " Line:" + line + " Col:" + col + " " + msg);
1438 }
1439
1440 void error(String msg) throws IOException
1441 {
1442 throw new IOException("Error in File:" + inputFile + " " + msg);
1443 }
1444
1445 //============== Non Parsing methods ================================
1446 void o(Object str) {
1447 out.print(str);
1448 }
1449
1450 void ol(Object str) {
1451 out.println(str);
1452 }
1453
1454 void ol() {
1455 out.println();
1456 }
1457
1458 /**
1459 Returns the src_encoding directive (if any) defined in this page.
1460 */
1461 String getSourceEncoding() {
1462 return src_encoding;
1463 }
1464
1465 void writePage() throws IOException
1466 {
1467 if (! includeMode)
1468 {
1469 if (directives.containsKey(d_src_encoding)) {
1470 this.src_encoding = (String) directives.get(d_src_encoding);
1471 this.src_encoding = removeLeadingTrailingQuote(this.src_encoding);
1472 }
1473
1474 //create a appropriate PrintWriter based on either the default
1475 //jave encoding or the page specified java encoding
1476 //the java source file will be written out in this encoding
1477
1478 FileOutputStream fout = new FileOutputStream(outputFile);
1479 OutputStreamWriter fw = (src_encoding != null) ?
1480 new OutputStreamWriter(fout, src_encoding) :
1481 new OutputStreamWriter(fout);
1482
1483 out = new PrintWriter(new BufferedWriter(fw));
1484 }
1485
1486 if (! includeMode)
1487 {
1488 writePackage();
1489 writeImports();
1490
1491 o ("public class ");
1492 o (classname);
1493 ol(" extends fc.web.page.PageImpl");
1494 ol("{");
1495 }
1496
1497 writeFields();
1498
1499 if (! includeMode) {
1500 writeConstructor();
1501 }
1502
1503 writeMethods();
1504
1505 if (! includeMode) {
1506 ol("}");
1507 }
1508 }
1509
1510 void writePackage()
1511 {
1512 o ("package ");
1513 o (packagename);
1514 ol(";");
1515 ol();
1516 }
1517
1518 void writeImports() throws IOException
1519 {
1520 ol("import javax.servlet.*;");
1521 ol("import javax.servlet.http.*;");
1522 ol("import java.io.*;");
1523 ol("import java.util.*;");
1524 //write this in case (very rare) that a page overrides the
1525 //Page.init()/destory methods [we need pageservlet for init(..)]
1526 ol("import fc.web.page.PageServlet;");
1527 for (int n = 0; n < imps.size(); n++) {
1528 ((Element)imps.get(n)).render();
1529 ol();
1530 }
1531 ol();
1532 }
1533
1534 void writeFields()
1535 {
1536 }
1537
1538 void writeConstructor()
1539 {
1540 }
1541
1542 void writeMethods() throws IOException
1543 {
1544 writeDeclaredMethods();
1545 writeIncludedMethods();
1546 writeRenderMethod();
1547 }
1548
1549 void writeDeclaredMethods() throws IOException
1550 {
1551 for (int n = 0; n < decl.size(); n++) {
1552 ((Element)decl.get(n)).render();
1553 }
1554
1555 if (decl.size() > 0)
1556 ol();
1557 }
1558
1559 void writeIncludedMethods() throws IOException
1560 {
1561 for (int n = 0; n < inc_decl.size(); n++) {
1562 ((Element)inc_decl.get(n)).render();
1563 }
1564
1565 if (inc_decl.size() > 0)
1566 ol();
1567 }
1568
1569 void writeRenderMethod() throws IOException
1570 {
1571 if (! includeMode)
1572 writeRenderTop();
1573
1574 for (int n = 0; n < tree.size(); n++) {
1575 ((Element)tree.get(n)).render();
1576 }
1577
1578 if (! includeMode)
1579 writeRenderBottom();
1580
1581 }
1582
1583 void writeRenderTop() throws IOException
1584 {
1585 ol("public void render(HttpServletRequest req, HttpServletResponse res) throws Exception");
1586 ol("\t{");
1587 ol(" /* for people used to typing 'request/response' */");
1588 ol(" final HttpServletRequest request = req;");
1589 ol(" final HttpServletResponse response = res;");
1590 ol();
1591 //mime+charset
1592 String content_type = "";
1593 if (directives.containsKey(d_mimetype))
1594 {
1595 String mtype = (String) directives.get(d_mimetype);
1596 if (! (mtype.equals("") || mtype.equals(mimetype_none)) )
1597 {
1598 mtype = removeLeadingTrailingQuote(mtype);
1599 content_type += mtype;
1600 }
1601 }
1602 else{
1603 content_type += Page.DEFAULT_MIME_TYPE;
1604 }
1605
1606
1607 if (directives.containsKey(d_encoding)) {
1608 String encoding = (String) directives.get(d_encoding);
1609 encoding = removeLeadingTrailingQuote(encoding);
1610 /*an empty encoding means that the encoding is specified in the
1611 html header*/
1612 if (! encoding.trim().equals("")) {
1613 content_type += "; charset=";
1614 content_type += encoding;
1615 }
1616 }
1617 else{
1618 content_type += "; charset=";
1619 content_type += Page.DEFAULT_ENCODING;
1620 }
1621
1622 o (" res.setContentType(\""); o (content_type); ol("\");");
1623
1624 //buffer
1625 if (directives.containsKey(d_buffersize)) {
1626 o (" res.setBufferSize(");
1627 o (directives.get(d_buffersize));
1628 ol(");");
1629 }
1630
1631 //stream or writer
1632 boolean stream = false;
1633 if (directives.containsKey(d_out))
1634 {
1635 String stream_type = ((String) directives.get(d_out))
1636 .toLowerCase().intern();
1637
1638 if (stream_type == d_out_stream1 || stream_type == d_out_stream2)
1639 stream = true;
1640 else if (stream_type == d_out_writer)
1641 stream = false;
1642 else
1643 error("Did not understand directive [directive name=out, value=" + stream_type + "]. Choose between (" + d_out_stream1 + ") and (" + d_out_writer + ")");
1644 }
1645
1646 if (stream)
1647 ol(" ServletOutputStream out = res.getOutputStream();");
1648 else
1649 ol(" PrintWriter out = res.getWriter();");
1650
1651 }
1652
1653 void writeRenderBottom() throws IOException
1654 {
1655 ol();
1656 ol("\t} //~render end");
1657 }
1658
1659
1660 /*
1661 int tabcount = 1;
1662 String tab = "\t";
1663 void tabInc() {
1664 tab = StringUtil.repeat('\t', ++tabcount);
1665 }
1666 void tabDec() {
1667 tab = StringUtil.repeat('\t', --tabcount);
1668 }
1669 */
1670
1671 abstract class Element {
1672 abstract void render() throws IOException;
1673 //text, include etc., implement this as needed.
1674 void addExp(Exp e) {
1675 throw new RuntimeException("Internal error: not implemented by this object");
1676 }
1677 }
1678
1679 //this should NOT be added to the tree directly but added to Text or Hash
1680 //via the addExp() method. This is because exps must be printed inline
1681 class Exp extends Element
1682 {
1683 String str;
1684
1685 Exp(CharArrayWriter buf) {
1686 this.str = buf.toString();
1687 if (dbg) dbgread("<new EXP> "+ str);
1688 }
1689
1690 void render() {
1691 o("out.print (");
1692 o(str);
1693 ol(");");
1694 }
1695 }
1696
1697 class Text extends Element
1698 {
1699 String offset_space;
1700 final List list = new ArrayList();
1701
1702 //each text section is parsed by a text node. Within EACH text
1703 //node, we split it's contained text into separate lines and
1704 //generate code to print each line with a "out.println(...)"
1705 //statement. This maintains the same source order as the molly
1706 //page. If we munge together everything and print all of it's
1707 //contents with just one out.println(...)" statement, we would
1708 //get one large line with embedded \n and that would make
1709 //things more difficult to co-relate with the source file.
1710
1711 Text(final String offset, final CharArrayWriter b)
1712 {
1713 if (offset == null)
1714 offset_space = "\t";
1715 else
1716 offset_space = "\t" + offset;
1717
1718 final char[] buf = b.toCharArray();
1719
1720 boolean prevWasCR = false;
1721 //jdk default is 32. we say 256. not too large, maybe
1722 //less cache pressure. not too important, gets resized
1723 //as needed anyway.
1724 final CharArrayWriter tmp = new CharArrayWriter(256);
1725
1726 for (int i=0, j=1; i < buf.length; i++, j++)
1727 {
1728 char c = buf[i];
1729 if (j == buf.length) {
1730 tmp.append(quote(c));
1731 list.add(tmp.toString());
1732 tmp.reset();
1733 }
1734 else if (c == '\n') {
1735 tmp.append(quote(c));
1736 if (! prevWasCR) {
1737 list.add(tmp.toString());
1738 tmp.reset();
1739 }
1740 }
1741 else if (c == '\r') {
1742 tmp.append(quote(c));
1743 list.add(tmp.toString());
1744 tmp.reset();
1745 prevWasCR = true;
1746 }
1747 else{
1748 tmp.append(quote(c));
1749 prevWasCR = false;
1750 }
1751 }
1752
1753 if (dbg) {
1754 String classname = getClass().getName();
1755 dbgread("<new " + classname.substring(classname.indexOf("$")+1,classname.length()) + ">",list);
1756 }
1757 }
1758
1759 Text(CharArrayWriter b)
1760 {
1761 this(null, b);
1762 }
1763
1764 void addExp(Exp e)
1765 {
1766 list.add(e);
1767 }
1768
1769 void render()
1770 {
1771 for (int i=0, j=1; i<list.size(); i++, j++)
1772 {
1773 Object obj = list.get(i); //can be String or Exp
1774 if (obj instanceof Exp) {
1775 o(offset_space);
1776 ((Exp)obj).render();
1777 }
1778 else{
1779 o(offset_space);
1780 o("out.print (\"");
1781 o(obj);
1782 ol("\");");
1783 }
1784 }
1785 } //render
1786 }
1787
1788 class Hash extends Text
1789 {
1790 Hash(final String offset, final CharArrayWriter b)
1791 {
1792 super(offset, b);
1793 }
1794
1795 //same as super.render() except for j == list.size() o/ol() below
1796 void render()
1797 {
1798 for (int i=0, j=1; i<list.size(); i++, j++)
1799 {
1800 Object obj = list.get(i); //can be String or Exp
1801 if (obj instanceof Exp) {
1802 o(offset_space);
1803 ((Exp)obj).render();
1804 }
1805 else{
1806 o(offset_space);
1807 o("out.print (\"");
1808 o(obj);
1809
1810 if (j == list.size())
1811 o ("\");");
1812 else
1813 ol("\");");
1814 }
1815 }
1816 } //render
1817 }
1818
1819 class Heredoc extends Text
1820 {
1821 Heredoc(final CharArrayWriter buf)
1822 {
1823 super(null, buf);
1824 }
1825
1826 //override, exp cannot be added to heredoc sections
1827 void addExp(Exp e)
1828 {
1829 throw new IllegalStateException("Internal implementation error: this method should not be called for a Heredoc object");
1830 }
1831
1832 void render()
1833 {
1834 for (int i=0, j=1; i<list.size(); i++, j++)
1835 {
1836 Object obj = list.get(i);
1837 o(offset_space);
1838 o("out.print (\"");
1839 o(obj);
1840 ol("\");");
1841 }
1842 } //render
1843 }
1844
1845
1846 class Code extends Element
1847 {
1848 List list = new ArrayList();
1849
1850 Code(CharArrayWriter b)
1851 {
1852 //we split the code section into separate lines and
1853 //print each line with a out.print(...). This maintains
1854 //the same source order as the molly page. If we munge together
1855 //everything, we would get one large line with embedded \n
1856 //and that would make things more difficult to co-relate.
1857 final char[] buf = b.toCharArray();
1858 CharArrayWriter tmp = new CharArrayWriter();
1859 for (int i=0, j=1; i < buf.length; i++, j++) {
1860 char c = buf[i];
1861 if (j == buf.length) { //end of buffer
1862 tmp.append(c);
1863 list.add(tmp.toString());
1864 tmp.reset();
1865 }
1866 else if (c == '\n') {
1867 tmp.append(c);
1868 list.add(tmp.toString());
1869 tmp.reset();
1870 }
1871 else
1872 tmp.append(c);
1873 }
1874 if (dbg) {
1875 String classname = getClass().getName();
1876 dbgread("<new " + classname.substring(classname.indexOf("$")+1,classname.length()) + ">",list);
1877 }
1878 }
1879
1880 void render() {
1881 for (int i = 0; i < list.size(); i++) {
1882 o('\t');
1883 o(list.get(i));
1884 }
1885 }
1886 }
1887
1888 class Comment extends Element
1889 {
1890 String str;
1891
1892 Comment(CharArrayWriter buf) {
1893 this.str = buf.toString();
1894 if (dbg) dbgread("<new COMMENT> "+ str);
1895 }
1896
1897 void render() {
1898 //we don't print commented sections
1899 }
1900 }
1901
1902 class Decl extends Code
1903 {
1904 Decl(CharArrayWriter buf) {
1905 super(buf);
1906 }
1907
1908 void render() {
1909 for (int i = 0; i < list.size(); i++) {
1910 o (list.get(i));
1911 }
1912 }
1913 }
1914
1915 /* base class for Forward and Include */
1916 class ForwardIncludeElement extends Element
1917 {
1918 List parts = new ArrayList();
1919 boolean useBuf = false;
1920
1921 // the following is for includes with expressions
1922 // [include foo[=i].html]
1923 // i could be 1,2,3.. the parser adds the xpression [=i] to this
1924 // object if it's present via the addExp method below
1925 void add(CharArrayWriter buf)
1926 {
1927 parts.add(buf.toString().trim());
1928 if (parts.size() > 1) {
1929 useBuf = true;
1930 }
1931 }
1932
1933 void addExp(Exp e)
1934 {
1935 parts.add(e);
1936 useBuf = true;
1937 }
1938
1939 void render() throws IOException
1940 {
1941 if (parts.size() == 0) {
1942 //log.warn("possible internal error, parts.size()==0 in Forward");
1943 return;
1944 }
1945
1946 ol("\t{ //this code block gives 'rd' its own namespace");
1947
1948 if (! useBuf) {
1949 o ("\tfinal RequestDispatcher rd = req.getRequestDispatcher(\"");
1950 //only 1 string
1951 o (removeLeadingTrailingQuote(parts.get(0).toString()));
1952 ol("\");");
1953 }
1954 else{
1955 ol("\tfinal StringBuilder buf = new StringBuilder();");
1956 for (int n = 0; n < parts.size(); n++) {
1957 Object obj = parts.get(n);
1958 if ( n == 0 || (n + 1) == parts.size() ) {
1959 obj = removeLeadingTrailingQuote(obj.toString());
1960 }
1961 if (obj instanceof String) {
1962 o ("\tbuf.append(\"");
1963 o (obj);
1964 ol("\");");
1965 }
1966 else{
1967 o ("\tbuf.append(");
1968 o ( ((Exp)obj).str );
1969 ol(");");
1970 }
1971 } //for
1972 ol("\tfinal RequestDispatcher rd = req.getRequestDispatcher(buf.toString());");
1973 } //else
1974 }
1975 }
1976
1977 /* a request dispatcher based include. */
1978 class Include extends ForwardIncludeElement
1979 {
1980 Include() {
1981 if (dbg) dbgread("<new INCLUDE> ");
1982 }
1983
1984 void render() throws IOException
1985 {
1986 super.render();
1987 ol("\trd.include(req, res);");
1988 ol("\t} //end rd block");
1989 }
1990 }
1991
1992 /* a request dispatcher based forward */
1993 class Forward extends ForwardIncludeElement
1994 {
1995 Forward() {
1996 if (dbg) dbgread("<new FORWARD>");
1997 }
1998
1999 void render() throws IOException
2000 {
2001 super.render();
2002 ol("\t//WARNING: any uncommitted page content before this forward will be discarded.");
2003 ol("\t//If the response has already been committed an exception will be thrown. ");
2004
2005 ol("\trd.forward(req, res);");
2006
2007 ol("\t//NOTE: You should 'return' right after this line. There should be no content in your ");
2008 ol("\t//page after the forward statement");
2009 ol("\t} //end rd block");
2010 }
2011 }
2012
2013 /*
2014 A molly mechanism to include an external file whose contents will
2015 be rendered as part of the page.
2016 */
2017 class IncludeFile extends Element
2018 {
2019 String str;
2020 String opt;
2021
2022 IncludeFile(CharArrayWriter buf) {
2023 if (dbg) dbgread("<new INCLUDE-FILE> ");
2024 str = removeLeadingTrailingQuote(buf.toString().trim());
2025 }
2026
2027 void setOption(String opt) {
2028 this.opt = opt;
2029 }
2030
2031 void render() throws IOException
2032 {
2033 File includeFile = null;
2034 File parentDir = inputFile.getParentFile();
2035 if (parentDir == null) {
2036 parentDir = new File(".");
2037 }
2038
2039 if (str.startsWith("/"))
2040 includeFile = new File(contextRoot, str);
2041 else
2042 includeFile = new File(parentDir, str);
2043
2044 //System.out.println(">>>>>>>>>> f="+f +";root="+contextRoot);
2045
2046 if (! includeFile.exists()) {
2047 throw new IOException("Include file does not exist: " + includeFile.getCanonicalPath());
2048 }
2049
2050 o("//>>>START INCLUDE from: ");
2051 o(includeFile.getAbsolutePath());
2052 ol();
2053
2054 if (opt != null && opt.toLowerCase().equalsIgnoreCase("noparse")) {
2055 o(IOUtil.inputStreamToString(new FileInputStream(includeFile)));
2056 }
2057 else{
2058 PageParser pp = new PageParser(contextRoot, includeFile, out, classname, log);
2059 pp.includeMode().parse(); /*writes to out*/
2060 }
2061
2062 o("//>>>END INCLUDE from: ");
2063 o(includeFile.getAbsolutePath());
2064 ol();
2065
2066 //circularities are tricky, later
2067 //includeMap.put(pageloc, includeFile.getCanonicalPath());
2068 }
2069 }
2070
2071 /* a molly mechanism to include an external file containing code and method
2072 declarations. These are typically commom utility methods and global
2073 vars. The included file is not parsed by the molly parser... the contents
2074 are treated as if they were written directly inside a [!....!] block.
2075 */
2076 class IncludeDecl extends Element
2077 {
2078 String str;
2079 String opt;
2080
2081 IncludeDecl(CharArrayWriter buf) {
2082 if (dbg) dbgread("<new INCLUDE-DECL> ");
2083 str = removeLeadingTrailingQuote(buf.toString().trim());
2084 }
2085
2086 void setOption(String opt) {
2087 this.opt = opt;
2088 }
2089
2090 void render() throws IOException
2091 {
2092 File f = null;
2093 File parentDir = inputFile.getParentFile();
2094 if (parentDir == null) {
2095 parentDir = new File(".");
2096 }
2097
2098 final int strlen = str.length();
2099
2100 if (str.startsWith("\"") || str.startsWith("'"))
2101 {
2102 if (strlen == 1) //just " or '
2103 throw new IOException("Bad include file name: " + str);
2104
2105 str = str.substring(1, strlen);
2106 }
2107
2108 if (str.endsWith("\"") || str.endsWith("'"))
2109 {
2110 if (strlen == 1) //just " or '
2111 throw new IOException("Bad include file name: " + str);
2112
2113 str = str.substring(0, strlen-1);
2114 }
2115
2116 if (str.startsWith("/"))
2117 f = new File(contextRoot, str);
2118 else
2119 f = new File(parentDir, str);
2120
2121 /* f = new File(parentDir, str); */
2122
2123 if (! f.exists()) {
2124 throw new IOException("Include file does not exist: " + f.getCanonicalPath());
2125 }
2126
2127 o("//>>>START INCLUDE DECLARTIONS from: ");
2128 o(f.getAbsolutePath());
2129 ol();
2130
2131 o(IOUtil.inputStreamToString(new FileInputStream(f)));
2132
2133 o("//>>>END INCLUDE DECLARATIONS from: ");
2134 o(f.getAbsolutePath());
2135 ol();
2136
2137 //circularities are tricky, later
2138 //includeMap.put(pageloc, f.getCanonicalPath());
2139 }
2140 }
2141
2142 class Import extends Code
2143 {
2144 Import(CharArrayWriter buf) {
2145 super(buf);
2146 }
2147
2148 void render() {
2149 for (int i = 0; i < list.size(); i++) {
2150 o (list.get(i));
2151 }
2152 }
2153 }
2154
2155 /**
2156 removes starting and trailing single/double quotes. used by the
2157 include/forward render methods only, NOT used while parsing.
2158 */
2159 private static String removeLeadingTrailingQuote(String str)
2160 {
2161 if (str == null)
2162 return str;
2163
2164 if ( str.startsWith("\"") || str.startsWith("'") ) {
2165 str = str.substring(1, str.length());
2166 }
2167
2168 if ( str.endsWith("\"") || str.endsWith("'") ) {
2169 str = str.substring(0, str.length()-1);
2170 }
2171
2172 return str;
2173 }
2174
2175 //===============================================
2176
2177 public static void main (String args[]) throws IOException
2178 {
2179 Args myargs = new Args(args);
2180 myargs.setUsage("java " + myargs.getMainClassName()
2181 + "\n"
2182 + "Required params:\n"
2183 + " -classname output_class_name\n"
2184 + " -in input_page_file\n"
2185 + "\nOptional params:\n"
2186 + " -encoding <page_encoding>\n"
2187 + " -contextRoot <webapp root-directory or any other directory>\n"
2188 + " this directory is used as the starting directory for absolute (starting\n"
2189 + " with a \"/\") include/forward directives in a page>. If not specified\n"
2190 + " defaults to the same directory as the page file\n"
2191 + " -out <output_file_name>\n"
2192 + " the output file is optional and defaults to the standard out if not specified."
2193 );
2194 //String encoding = myargs.get("encoding", Page.DEFAULT_ENCODING);
2195
2196 File input = new File(myargs.getRequired("in"));
2197 File contextRoot = null;
2198
2199 if (myargs.flagExists("contextRoot"))
2200 contextRoot = new File(myargs.get("contextRoot"));
2201 else
2202 contextRoot = input;
2203
2204 PrintWriter output;
2205
2206 if (myargs.get("out") != null)
2207 output = new PrintWriter(new FileWriter(myargs.get("out")));
2208 else
2209 output = new PrintWriter(new OutputStreamWriter(System.out));
2210
2211 PageParser parser = new PageParser(contextRoot, input, output, myargs.getRequired("classname"), Log.getDefault());
2212 parser.parse();
2213 }
2214
2215 }