1 /*
  2  * Copyright (c) 2001, 2021, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 
 26 package sun.nio.cs;
 27 
 28 import java.io.IOException;
 29 import java.io.OutputStream;
 30 import java.io.UnsupportedEncodingException;
 31 import java.io.Writer;
 32 import java.nio.ByteBuffer;
 33 import java.nio.CharBuffer;
 34 import java.nio.channels.WritableByteChannel;
 35 import java.nio.charset.Charset;
 36 import java.nio.charset.CharsetEncoder;
 37 import java.nio.charset.CoderResult;
 38 import java.nio.charset.CodingErrorAction;
 39 import java.nio.charset.IllegalCharsetNameException;
 40 import java.nio.charset.UnsupportedCharsetException;
 41 
 42 public class StreamEncoder extends Writer
 43 {
 44 
 45     private static final int DEFAULT_BYTE_BUFFER_SIZE = 8192;
 46 
 47     private volatile boolean closed;
 48 
 49     private void ensureOpen() throws IOException {
 50         if (closed)
 51             throw new IOException("Stream closed");
 52     }
 53 
 54     // Factories for java.io.OutputStreamWriter
 55     public static StreamEncoder forOutputStreamWriter(OutputStream out,
 56                                                       Object lock,
 57                                                       String charsetName)
 58         throws UnsupportedEncodingException
 59     {
 60         String csn = charsetName;
 61         if (csn == null) {
 62             csn = Charset.defaultCharset().name();
 63         }
 64         try {
 65             return new StreamEncoder(out, lock, Charset.forName(csn));
 66         } catch (IllegalCharsetNameException | UnsupportedCharsetException x) {
 67             throw new UnsupportedEncodingException (csn);
 68         }
 69     }
 70 
 71     public static StreamEncoder forOutputStreamWriter(OutputStream out,
 72                                                       Object lock,
 73                                                       Charset cs)
 74     {
 75         return new StreamEncoder(out, lock, cs);
 76     }
 77 
 78     public static StreamEncoder forOutputStreamWriter(OutputStream out,
 79                                                       Object lock,
 80                                                       CharsetEncoder enc)
 81     {
 82         return new StreamEncoder(out, lock, enc);
 83     }
 84 
 85 
 86     // Factory for java.nio.channels.Channels.newWriter
 87 
 88     public static StreamEncoder forEncoder(WritableByteChannel ch,
 89                                            CharsetEncoder enc,
 90                                            int minBufferCap)
 91     {
 92         return new StreamEncoder(ch, enc, minBufferCap);
 93     }
 94 
 95 
 96     // -- Public methods corresponding to those in OutputStreamWriter --
 97 
 98     // All synchronization and state/argument checking is done in these public
 99     // methods; the concrete stream-encoder subclasses defined below need not
100     // do any such checking.
101 
102     public String getEncoding() {
103         if (isOpen())
104             return encodingName();
105         return null;
106     }
107 
108     public void flushBuffer() throws IOException {
109         synchronized (lock) {
110             if (isOpen())
111                 implFlushBuffer();
112             else
113                 throw new IOException("Stream closed");
114         }
115     }
116 
117     public void write(int c) throws IOException {
118         char[] cbuf = new char[1];
119         cbuf[0] = (char) c;
120         write(cbuf, 0, 1);
121     }
122 
123     public void write(char[] cbuf, int off, int len) throws IOException {
124         synchronized (lock) {
125             ensureOpen();
126             if ((off < 0) || (off > cbuf.length) || (len < 0) ||
127                 ((off + len) > cbuf.length) || ((off + len) < 0)) {
128                 throw new IndexOutOfBoundsException();
129             } else if (len == 0) {
130                 return;
131             }
132             implWrite(cbuf, off, len);
133         }
134     }
135 
136     public void write(String str, int off, int len) throws IOException {
137         /* Check the len before creating a char buffer */
138         if (len < 0)
139             throw new IndexOutOfBoundsException();
140         char[] cbuf = new char[len];
141         str.getChars(off, off + len, cbuf, 0);
142         write(cbuf, 0, len);
143     }
144 
145     public void write(CharBuffer cb) throws IOException {
146         int position = cb.position();
147         try {
148             synchronized (lock) {
149                 ensureOpen();
150                 implWrite(cb);
151             }
152         } finally {
153             cb.position(position);
154         }
155     }
156 
157     public void flush() throws IOException {
158         synchronized (lock) {
159             ensureOpen();
160             implFlush();
161         }
162     }
163 
164     public void close() throws IOException {
165         synchronized (lock) {
166             if (closed)
167                 return;
168             try {
169                 implClose();
170             } finally {
171                 closed = true;
172             }
173         }
174     }
175 
176     private boolean isOpen() {
177         return !closed;
178     }
179 
180 
181     // -- Charset-based stream encoder impl --
182 
183     private final Charset cs;
184     private final CharsetEncoder encoder;
185     private final ByteBuffer bb;
186 
187     // Exactly one of these is non-null
188     private final OutputStream out;
189     private final WritableByteChannel ch;
190 
191     // Leftover first char in a surrogate pair
192     private boolean haveLeftoverChar = false;
193     private char leftoverChar;
194     private CharBuffer lcb = null;
195 
196     private StreamEncoder(OutputStream out, Object lock, Charset cs) {
197         this(out, lock,
198             cs.newEncoder()
199                 .onMalformedInput(CodingErrorAction.REPLACE)
200                 .onUnmappableCharacter(CodingErrorAction.REPLACE));
201     }
202 
203     private StreamEncoder(OutputStream out, Object lock, CharsetEncoder enc) {
204         super(lock);
205         this.out = out;
206         this.ch = null;
207         this.cs = enc.charset();
208         this.encoder = enc;
209         this.bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);
210     }
211 
212     private StreamEncoder(WritableByteChannel ch, CharsetEncoder enc, int mbc) {
213         this.out = null;
214         this.ch = ch;
215         this.cs = enc.charset();
216         this.encoder = enc;
217         this.bb = ByteBuffer.allocate(mbc < 0
218                                   ? DEFAULT_BYTE_BUFFER_SIZE
219                                   : mbc);
220     }
221 
222     private void writeBytes() throws IOException {
223         bb.flip();
224         int lim = bb.limit();
225         int pos = bb.position();
226         assert (pos <= lim);
227         int rem = (pos <= lim ? lim - pos : 0);
228 
229         if (rem > 0) {
230             if (ch != null) {
231                 int wc = ch.write(bb);
232                 assert wc == rem : rem;
233             } else {
234                 out.write(bb.array(), bb.arrayOffset() + pos, rem);
235             }
236         }
237         bb.clear();
238     }
239 
240     private void flushLeftoverChar(CharBuffer cb, boolean endOfInput)
241         throws IOException
242     {
243         if (!haveLeftoverChar && !endOfInput)
244             return;
245         if (lcb == null)
246             lcb = CharBuffer.allocate(2);
247         else
248             lcb.clear();
249         if (haveLeftoverChar)
250             lcb.put(leftoverChar);
251         if ((cb != null) && cb.hasRemaining())
252             lcb.put(cb.get());
253         lcb.flip();
254         while (lcb.hasRemaining() || endOfInput) {
255             CoderResult cr = encoder.encode(lcb, bb, endOfInput);
256             if (cr.isUnderflow()) {
257                 if (lcb.hasRemaining()) {
258                     leftoverChar = lcb.get();
259                     if (cb != null && cb.hasRemaining()) {
260                         lcb.clear();
261                         lcb.put(leftoverChar).put(cb.get()).flip();
262                         continue;
263                     }
264                     return;
265                 }
266                 break;
267             }
268             if (cr.isOverflow()) {
269                 assert bb.position() > 0;
270                 writeBytes();
271                 continue;
272             }
273             cr.throwException();
274         }
275         haveLeftoverChar = false;
276     }
277 
278     void implWrite(char[] cbuf, int off, int len)
279         throws IOException
280     {
281         CharBuffer cb = CharBuffer.wrap(cbuf, off, len);
282         implWrite(cb);
283     }
284 
285     void implWrite(CharBuffer cb)
286         throws IOException
287     {
288         if (haveLeftoverChar) {
289             flushLeftoverChar(cb, false);
290         }
291 
292         while (cb.hasRemaining()) {
293             CoderResult cr = encoder.encode(cb, bb, false);
294             if (cr.isUnderflow()) {
295                 assert (cb.remaining() <= 1) : cb.remaining();
296                 if (cb.remaining() == 1) {
297                     haveLeftoverChar = true;
298                     leftoverChar = cb.get();
299                 }
300                 break;
301             }
302             if (cr.isOverflow()) {
303                 assert bb.position() > 0;
304                 writeBytes();
305                 continue;
306             }
307             cr.throwException();
308         }
309     }
310 
311     void implFlushBuffer() throws IOException {
312         if (bb.position() > 0) {
313             writeBytes();
314         }
315     }
316 
317     void implFlush() throws IOException {
318         implFlushBuffer();
319         if (out != null) {
320             out.flush();
321         }
322     }
323 
324     void implClose() throws IOException {
325         flushLeftoverChar(null, true);
326         try {
327             for (;;) {
328                 CoderResult cr = encoder.flush(bb);
329                 if (cr.isUnderflow())
330                     break;
331                 if (cr.isOverflow()) {
332                     assert bb.position() > 0;
333                     writeBytes();
334                     continue;
335                 }
336                 cr.throwException();
337             }
338 
339             if (bb.position() > 0)
340                 writeBytes();
341             if (ch != null)
342                 ch.close();
343             else {
344                 try {
345                     out.flush();
346                 } finally {
347                     out.close();
348                 }
349             }
350         } catch (IOException x) {
351             encoder.reset();
352             throw x;
353         }
354     }
355 
356     String encodingName() {
357         return ((cs instanceof HistoricallyNamedCharset)
358             ? ((HistoricallyNamedCharset)cs).historicalName()
359             : cs.name());
360     }
361 }