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.io;
050    
051    import com.caucho.hessian.io.Serializer;
052    import com.caucho.hessian.io.SerializerFactory;
053    
054    import java.io.IOException;
055    import java.io.OutputStream;
056    import java.util.Calendar;
057    import java.util.Date;
058    import java.util.IdentityHashMap;
059    import java.util.TimeZone;
060    
061    /**
062     * Output stream for Burlap requests, compatible with microedition
063     * Java.  It only uses classes and types available in JDK.
064     *
065     * <p>Since BurlapOutput does not depend on any classes other than
066     * in the JDK, it can be extracted independently into a smaller package.
067     *
068     * <p>BurlapOutput is unbuffered, so any client needs to provide
069     * its own buffering.
070     *
071     * <pre>
072     * OutputStream os = ...; // from http connection
073     * BurlapOutput out = new BurlapOutput(os);
074     * String value;
075     *
076     * out.startCall("hello");  // start hello call
077     * out.writeString("arg1"); // write a string argument
078     * out.completeCall();      // complete the call
079     * </pre>
080     */
081    public class BurlapOutput extends AbstractBurlapOutput {
082      // the output stream
083      protected OutputStream os;
084      // map of references
085      private IdentityHashMap _refs;
086    
087      private Date date;
088      private Calendar utcCalendar;
089      private Calendar localCalendar;
090      /**
091       * Creates a new Burlap output stream, initialized with an
092       * underlying output stream.
093       *
094       * @param os the underlying output stream.
095       */
096      public BurlapOutput(OutputStream os)
097      {
098        init(os);
099      }
100    
101      /**
102       * Creates an uninitialized Burlap output stream.
103       */
104      public BurlapOutput()
105      {
106      }
107    
108      /**
109       * Initializes the output
110       */
111      public void init(OutputStream os)
112      {
113        this.os = os;
114    
115        _refs = null;
116    
117        if (_serializerFactory == null)
118          _serializerFactory = new SerializerFactory();
119      }
120    
121      /**
122       * Writes a complete method call.
123       */
124      public void call(String method, Object []args)
125        throws IOException
126      {
127        startCall(method);
128        
129        if (args != null) {
130          for (int i = 0; i < args.length; i++)
131            writeObject(args[i]);
132        }
133        
134        completeCall();
135      }
136    
137      /**
138       * Starts the method call.  Clients would use <code>startCall</code>
139       * instead of <code>call</code> if they wanted finer control over
140       * writing the arguments, or needed to write headers.
141       *
142       * <code><pre>
143       * &lt;burlap:call>
144       * &lt;method>method-name&lt;/method>
145       * </pre></code>
146       *
147       * @param method the method name to call.
148       */
149      public void startCall(String method)
150        throws IOException
151      {
152        print("<burlap:call><method>");
153        print(method);
154        print("</method>");
155      }
156    
157      /**
158       * Starts the method call.  Clients would use <code>startCall</code>
159       * instead of <code>call</code> if they wanted finer control over
160       * writing the arguments, or needed to write headers.
161       *
162       * <code><pre>
163       * &lt;method>method-name&lt;/method>
164       * </pre></code>
165       *
166       * @param method the method name to call.
167       */
168      public void startCall()
169        throws IOException
170      {
171        print("<burlap:call>");
172      }
173    
174      /**
175       * Writes the method for a call.
176       *
177       * <code><pre>
178       * &lt;method>value&lt;/method>
179       * </pre></code>
180       *
181       * @param method the method name to call.
182       */
183      public void writeMethod(String method)
184        throws IOException
185      {
186        print("<method>");
187        print(method);
188        print("</method>");
189      }
190      
191    
192      /**
193       * Completes.
194       *
195       * <code><pre>
196       * &lt;/burlap:call>
197       * </pre></code>
198       */
199      public void completeCall()
200        throws IOException
201      {
202        print("</burlap:call>");
203      }
204    
205      /**
206       * Starts the reply
207       *
208       * <p>A successful completion will have a single value:
209       *
210       * <pre>
211       * r
212       * </pre>
213       */
214      public void startReply()
215        throws IOException
216      {
217        print("<burlap:reply>");
218      }
219    
220      /**
221       * Completes reading the reply
222       *
223       * <p>A successful completion will have a single value:
224       *
225       * <pre>
226       * &lt;/burlap:reply>
227       * </pre>
228       */
229      public void completeReply()
230        throws IOException
231      {
232        print("</burlap:reply>");
233      }
234    
235      /**
236       * Writes a header name.  The header value must immediately follow.
237       *
238       * <code><pre>
239       * &lt;header>foo&lt;/header>&lt;int>value&lt;/int>
240       * </pre></code>
241       */
242      public void writeHeader(String name)
243        throws IOException
244      {
245        print("<header>");
246        printString(name);
247        print("</header>");
248      }
249    
250      /**
251       * Writes a fault.  The fault will be written
252       * as a descriptive string followed by an object:
253       *
254       * <code><pre>
255       * &lt;fault>
256       * &lt;string>code
257       * &lt;string>the fault code
258       *
259       * &lt;string>message
260       * &lt;string>the fault mesage
261       *
262       * &lt;string>detail
263       * &lt;map>t\x00\xnnjavax.ejb.FinderException
264       *     ...
265       * &lt;/map>
266       * &lt;/fault>
267       * </pre></code>
268       *
269       * @param code the fault code, a three digit
270       */
271      public void writeFault(String code, String message, Object detail)
272        throws IOException
273      {
274        print("<fault>");
275        writeString("code");
276        writeString(code);
277    
278        writeString("message");
279        writeString(message);
280    
281        if (detail != null) {
282          writeString("detail");
283          writeObject(detail);
284        }
285        print("</fault>");
286      }
287    
288      /**
289       * Writes any object to the output stream.
290       */
291      public void writeObject(Object object)
292        throws IOException
293      {
294        if (object == null) {
295          writeNull();
296          return;
297        }
298    
299        Serializer serializer;
300    
301        serializer = _serializerFactory.getSerializer(object.getClass());
302    
303        serializer.writeObject(object, this);
304      }
305    
306      /**
307       * Writes the list header to the stream.  List writers will call
308       * <code>writeListBegin</code> followed by the list contents and then
309       * call <code>writeListEnd</code>.
310       *
311       * <code><pre>
312       * &lt;list>
313       *   &lt;type>java.util.ArrayList&lt;/type>
314       *   &lt;length>3&lt;/length>
315       *   &lt;int>1&lt;/int>
316       *   &lt;int>2&lt;/int>
317       *   &lt;int>3&lt;/int>
318       * &lt;/list>
319       * </pre></code>
320       */
321      public boolean writeListBegin(int length, String type)
322        throws IOException
323      {
324        print("<list><type>");
325        
326        if (type != null)
327          print(type);
328        
329        print("</type><length>");
330        print(length);
331        print("</length>");
332    
333        return true;
334      }
335    
336      /**
337       * Writes the tail of the list to the stream.
338       */
339      public void writeListEnd()
340        throws IOException
341      {
342        print("</list>");
343      }
344    
345      /**
346       * Writes the map header to the stream.  Map writers will call
347       * <code>writeMapBegin</code> followed by the map contents and then
348       * call <code>writeMapEnd</code>.
349       *
350       * <code><pre>
351       * &lt;map>
352       *   &lt;type>type&lt;/type>
353       *   (&lt;key> &lt;value>)*
354       * &lt;/map>
355       * </pre></code>
356       */
357      public void writeMapBegin(String type)
358        throws IOException
359      {
360        print("<map><type>");
361        if (type != null)
362          print(type);
363        
364        print("</type>");
365      }
366    
367      /**
368       * Writes the tail of the map to the stream.
369       */
370      public void writeMapEnd()
371        throws IOException
372      {
373        print("</map>");
374      }
375    
376      /**
377       * Writes a remote object reference to the stream.  The type is the
378       * type of the remote interface.
379       *
380       * <code><pre>
381       * &lt;remote>
382       *   &lt;type>test.account.Account&lt;/type>
383       *   &lt;string>http://caucho.com/foo;ejbid=bar&lt;/string>
384       * &lt;/remote>
385       * </pre></code>
386       */
387      public void writeRemote(String type, String url)
388        throws IOException
389      {
390        print("<remote><type>");
391        print(type);
392        print("</type><string>");
393        print(url);
394        print("</string></remote>");
395      }
396    
397      /**
398       * Writes a boolean value to the stream.  The boolean will be written
399       * with the following syntax:
400       *
401       * <code><pre>
402       * &lt;boolean>0&lt;/boolean>
403       * &lt;boolean>1&lt;/boolean>
404       * </pre></code>
405       *
406       * @param value the boolean value to write.
407       */
408      public void writeBoolean(boolean value)
409        throws IOException
410      {
411        if (value)
412          print("<boolean>1</boolean>");
413        else
414          print("<boolean>0</boolean>");
415      }
416    
417      /**
418       * Writes an integer value to the stream.  The integer will be written
419       * with the following syntax:
420       *
421       * <code><pre>
422       * &lt;int>int value&lt;/int>
423       * </pre></code>
424       *
425       * @param value the integer value to write.
426       */
427      public void writeInt(int value)
428        throws IOException
429      {
430        print("<int>");
431        print(value);
432        print("</int>");
433      }
434    
435      /**
436       * Writes a long value to the stream.  The long will be written
437       * with the following syntax:
438       *
439       * <code><pre>
440       * &lt;long>int value&lt;/long>
441       * </pre></code>
442       *
443       * @param value the long value to write.
444       */
445      public void writeLong(long value)
446        throws IOException
447      {
448        print("<long>");
449        print(value);
450        print("</long>");
451      }
452    
453      /**
454       * Writes a double value to the stream.  The double will be written
455       * with the following syntax:
456       *
457       * <code><pre>
458       * &lt;double>value&lt;/double>
459       * </pre></code>
460       *
461       * @param value the double value to write.
462       */
463      public void writeDouble(double value)
464        throws IOException
465      {
466        print("<double>");
467        print(value);
468        print("</double>");
469      }
470    
471      /**
472       * Writes a date to the stream.
473       *
474       * <code><pre>
475       * &lt;date>iso8901&lt;/date>
476       * </pre></code>
477       *
478       * @param time the date in milliseconds from the epoch in UTC
479       */
480      public void writeUTCDate(long time)
481        throws IOException
482      {
483        print("<date>");
484        if (utcCalendar == null) {
485          utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
486          date = new Date();
487        }
488    
489        date.setTime(time);
490        utcCalendar.setTime(date);
491    
492        printDate(utcCalendar);
493        print("</date>");
494      }
495    
496      /**
497       * Writes a null value to the stream.
498       * The null will be written with the following syntax
499       *
500       * <code><pre>
501       * &lt;null>&lt;/null>
502       * </pre></code>
503       *
504       * @param value the string value to write.
505       */
506      public void writeNull()
507        throws IOException
508      {
509        print("<null></null>");
510      }
511    
512      /**
513       * Writes a string value to the stream using UTF-8 encoding.
514       * The string will be written with the following syntax:
515       *
516       * <code><pre>
517       * &lt;string>string-value&lt;/string>
518       * </pre></code>
519       *
520       * If the value is null, it will be written as
521       *
522       * <code><pre>
523       * &lt;null>&lt;/null>
524       * </pre></code>
525       *
526       * @param value the string value to write.
527       */
528      public void writeString(String value)
529        throws IOException
530      {
531        if (value == null) {
532          print("<null></null>");
533        }
534        else {
535          print("<string>");
536          printString(value);
537          print("</string>");
538        }
539      }
540    
541      /**
542       * Writes a string value to the stream using UTF-8 encoding.
543       * The string will be written with the following syntax:
544       *
545       * <code><pre>
546       * S b16 b8 string-value
547       * </pre></code>
548       *
549       * If the value is null, it will be written as
550       *
551       * <code><pre>
552       * N
553       * </pre></code>
554       *
555       * @param value the string value to write.
556       */
557      public void writeString(char []buffer, int offset, int length)
558        throws IOException
559      {
560        if (buffer == null) {
561          print("<null></null>");
562        }
563        else {
564          print("<string>");
565          printString(buffer, offset, length);
566          print("</string>");
567        }
568      }
569    
570      /**
571       * Writes a byte array to the stream.
572       * The array will be written with the following syntax:
573       *
574       * <code><pre>
575       * &lt;base64>bytes&lt;/base64>
576       * </pre></code>
577       *
578       * If the value is null, it will be written as
579       *
580       * <code><pre>
581       * &lt;null>&lt;/null>
582       * </pre></code>
583       *
584       * @param value the string value to write.
585       */
586      public void writeBytes(byte []buffer)
587        throws IOException
588      {
589        if (buffer == null)
590          print("<null></null>");
591        else
592          writeBytes(buffer, 0, buffer.length);
593      }
594      /**
595       * Writes a byte array to the stream.
596       * The array will be written with the following syntax:
597       *
598       * <code><pre>
599       * &lt;base64>bytes&lt;/base64>
600       * </pre></code>
601       *
602       * If the value is null, it will be written as
603       *
604       * <code><pre>
605       * &lt;null>&lt;/null>
606       * </pre></code>
607       *
608       * @param value the string value to write.
609       */
610      public void writeBytes(byte []buffer, int offset, int length)
611        throws IOException
612      {
613        if (buffer == null) {
614          print("<null></null>");
615        }
616        else {
617          print("<base64>");
618    
619          int i = 0;
620          for (; i + 2 < length; i += 3) {
621            if (i != 0 && (i & 0x3f) == 0)
622              print('\n');
623    
624            int v = (((buffer[offset + i] & 0xff) << 16) +
625                     ((buffer[offset + i + 1] & 0xff) << 8) + 
626                     (buffer[offset + i + 2] & 0xff));
627    
628            print(encode(v >> 18));
629            print(encode(v >> 12));
630            print(encode(v >> 6));
631            print(encode(v));
632          }
633    
634          if (i + 1 < length) {
635            int v = (((buffer[offset + i] & 0xff) << 8) +
636                     (buffer[offset + i + 1] & 0xff));
637    
638            print(encode(v >> 10));
639            print(encode(v >> 4));
640            print(encode(v << 2));
641            print('=');
642          }
643          else if (i < length) {
644            int v = buffer[offset + i] & 0xff;
645    
646            print(encode(v >> 2));
647            print(encode(v << 4));
648            print('=');
649            print('=');
650          }
651          
652          print("</base64>");
653        }
654      }
655      
656      /**
657       * Writes a byte buffer to the stream.
658       */
659      public void writeByteBufferStart()
660        throws IOException
661      {
662        throw new UnsupportedOperationException();
663      }
664      
665      /**
666       * Writes a byte buffer to the stream.
667       *
668       * <code><pre>
669       * b b16 b18 bytes
670       * </pre></code>
671       */
672      public void writeByteBufferPart(byte []buffer, int offset, int length)
673        throws IOException
674      {
675        throw new UnsupportedOperationException();
676      }
677      
678      /**
679       * Writes a byte buffer to the stream.
680       *
681       * <code><pre>
682       * b b16 b18 bytes
683       * </pre></code>
684       */
685      public void writeByteBufferEnd(byte []buffer, int offset, int length)
686        throws IOException
687      {
688        throw new UnsupportedOperationException();
689      }
690    
691      /**
692       * Encodes a digit
693       */
694      private char encode(int d)
695      {
696        d &= 0x3f;
697        if (d < 26)
698          return (char) (d + 'A');
699        else if (d < 52)
700          return (char) (d + 'a' - 26);
701        else if (d < 62)
702          return (char) (d + '0' - 52);
703        else if (d == 62)
704          return '+';
705        else
706          return '/';
707      }
708    
709      /**
710       * Writes a reference.
711       *
712       * <code><pre>
713       * &lt;ref>int&lt;/ref>
714       * </pre></code>
715       *
716       * @param value the integer value to write.
717       */
718      public void writeRef(int value)
719        throws IOException
720      {
721        print("<ref>");
722        print(value);
723        print("</ref>");
724      }
725    
726      /**
727       * If the object has already been written, just write its ref.
728       *
729       * @return true if we're writing a ref.
730       */
731      public boolean addRef(Object object)
732        throws IOException
733      {
734        if (_refs == null)
735          _refs = new IdentityHashMap();
736    
737        Integer ref = (Integer) _refs.get(object);
738    
739        if (ref != null) {
740          int value = ref.intValue();
741          
742          writeRef(value);
743          return true;
744        }
745        else {
746          _refs.put(object, new Integer(_refs.size()));
747          
748          return false;
749        }
750      }
751      
752      @Override
753      public int getRef(Object obj)
754      {
755        if (_refs == null)
756          return -1;
757        
758        Integer ref = (Integer) _refs.get(obj);
759        
760        if (ref != null)
761          return ref;
762        else
763          return -1;
764      }
765    
766      /**
767       * Removes a reference.
768       */
769      public boolean removeRef(Object obj)
770        throws IOException
771      {
772        if (_refs != null) {
773          _refs.remove(obj);
774    
775          return true;
776        }
777        else
778          return false;
779      }
780    
781      /**
782       * Replaces a reference from one object to another.
783       */
784      public boolean replaceRef(Object oldRef, Object newRef)
785        throws IOException
786      {
787        Integer value = (Integer) _refs.remove(oldRef);
788    
789        if (value != null) {
790          _refs.put(newRef, value);
791          return true;
792        }
793        else
794          return false;
795      }
796    
797      /**
798       * Prints a string to the stream, encoded as UTF-8
799       *
800       * @param v the string to print.
801       */
802      public void printString(String v)
803        throws IOException
804      {
805        printString(v, 0, v.length());
806      }
807      
808      /**
809       * Prints a string to the stream, encoded as UTF-8
810       *
811       * @param v the string to print.
812       */
813      public void printString(String v, int offset, int length)
814        throws IOException
815      {
816        for (int i = 0; i < length; i++) {
817          char ch = v.charAt(i + offset);
818    
819          if (ch == '<') {
820            os.write('&');
821            os.write('#');
822            os.write('6');
823            os.write('0');
824            os.write(';');
825          }
826          else if (ch == '&') {
827            os.write('&');
828            os.write('#');
829            os.write('3');
830            os.write('8');
831            os.write(';');
832          }
833          else if (ch < 0x80)
834            os.write(ch);
835          else if (ch < 0x800) {
836            os.write(0xc0 + ((ch >> 6) & 0x1f));
837            os.write(0x80 + (ch & 0x3f));
838          }
839          else {
840            os.write(0xe0 + ((ch >> 12) & 0xf));
841            os.write(0x80 + ((ch >> 6) & 0x3f));
842            os.write(0x80 + (ch & 0x3f));
843          }
844        }
845      }
846      
847      /**
848       * Prints a string to the stream, encoded as UTF-8
849       *
850       * @param v the string to print.
851       */
852      public void printString(char []v, int offset, int length)
853        throws IOException
854      {
855        for (int i = 0; i < length; i++) {
856          char ch = v[i + offset];
857    
858          if (ch < 0x80)
859            os.write(ch);
860          else if (ch < 0x800) {
861            os.write(0xc0 + ((ch >> 6) & 0x1f));
862            os.write(0x80 + (ch & 0x3f));
863          }
864          else {
865            os.write(0xe0 + ((ch >> 12) & 0xf));
866            os.write(0x80 + ((ch >> 6) & 0x3f));
867            os.write(0x80 + (ch & 0x3f));
868          }
869        }
870      }
871      
872      /**
873       * Prints a date.
874       *
875       * @param date the date to print.
876       */
877      public void printDate(Calendar calendar)
878        throws IOException
879      {
880        int year = calendar.get(Calendar.YEAR);
881    
882        os.write((char) ('0' + (year / 1000 % 10)));
883        os.write((char) ('0' + (year / 100 % 10)));
884        os.write((char) ('0' + (year / 10 % 10)));
885        os.write((char) ('0' + (year % 10)));
886    
887        int month = calendar.get(Calendar.MONTH) + 1;
888        os.write((char) ('0' + (month / 10 % 10)));
889        os.write((char) ('0' + (month % 10)));
890    
891        int day = calendar.get(Calendar.DAY_OF_MONTH);
892        os.write((char) ('0' + (day / 10 % 10)));
893        os.write((char) ('0' + (day % 10)));
894    
895        os.write('T');
896    
897        int hour = calendar.get(Calendar.HOUR_OF_DAY);
898        os.write((char) ('0' + (hour / 10 % 10)));
899        os.write((char) ('0' + (hour % 10)));
900    
901        int minute = calendar.get(Calendar.MINUTE);
902        os.write((char) ('0' + (minute / 10 % 10)));
903        os.write((char) ('0' + (minute % 10)));
904    
905        int second = calendar.get(Calendar.SECOND);
906        os.write((char) ('0' + (second / 10 % 10)));
907        os.write((char) ('0' + (second % 10)));
908    
909        int ms = calendar.get(Calendar.MILLISECOND);
910        os.write('.');
911        os.write((char) ('0' + (ms / 100 % 10)));
912        os.write((char) ('0' + (ms / 10 % 10)));
913        os.write((char) ('0' + (ms % 10)));
914    
915        os.write('Z');
916      }
917      
918      /**
919       * Prints a char to the stream.
920       *
921       * @param v the char to print.
922       */
923      protected void print(char v)
924        throws IOException
925      {
926        os.write(v);
927      }
928      
929      /**
930       * Prints an integer to the stream.
931       *
932       * @param v the integer to print.
933       */
934      protected void print(int v)
935        throws IOException
936      {
937        print(String.valueOf(v));
938      }
939    
940      /**
941       * Prints a long to the stream.
942       *
943       * @param v the long to print.
944       */
945      protected void print(long v)
946        throws IOException
947      {
948        print(String.valueOf(v));
949      }
950    
951      /**
952       * Prints a double to the stream.
953       *
954       * @param v the double to print.
955       */
956      protected void print(double v)
957        throws IOException
958      {
959        print(String.valueOf(v));
960      }
961    
962      /**
963       * Prints a string as ascii to the stream.  Used for tags, etc.
964       * that are known to the ascii.
965       *
966       * @param s the ascii string to print.
967       */
968      protected void print(String s)
969        throws IOException
970      {
971        int len = s.length();
972        for (int i = 0; i < len; i++) {
973          int ch = s.charAt(i);
974    
975          os.write(ch);
976        }
977      }
978    }