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    }