001/* OutputStreamWriter.java -- Writer that converts chars to bytes
002   Copyright (C) 1998, 1999, 2000, 2001, 2003, 2005  Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package java.io;
040
041import gnu.java.nio.charset.EncodingHelper;
042
043import java.nio.ByteBuffer;
044import java.nio.CharBuffer;
045import java.nio.charset.CharacterCodingException;
046import java.nio.charset.Charset;
047import java.nio.charset.CharsetEncoder;
048import java.nio.charset.CodingErrorAction;
049import java.nio.charset.MalformedInputException;
050
051/**
052 * This class writes characters to an output stream that is byte oriented
053 * It converts the chars that are written to bytes using an encoding layer,
054 * which is specific to a particular encoding standard.  The desired
055 * encoding can either be specified by name, or if no encoding is specified,
056 * the system default encoding will be used.  The system default encoding
057 * name is determined from the system property <code>file.encoding</code>.
058 * The only encodings that are guaranteed to be available are "8859_1"
059 * (the Latin-1 character set) and "UTF8".  Unfortunately, Java does not
060 * provide a mechanism for listing the encodings that are supported in
061 * a given implementation.
062 * <p>
063 * Here is a list of standard encoding names that may be available:
064 * <p>
065 * <ul>
066 * <li>8859_1 (ISO-8859-1/Latin-1)
067 * <li>8859_2 (ISO-8859-2/Latin-2)
068 * <li>8859_3 (ISO-8859-3/Latin-3)
069 * <li>8859_4 (ISO-8859-4/Latin-4)
070 * <li>8859_5 (ISO-8859-5/Latin-5)
071 * <li>8859_6 (ISO-8859-6/Latin-6)
072 * <li>8859_7 (ISO-8859-7/Latin-7)
073 * <li>8859_8 (ISO-8859-8/Latin-8)
074 * <li>8859_9 (ISO-8859-9/Latin-9)
075 * <li>ASCII (7-bit ASCII)
076 * <li>UTF8 (UCS Transformation Format-8)
077 * <li>More Later
078 * </ul>
079 *
080 * @author Aaron M. Renn (arenn@urbanophile.com)
081 * @author Per Bothner (bothner@cygnus.com)
082 * @date April 17, 1998.
083 */
084public class OutputStreamWriter extends Writer
085{
086  /**
087   * The output stream.
088   */
089  private OutputStream out;
090
091  /**
092   * The charset encoder.
093   */
094  private final CharsetEncoder encoder;
095
096  /**
097   * java.io canonical name of the encoding.
098   */
099  private final String encodingName;
100
101  /**
102   * Buffer output before character conversion as it has costly overhead.
103   */
104  private final CharBuffer outputBuffer;
105  private final static int BUFFER_SIZE = 1024;
106
107  /**
108   * This method initializes a new instance of <code>OutputStreamWriter</code>
109   * to write to the specified stream using a caller supplied character
110   * encoding scheme.  Note that due to a deficiency in the Java language
111   * design, there is no way to determine which encodings are supported.
112   *
113   * @param out The <code>OutputStream</code> to write to
114   * @param encoding_scheme The name of the encoding scheme to use for
115   * character to byte translation
116   *
117   * @exception UnsupportedEncodingException If the named encoding is
118   * not available.
119   */
120  public OutputStreamWriter (OutputStream out, String encoding_scheme)
121    throws UnsupportedEncodingException
122  {
123    CharsetEncoder encoder;
124    String encodingName;
125    this.out = out;
126    outputBuffer = CharBuffer.allocate(BUFFER_SIZE);
127
128    try
129      {
130        // Don't use NIO if avoidable
131        if(EncodingHelper.isISOLatin1(encoding_scheme))
132          {
133            encodingName = "ISO8859_1";
134            encoder = null;
135          }
136       else
137         {
138           /*
139            * Workaround for encodings with a byte-order-mark.
140            * We only want to write it once per stream.
141            */
142           try
143             {
144               if(encoding_scheme.equalsIgnoreCase("UnicodeBig") ||
145                  encoding_scheme.equalsIgnoreCase("UTF-16") ||
146                  encoding_scheme.equalsIgnoreCase("UTF16"))
147                 {
148                   encoding_scheme = "UTF-16BE";
149                   out.write((byte)0xFE);
150                   out.write((byte)0xFF);
151                 }
152               else if(encoding_scheme.equalsIgnoreCase("UnicodeLittle"))
153                 {
154                   encoding_scheme = "UTF-16LE";
155                   out.write((byte)0xFF);
156                   out.write((byte)0xFE);
157                 }
158             }
159           catch(IOException ioe)
160             {
161             }
162
163           Charset cs = EncodingHelper.getCharset(encoding_scheme);
164           if(cs == null)
165             throw new UnsupportedEncodingException("Encoding "+encoding_scheme+
166                                                    " unknown");
167           encoder = cs.newEncoder();
168           encodingName = EncodingHelper.getOldCanonical(cs.name());
169
170           encoder.onMalformedInput(CodingErrorAction.REPLACE);
171           encoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
172         }
173      }
174    catch(RuntimeException e)
175      {
176        // Default to ISO Latin-1, will happen if this is called, for instance,
177        //  before the NIO provider is loadable.
178        encoder = null;
179        encodingName = "ISO8859_1";
180      }
181    this.encoder = encoder;
182    this.encodingName = encodingName;
183  }
184
185  /**
186   * This method initializes a new instance of <code>OutputStreamWriter</code>
187   * to write to the specified stream using the default encoding.
188   *
189   * @param out The <code>OutputStream</code> to write to
190   */
191  public OutputStreamWriter (OutputStream out)
192  {
193    CharsetEncoder encoder;
194    String encodingName;
195    this.out = out;
196    outputBuffer = CharBuffer.allocate(BUFFER_SIZE);
197    try
198      {
199        String encoding = System.getProperty("file.encoding");
200        Charset cs = Charset.forName(encoding);
201        encoder = cs.newEncoder();
202        encodingName =  EncodingHelper.getOldCanonical(cs.name());
203      }
204    catch(RuntimeException e)
205      {
206        encoder = null;
207        encodingName = "ISO8859_1";
208      }
209
210    if(encoder != null)
211      {
212        encoder.onMalformedInput(CodingErrorAction.REPLACE);
213        encoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
214      }
215    this.encoder = encoder;
216    this.encodingName = encodingName;
217  }
218
219  /**
220   * This method initializes a new instance of <code>OutputStreamWriter</code>
221   * to write to the specified stream using a given <code>Charset</code>.
222   *
223   * @param out The <code>OutputStream</code> to write to
224   * @param cs The <code>Charset</code> of the encoding to use
225   *
226   * @since 1.5
227   */
228  public OutputStreamWriter(OutputStream out, Charset cs)
229  {
230    this.out = out;
231    encoder = cs.newEncoder();
232    encoder.onMalformedInput(CodingErrorAction.REPLACE);
233    encoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
234    outputBuffer = CharBuffer.allocate(BUFFER_SIZE);
235    encodingName = EncodingHelper.getOldCanonical(cs.name());
236  }
237
238  /**
239   * This method initializes a new instance of <code>OutputStreamWriter</code>
240   * to write to the specified stream using a given
241   * <code>CharsetEncoder</code>.
242   *
243   * @param out The <code>OutputStream</code> to write to
244   * @param enc The <code>CharsetEncoder</code> to encode the output with
245   *
246   * @since 1.5
247   */
248  public OutputStreamWriter(OutputStream out, CharsetEncoder enc)
249  {
250    this.out = out;
251    encoder = enc;
252    outputBuffer = CharBuffer.allocate(BUFFER_SIZE);
253    Charset cs = enc.charset();
254    if (cs == null)
255      encodingName = "US-ASCII";
256    else
257      encodingName = EncodingHelper.getOldCanonical(cs.name());
258  }
259
260  /**
261   * This method closes this stream, and the underlying
262   * <code>OutputStream</code>
263   *
264   * @exception IOException If an error occurs
265   */
266  public void close () throws IOException
267  {
268    if(out == null)
269      return;
270    flush();
271    out.close ();
272    out = null;
273  }
274
275  /**
276   * This method returns the name of the character encoding scheme currently
277   * in use by this stream.  If the stream has been closed, then this method
278   * may return <code>null</code>.
279   *
280   * @return The encoding scheme name
281   */
282  public String getEncoding ()
283  {
284    return out != null ? encodingName : null;
285  }
286
287  /**
288   * This method flushes any buffered bytes to the underlying output sink.
289   *
290   * @exception IOException If an error occurs
291   */
292  public void flush () throws IOException
293  {
294      if(out != null){
295          if(outputBuffer != null){
296              char[] buf = new char[outputBuffer.position()];
297              if(buf.length > 0){
298                  outputBuffer.flip();
299                  outputBuffer.get(buf);
300                  writeConvert(buf, 0, buf.length);
301                  outputBuffer.clear();
302              }
303          }
304          out.flush ();
305      }
306  }
307
308  /**
309   * This method writes <code>count</code> characters from the specified
310   * array to the output stream starting at position <code>offset</code>
311   * into the array.
312   *
313   * @param buf The array of character to write from
314   * @param offset The offset into the array to start writing chars from
315   * @param count The number of chars to write.
316   *
317   * @exception IOException If an error occurs
318   */
319  public void write (char[] buf, int offset, int count) throws IOException
320  {
321    if(out == null)
322      throw new IOException("Stream is closed.");
323    if(buf == null)
324      throw new IOException("Buffer is null.");
325
326    if(outputBuffer != null)
327        {
328            if(count >= outputBuffer.remaining())
329                {
330                    int r = outputBuffer.remaining();
331                    outputBuffer.put(buf, offset, r);
332                    writeConvert(outputBuffer.array(), 0, BUFFER_SIZE);
333                    outputBuffer.clear();
334                    offset += r;
335                    count -= r;
336                    // if the remaining bytes is larger than the whole buffer,
337                    // just don't buffer.
338                    if(count >= outputBuffer.remaining()){
339                      writeConvert(buf, offset, count);
340                      return;
341                    }
342                }
343            outputBuffer.put(buf, offset, count);
344        } else writeConvert(buf, offset, count);
345  }
346
347 /**
348  * Converts and writes characters.
349  */
350  private void writeConvert (char[] buf, int offset, int count)
351      throws IOException
352  {
353    if(encoder == null)
354    {
355      byte[] b = new byte[count];
356      for(int i=0;i<count;i++)
357        b[i] = nullConversion(buf[offset+i]);
358      out.write(b);
359    } else {
360      try  {
361        ByteBuffer output = encoder.encode(CharBuffer.wrap(buf,offset,count));
362        encoder.reset();
363        if(output.hasArray())
364          out.write(output.array());
365        else
366          {
367            byte[] outbytes = new byte[output.remaining()];
368            output.get(outbytes);
369            out.write(outbytes);
370          }
371      } catch(IllegalStateException e) {
372        throw new IOException("Internal error.");
373      } catch(MalformedInputException e) {
374        throw new IOException("Invalid character sequence.");
375      } catch(CharacterCodingException e) {
376        throw new IOException("Unmappable character.");
377      }
378    }
379  }
380
381  private byte nullConversion(char c) {
382          return (byte)((c <= 0xFF)?c:'?');
383  }
384
385  /**
386   * This method writes <code>count</code> bytes from the specified
387   * <code>String</code> starting at position <code>offset</code> into the
388   * <code>String</code>.
389   *
390   * @param str The <code>String</code> to write chars from
391   * @param offset The position in the <code>String</code> to start
392   * writing chars from
393   * @param count The number of chars to write
394   *
395   * @exception IOException If an error occurs
396   */
397  public void write (String str, int offset, int count) throws IOException
398  {
399    if(str == null)
400      throw new IOException("String is null.");
401
402    write(str.toCharArray(), offset, count);
403  }
404
405  /**
406   * This method writes a single character to the output stream.
407   *
408   * @param ch The char to write, passed as an int.
409   *
410   * @exception IOException If an error occurs
411   */
412  public void write (int ch) throws IOException
413  {
414          // No buffering, no encoding ... just pass through
415          if (encoder == null && outputBuffer == null) {
416                  out.write(nullConversion((char)ch));
417          } else {
418                  if (outputBuffer != null) {
419                          if (outputBuffer.remaining() == 0) {
420                                  writeConvert(outputBuffer.array(), 0, BUFFER_SIZE);
421                                  outputBuffer.clear();
422                          }
423                          outputBuffer.put((char)ch);
424                  } else {
425                      writeConvert(new char[]{ (char)ch }, 0, 1);
426                  }
427          }
428  }
429} // class OutputStreamWriter