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