001    /*
002     * Copyright (c) 2001-2004 Caucho Technology, Inc.  All rights reserved.
003     *
004     * The Apache Software License, Version 1.1
005     *
006     * Redistribution and use in source and binary forms, with or without
007     * modification, are permitted provided that the following conditions
008     * are met:
009     *
010     * 1. Redistributions of source code must retain the above copyright
011     *    notice, this list of conditions and the following disclaimer.
012     *
013     * 2. Redistributions in binary form must reproduce the above copyright
014     *    notice, this list of conditions and the following disclaimer in
015     *    the documentation and/or other materials provided with the
016     *    distribution.
017     *
018     * 3. The end-user documentation included with the redistribution, if
019     *    any, must include the following acknowlegement:
020     *       "This product includes software developed by the
021     *        Caucho Technology (http://www.caucho.com/)."
022     *    Alternately, this acknowlegement may appear in the software itself,
023     *    if and wherever such third-party acknowlegements normally appear.
024     *
025     * 4. The names "Burlap", "Resin", and "Caucho" must not be used to
026     *    endorse or promote products derived from this software without prior
027     *    written permission. For written permission, please contact
028     *    info@caucho.com.
029     *
030     * 5. Products derived from this software may not be called "Resin"
031     *    nor may "Resin" appear in their names without prior written
032     *    permission of Caucho Technology.
033     *
034     * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
035     * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
036     * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
037     * DISCLAIMED.  IN NO EVENT SHALL CAUCHO TECHNOLOGY OR ITS CONTRIBUTORS
038     * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
039     * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
040     * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
041     * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
042     * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
043     * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
044     * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
045     *
046     * @author Scott Ferguson
047     */
048    
049    package com.caucho.burlap.client;
050    
051    import java.io.ByteArrayOutputStream;
052    import java.io.IOException;
053    import java.io.InputStream;
054    import java.util.Calendar;
055    import java.util.Date;
056    import java.util.Hashtable;
057    import java.util.TimeZone;
058    import java.util.Vector;
059    
060    /**
061     * Input stream for Burlap requests, compatible with microedition
062     * Java.  It only uses classes and types available to J2ME.  In
063     * particular, it does not have any support for the <double> type.
064     *
065     * <p>MicroBurlapInput does not depend on any classes other than
066     * in J2ME, so it can be extracted independently into a smaller package.
067     *
068     * <p>MicroBurlapInput is unbuffered, so any client needs to provide
069     * its own buffering.
070     *
071     * <pre>
072     * InputStream is = ...; // from http connection
073     * MicroBurlapInput in = new MicroBurlapInput(is);
074     * String value;
075     *
076     * in.startReply();         // read reply header
077     * value = in.readString(); // read string value
078     * in.completeReply();      // read reply footer
079     * </pre>
080     */
081    public class MicroBurlapInput {
082      private static int base64Decode[];
083      
084      private InputStream is;
085      protected int peek;
086      protected boolean peekTag;
087      protected Date date;
088      protected Calendar utcCalendar;
089      private Calendar localCalendar;
090      protected Vector refs;
091      protected String method;
092      protected StringBuffer sbuf = new StringBuffer();
093      protected StringBuffer entity = new StringBuffer();
094    
095      /**
096       * Creates a new Burlap input stream, initialized with an
097       * underlying input stream.
098       *
099       * @param is the underlying input stream.
100       */
101      public MicroBurlapInput(InputStream is)
102      {
103        init(is);
104      }
105    
106      /**
107       * Creates an uninitialized Burlap input stream.
108       */
109      public MicroBurlapInput()
110      {
111      }
112    
113      /**
114       * Returns a call's method.
115       */
116      public String getMethod()
117      {
118        return method;
119      }
120    
121      /**
122       * Initialize the Burlap input stream with a new underlying stream.
123       * Applications can use <code>init(InputStream)</code> to reuse
124       * MicroBurlapInput to save garbage collection.
125       */
126      public void init(InputStream is)
127      {
128        this.is = is;
129        this.refs = null;
130      }
131    
132      /**
133       * Starts reading the call
134       *
135       * <p>A successful completion will have a single value:
136       *
137       * <pre>
138       * &lt;burlap:call>
139       * &lt;method>method&lt;/method>
140       * </pre>
141       */
142      public void startCall()
143        throws IOException
144      {
145        expectStartTag("burlap:call");
146        expectStartTag("method");
147        method = parseString();
148        expectEndTag("method");
149        this.refs = null;
150      }
151    
152      /**
153       * Completes reading the call.
154       *
155       * <pre>
156       * &lt;/burlap:call>
157       * </pre>
158       */
159      public void completeCall()
160        throws IOException
161      {
162        expectEndTag("burlap:call");
163      }
164    
165      /**
166       * Reads a reply as an object.
167       * If the reply has a fault, throws the exception.
168       */
169      public Object readReply(Class expectedClass)
170        throws Exception
171      {
172        if (startReply()) {
173          Object value = readObject(expectedClass);
174          completeReply();
175          return value;
176        }
177        else {
178          Hashtable fault = readFault();
179    
180          Object detail = fault.get("detail");
181          if (detail instanceof Exception)
182            throw (Exception) detail;
183    
184          else {
185            String code = (String) fault.get("code");
186            String message = (String) fault.get("message");
187            
188            throw new BurlapServiceException(message, code, detail);
189          }
190        }
191      }
192    
193      /**
194       * Starts reading the reply.
195       *
196       * <p>A successful completion will have a single value.  An unsuccessful
197       * one will have a fault:
198       *
199       * <pre>
200       * &lt;burlap:reply>
201       * </pre>
202       *
203       * @return true if success, false for fault.
204       */
205      public boolean startReply()
206        throws IOException
207      {
208        this.refs = null;
209        
210        expectStartTag("burlap:reply");
211    
212        if (! parseTag())
213          throw new BurlapProtocolException("expected <value>");
214    
215        String tag = sbuf.toString();
216        if (tag.equals("fault")) {
217          peekTag = true;
218          return false;
219        }
220        else {
221          peekTag = true;
222          return true;
223        }
224      }
225    
226      /**
227       * Completes reading the reply.
228       *
229       * <pre>
230       * &lt;/burlap:reply>
231       * </pre>
232       */
233      public void completeReply()
234        throws IOException
235      {
236        expectEndTag("burlap:reply");
237      }
238    
239      /**
240       * Reads a boolean value from the input stream.
241       */
242      public boolean readBoolean()
243        throws IOException
244      {
245        expectStartTag("boolean");
246    
247        int value = parseInt();
248        
249        expectEndTag("boolean");
250    
251        return value != 0;
252      }
253    
254      /**
255       * Reads an integer value from the input stream.
256       */
257      public int readInt()
258        throws IOException
259      {
260        expectStartTag("int");
261    
262        int value = parseInt();
263        
264        expectEndTag("int");
265    
266        return value;
267      }
268    
269      /**
270       * Reads a long value from the input stream.
271       */
272      public long readLong()
273        throws IOException
274      {
275        expectStartTag("long");
276    
277        long value = parseLong();
278        
279        expectEndTag("long");
280    
281        return value;
282      }
283    
284      /**
285       * Reads a date value from the input stream.
286       */
287      public long readUTCDate()
288        throws IOException
289      {
290        expectStartTag("date");
291    
292        if (utcCalendar == null)
293          utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
294    
295        long value = parseDate(utcCalendar);
296        
297        expectEndTag("date");
298    
299        return value;
300      }
301    
302      /**
303       * Reads a date value from the input stream.
304       */
305      public long readLocalDate()
306        throws IOException
307      {
308        expectStartTag("date");
309    
310        if (localCalendar == null)
311          localCalendar = Calendar.getInstance();
312    
313        long value = parseDate(localCalendar);
314        
315        expectEndTag("date");
316    
317        return value;
318      }
319    
320      /**
321       * Reads a remote value from the input stream.
322       */
323      public BurlapRemote readRemote()
324        throws IOException
325      {
326        expectStartTag("remote");
327    
328        String type = readType();
329        String url = readString();
330        
331        expectEndTag("remote");
332    
333        return new BurlapRemote(type, url);
334      }
335    
336      /**
337       * Reads a string value from the input stream.
338       *
339       * <p>The two valid possibilities are either a &lt;null>
340       * or a &lt;string>.  The string value is encoded in utf-8, and
341       * understands the basic XML escapes: "&123;", "&lt;", "&gt;",
342       * "&apos;", "&quot;".
343       *
344       * <pre>
345       * &lt;null>&lt;/null>
346       * &lt;string>a utf-8 encoded string&lt;/string>
347       * </pre>
348       */
349      public String readString()
350        throws IOException
351      {
352        if (! parseTag())
353          throw new BurlapProtocolException("expected <string>");
354    
355        String tag = sbuf.toString();
356        if (tag.equals("null")) {
357          expectEndTag("null");
358          return null;
359        }
360        else if (tag.equals("string")) {
361          sbuf.setLength(0);
362          parseString(sbuf);
363          String value = sbuf.toString();
364          expectEndTag("string");
365          return value;
366        }
367        else
368          throw expectBeginTag("string", tag);
369      }
370    
371      /**
372       * Reads a byte array from the input stream.
373       *
374       * <p>The two valid possibilities are either a &lt;null>
375       * or a &lt;base64>.
376       */
377      public byte []readBytes()
378        throws IOException
379      {
380        if (! parseTag())
381          throw new BurlapProtocolException("expected <base64>");
382    
383        String tag = sbuf.toString();
384        if (tag.equals("null")) {
385          expectEndTag("null");
386          return null;
387        }
388        else if (tag.equals("base64")) {
389          sbuf.setLength(0);
390          byte []value = parseBytes();
391          expectEndTag("base64");
392          return value;
393        }
394        else
395          throw expectBeginTag("base64", tag);
396      }
397    
398      /**
399       * Reads an arbitrary object the input stream.
400       */
401      public Object readObject(Class expectedClass)
402        throws IOException
403      {
404        if (! parseTag())
405          throw new BurlapProtocolException("expected <tag>");
406    
407        String tag = sbuf.toString();
408        if (tag.equals("null")) {
409          expectEndTag("null");
410          return null;
411        }
412        else if (tag.equals("boolean")) {
413          int value = parseInt();
414          expectEndTag("boolean");
415          return new Boolean(value != 0);
416        }
417        else if (tag.equals("int")) {
418          int value = parseInt();
419          expectEndTag("int");
420          return new Integer(value);
421        }
422        else if (tag.equals("long")) {
423          long value = parseLong();
424          expectEndTag("long");
425          return new Long(value);
426        }
427        else if (tag.equals("string")) {
428          sbuf.setLength(0);
429          parseString(sbuf);
430          String value = sbuf.toString();
431          expectEndTag("string");
432          return value;
433        }
434        else if (tag.equals("xml")) {
435          sbuf.setLength(0);
436          parseString(sbuf);
437          String value = sbuf.toString();
438          expectEndTag("xml");
439          return value;
440        }
441        else if (tag.equals("date")) {
442          if (utcCalendar == null)
443            utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
444          
445          long value = parseDate(utcCalendar);
446          expectEndTag("date");
447          return new Date(value);
448        }
449        else if (tag.equals("map")) {
450          String type = readType();
451    
452          return readMap(expectedClass, type);
453        }
454        else if (tag.equals("list")) {
455          String type = readType();
456          
457          int length = readLength();
458    
459          return readList(expectedClass, type, length);
460        }
461        else if (tag.equals("ref")) {
462          int value = parseInt();
463          expectEndTag("ref");
464    
465          return refs.elementAt(value);
466        }
467        else if (tag.equals("remote")) {
468          String type = readType();
469          String url = readString();
470    
471          expectEndTag("remote");
472    
473          return resolveRemote(type, url);
474        }
475        else
476          return readExtensionObject(expectedClass, tag);
477      }
478    
479      /**
480       * Reads a type value from the input stream.
481       *
482       * <pre>
483       * &lt;type>a utf-8 encoded string&lt;/type>
484       * </pre>
485       */
486      public String readType()
487        throws IOException
488      {
489        if (! parseTag())
490          throw new BurlapProtocolException("expected <type>");
491    
492        String tag = sbuf.toString();
493        if (! tag.equals("type"))
494          throw new BurlapProtocolException("expected <type>");
495        
496        sbuf.setLength(0);
497        parseString(sbuf);
498        String value = sbuf.toString();
499        expectEndTag("type");
500        
501        return value;
502      }
503    
504      /**
505       * Reads a length value from the input stream.  If the length isn't
506       * specified, returns -1.
507       *
508       * <pre>
509       * &lt;length>integer&lt;/length>
510       * </pre>
511       */
512      public int readLength()
513        throws IOException
514      {
515        expectStartTag("length");
516    
517        int ch = skipWhitespace();
518    
519        peek = ch;
520        
521        if (ch == '<') {
522          expectEndTag("length");
523          return -1;
524        }
525    
526        int value = parseInt();
527          
528        expectEndTag("length");
529        
530        return value;
531      }
532    
533      /**
534       * Resolves a remote object.
535       */
536      public Object resolveRemote(String type, String url)
537        throws IOException
538      {
539        return new BurlapRemote(type, url);
540      }
541    
542      /**
543       * Reads a fault.
544       */
545      public Hashtable readFault()
546        throws IOException
547      {
548        expectStartTag("fault");
549        
550        Hashtable map = new Hashtable();
551    
552        while (parseTag()) {
553          peekTag = true;
554          Object key = readObject(null);
555          Object value = readObject(null);
556    
557          if (key != null && value != null)
558            map.put(key, value);
559        }
560        
561        if (! sbuf.toString().equals("fault"))
562          throw new BurlapProtocolException("expected </fault>");
563    
564        return map;
565      }
566      
567      /**
568       * Reads an object from the input stream.
569       *
570       * @param expectedClass the calling routine's expected class
571       * @param type the type from the stream
572       */
573      public Object readMap(Class expectedClass, String type)
574        throws IOException
575      {
576        Hashtable map = new Hashtable();
577        if (refs == null)
578          refs = new Vector();
579        refs.addElement(map);
580    
581        while (parseTag()) {
582          peekTag = true;
583          Object key = readObject(null);
584          Object value = readObject(null);
585    
586          map.put(key, value);
587        }
588        if (! sbuf.toString().equals("map"))
589          throw new BurlapProtocolException("expected </map>");
590    
591        return map;
592      }
593    
594      /**
595       * Reads object unknown to MicroBurlapInput.
596       */
597      protected Object readExtensionObject(Class expectedClass, String tag)
598        throws IOException
599      {
600        throw new BurlapProtocolException("unknown object tag <" + tag + ">");
601      }
602      
603      /**
604       * Reads a list object from the input stream.
605       *
606       * @param expectedClass the calling routine's expected class
607       * @param type the type from the stream
608       * @param length the expected length, -1 for unspecified length
609       */
610      public Object readList(Class expectedClass, String type, int length)
611        throws IOException
612      {
613        Vector list = new Vector();
614        if (refs == null)
615          refs = new Vector();
616        refs.addElement(list);
617    
618        while (parseTag()) {
619          peekTag = true;
620          Object value = readObject(null);
621    
622          list.addElement(value);
623        }
624        
625        if (! sbuf.toString().equals("list"))
626          throw new BurlapProtocolException("expected </list>");
627    
628        return list;
629      }
630    
631      /**
632       * Parses an integer value from the stream.
633       */
634      protected int parseInt()
635        throws IOException
636      {
637        int sign = 1;
638        int value = 0;
639    
640        int ch = skipWhitespace();
641        if (ch == '+')
642          ch = read();
643        else if (ch == '-') {
644          sign = -1;
645          ch = read();
646        }
647    
648        for (; ch >= '0' && ch <= '9'; ch = read())
649          value = 10 * value + ch - '0';
650        
651        peek = ch;
652    
653        return sign * value;
654      }
655    
656      /**
657       * Parses a long value from the stream.
658       */
659      protected long parseLong()
660        throws IOException
661      {
662        long sign = 1;
663        long value = 0;
664    
665        int ch = skipWhitespace();
666        if (ch == '+')
667          ch = read();
668        else if (ch == '-') {
669          sign = -1;
670          ch = read();
671        }
672    
673        for (; ch >= '0' && ch <= '9'; ch = read()) {
674          value = 10 * value + ch - '0';
675        }
676    
677        peek = ch;
678    
679        return sign * value;
680      }
681    
682      /**
683       * Parses a date value from the stream.
684       */
685      protected long parseDate(Calendar calendar)
686        throws IOException
687      {
688        int ch = skipWhitespace();
689        
690        int year = 0;
691        for (int i = 0; i < 4; i++) {
692          if (ch >= '0' && ch <= '9')
693            year = 10 * year + ch - '0';
694          else
695            throw expectedChar("year", ch);
696    
697          ch = read();
698        }
699    
700        int month = 0;
701        for (int i = 0; i < 2; i++) {
702          if (ch >= '0' && ch <= '9')
703            month = 10 * month + ch - '0';
704          else
705            throw expectedChar("month", ch);
706    
707          ch = read();
708        }
709    
710        int day = 0;
711        for (int i = 0; i < 2; i++) {
712          if (ch >= '0' && ch <= '9')
713            day = 10 * day + ch - '0';
714          else
715            throw expectedChar("day", ch);
716    
717          ch = read();
718        }
719    
720        if (ch != 'T')
721          throw expectedChar("`T'", ch);
722    
723        ch = read();
724    
725        int hour = 0;
726        for (int i = 0; i < 2; i++) {
727          if (ch >= '0' && ch <= '9')
728            hour = 10 * hour + ch - '0';
729          else
730            throw expectedChar("hour", ch);
731    
732          ch = read();
733        }
734    
735        int minute = 0;
736        for (int i = 0; i < 2; i++) {
737          if (ch >= '0' && ch <= '9')
738            minute = 10 * minute + ch - '0';
739          else
740            throw expectedChar("minute", ch);
741    
742          ch = read();
743        }
744    
745        int second = 0;
746        for (int i = 0; i < 2; i++) {
747          if (ch >= '0' && ch <= '9')
748            second = 10 * second + ch - '0';
749          else
750            throw expectedChar("second", ch);
751    
752          ch = read();
753        }
754    
755        for (; ch > 0 && ch != '<'; ch = read()) {
756        }
757    
758        peek = ch;
759    
760        calendar.set(Calendar.YEAR, year);
761        calendar.set(Calendar.MONTH, month - 1);
762        calendar.set(Calendar.DAY_OF_MONTH, day);
763        calendar.set(Calendar.HOUR_OF_DAY, hour);
764        calendar.set(Calendar.MINUTE, minute);
765        calendar.set(Calendar.SECOND, second);
766        calendar.set(Calendar.MILLISECOND, 0);
767    
768        return calendar.getTime().getTime();
769      }
770    
771      /**
772       * Parses a string value from the stream.
773       * string buffer is used for the result.
774       */
775      protected String parseString()
776        throws IOException
777      {
778        StringBuffer sbuf = new StringBuffer();
779    
780        return parseString(sbuf).toString();
781      }
782      
783      /**
784       * Parses a string value from the stream.  The burlap object's
785       * string buffer is used for the result.
786       */
787      protected StringBuffer parseString(StringBuffer sbuf)
788        throws IOException
789      {
790        int ch = read();
791    
792        for (; ch >= 0 && ch != '<'; ch = read()) {
793          if (ch == '&') {
794            ch = read();
795    
796            if (ch == '#') {
797              ch = read();
798    
799              if (ch >= '0' && ch <= '9') {
800                int v = 0;
801                for (; ch >= '0' && ch <= '9'; ch = read()) {
802                  v = 10 * v + ch - '0';
803                }
804    
805                sbuf.append((char) v);
806              }
807            }
808            else {
809              StringBuffer entityBuffer = new StringBuffer();
810    
811              for (; ch >= 'a' && ch <= 'z'; ch = read())
812                entityBuffer.append((char) ch);
813    
814              String entity = entityBuffer.toString();
815              if (entity.equals("amp"))
816                sbuf.append('&');
817              else if (entity.equals("apos"))
818                sbuf.append('\'');
819              else if (entity.equals("quot"))
820                sbuf.append('"');
821              else if (entity.equals("lt"))
822                sbuf.append('<');
823              else if (entity.equals("gt"))
824                sbuf.append('>');
825              else
826                throw new BurlapProtocolException("unknown XML entity &" + entity + "; at `" + (char) ch + "'");
827            }
828    
829            if (ch != ';')
830              throw expectedChar("';'", ch);
831          }
832          else if (ch < 0x80)
833            sbuf.append((char) ch);
834          else if ((ch & 0xe0) == 0xc0) {
835            int ch1 = read();
836            int v = ((ch & 0x1f) << 6) + (ch1 & 0x3f);
837    
838            sbuf.append((char) v);
839          }
840          else if ((ch & 0xf0) == 0xe0) {
841            int ch1 = read();
842            int ch2 = read();
843            int v = ((ch & 0x0f) << 12) + ((ch1 & 0x3f) << 6) + (ch2 & 0x3f);
844    
845            sbuf.append((char) v);
846          }
847          else
848            throw new BurlapProtocolException("bad utf-8 encoding");
849        }
850    
851        peek = ch;
852    
853        return sbuf;
854      }
855      
856      /**
857       * Parses a byte array.
858       */
859      protected byte []parseBytes()
860        throws IOException
861      {
862        ByteArrayOutputStream bos = new ByteArrayOutputStream();
863    
864        parseBytes(bos);
865    
866        return bos.toByteArray();
867      }
868      
869      /**
870       * Parses a byte array.
871       */
872      protected ByteArrayOutputStream parseBytes(ByteArrayOutputStream bos)
873        throws IOException
874      {
875        int ch;
876        for (ch = read(); ch >= 0 && ch != '<'; ch = read()) {
877          int b1 = ch;
878          int b2 = read();
879          int b3 = read();
880          int b4 = read();
881    
882          if (b4 != '=') {
883            int chunk = ((base64Decode[b1] << 18) +
884                         (base64Decode[b2] << 12) +
885                         (base64Decode[b3] << 6) +
886                         (base64Decode[b4]));
887    
888            bos.write(chunk >> 16);
889            bos.write(chunk >> 8);
890            bos.write(chunk);
891          }
892          else if (b3 != '=') {
893            int chunk = ((base64Decode[b1] << 12) +
894                         (base64Decode[b2] << 6) +
895                         (base64Decode[b3]));
896    
897            bos.write(chunk >> 8);
898            bos.write(chunk);
899          }
900          else {
901            int chunk = ((base64Decode[b1] << 6) +
902                         (base64Decode[b2]));
903    
904            bos.write(chunk);
905          }
906        }
907    
908        if (ch == '<')
909          peek = ch;
910        
911        return bos;
912      }
913    
914      protected void expectStartTag(String tag)
915        throws IOException
916      {
917        if (! parseTag())
918          throw new BurlapProtocolException("expected <" + tag + ">");
919    
920        if (! sbuf.toString().equals(tag))
921          throw new BurlapProtocolException("expected <" + tag + "> at <" + sbuf + ">");
922      }
923    
924      protected void expectEndTag(String tag)
925        throws IOException
926      {
927        if (parseTag())
928          throw new BurlapProtocolException("expected </" + tag + ">");
929    
930        if (! sbuf.toString().equals(tag))
931          throw new BurlapProtocolException("expected </" + tag + "> at </" + sbuf + ">");
932      }
933    
934      /**
935       * Parses a tag.  Returns true if it's a start tag.
936       */
937      protected boolean parseTag()
938        throws IOException
939      {
940        if (peekTag) {
941          peekTag = false;
942          return true;
943        }
944        
945        int ch = skipWhitespace();
946        boolean isStartTag = true;
947    
948        if (ch != '<')
949          throw expectedChar("'<'", ch);
950    
951        ch = read();
952        if (ch == '/') {
953          isStartTag = false;
954          ch = is.read();
955        }
956        
957        if (! isTagChar(ch))
958          throw expectedChar("tag", ch);
959          
960        sbuf.setLength(0);
961        for (; isTagChar(ch); ch = read())
962          sbuf.append((char) ch);
963    
964        if (ch != '>')
965          throw expectedChar("'>'", ch);
966    
967        return isStartTag;
968      }
969    
970      protected IOException expectedChar(String expect, int actualChar)
971      {
972        return new BurlapProtocolException("expected " + expect + " at " +
973                               (char) actualChar + "'");
974      }
975    
976      protected IOException expectBeginTag(String expect, String tag)
977      {
978        return new BurlapProtocolException("expected <" + expect + "> at <" + tag + ">");
979      }
980    
981      private boolean isTagChar(int ch)
982      {
983        return (ch >= 'a' && ch <= 'z' ||
984                ch >= 'A' && ch <= 'Z' ||
985                ch >= '0' && ch <= '9' ||
986                ch == ':' || ch == '-');
987      }
988    
989      protected int skipWhitespace()
990        throws IOException
991      {
992        int ch = read();
993    
994        for (;
995             ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r';
996             ch = read()) {
997        }
998    
999        return ch;
1000      }
1001    
1002      protected boolean isWhitespace(int ch)
1003        throws IOException
1004      {
1005        return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r';
1006      }
1007    
1008      protected int read()
1009        throws IOException
1010      {
1011        if (peek > 0) {
1012          int value = peek;
1013          peek = 0;
1014          return value;
1015        }
1016        
1017        return is.read();
1018      }
1019    
1020      static {
1021        base64Decode = new int[256];
1022        for (int i = 'A'; i <= 'Z'; i++)
1023          base64Decode[i] = i - 'A';
1024        for (int i = 'a'; i <= 'z'; i++)
1025          base64Decode[i] = i - 'a' + 26;
1026        for (int i = '0'; i <= '9'; i++)
1027          base64Decode[i] = i - '0' + 52;
1028        base64Decode['+'] = 62;
1029        base64Decode['/'] = 63;
1030      }
1031    }