001/*
002 * This file is part of the JDrupes non-blocking HTTP Codec
003 * Copyright (C) 2016, 2017  Michael N. Lipp
004 *
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Lesser General Public License as published
007 * by the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
013 * License for more details.
014 *
015 * You should have received a copy of the GNU Lesser General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.httpcodec.util;
020
021import java.nio.ByteBuffer;
022import java.nio.CharBuffer;
023import java.nio.charset.CharacterCodingException;
024import java.nio.charset.Charset;
025import java.nio.charset.CharsetDecoder;
026import java.nio.charset.CoderResult;
027import java.nio.charset.CodingErrorAction;
028
029/**
030 * Provides a wrapper that makes a {@link CharsetDecoder} behave like an
031 * optimized {@code CharsetDecoder}.
032 * <P>
033 * The default implementations of the {@link CharsetDecoder}s in the JRE leave
034 * data in the input buffer if there isn't sufficient data to decode the next
035 * character. The invoker is expected to add more data to the input buffer in
036 * response to the underflow result. This processing model can be used if you
037 * have a single buffer that you fill using some source and drain using the
038 * decoder.
039 * <P>
040 * If you use a model where one buffer is drained by the decoder while in
041 * parallel another buffer is being filled by the source, this processing model
042 * induces some difficulties, because an unknown number of bytes would have to
043 * be copied over from the newly filled buffer to the previously decoded buffer.
044 * This class provides what the documentation of
045 * {@link CharsetDecoder#decodeLoop} calls an "optimized implementation". It
046 * always drains the input buffer completely, caching any bytes that cannot be
047 * decoded (yet) because they don't encode a complete character. When the
048 * {@link #decode(ByteBuffer, CharBuffer, boolean)} method is invoked for the
049 * next time, as many bytes as needed to decode a complete character will be
050 * added to the cached bytes and the now available character is appended to the
051 * output. Then, the rest of the input is bulk processed as usual.
052 */
053public class OptimizedCharsetDecoder {
054
055        private CharsetDecoder backing;
056        private ByteBuffer pending;
057
058        /**
059         * Creates a new instance.
060         * 
061         * @param backing the backing charset decoder
062         */
063        public OptimizedCharsetDecoder(CharsetDecoder backing) {
064                super();
065                this.backing = backing;
066                pending = ByteBuffer.allocate(32);
067        }
068
069        /**
070         * @return the result
071         * @see java.nio.charset.CharsetDecoder#averageCharsPerByte()
072         */
073        public final float averageCharsPerByte() {
074                return backing.averageCharsPerByte();
075        }
076
077        /**
078         * @return the result
079         * @see java.nio.charset.CharsetDecoder#charset()
080         */
081        public final Charset charset() {
082                return backing.charset();
083        }
084
085        /**
086         * @param in the data to decode
087         * @param out the decoded data
088         * @param endOfInput {@code true} if the completes the input
089         * @return the result
090         * @see java.nio.charset.CharsetDecoder#decode(java.nio.ByteBuffer,
091         *      java.nio.CharBuffer, boolean)
092         */
093        public final CoderResult decode(ByteBuffer in, CharBuffer out,
094                boolean endOfInput) {
095                CoderResult result;
096                if (pending.position() > 0) {
097                        if (!out.hasRemaining()) {
098                                // There must be space for "the one" decoded character
099                                return CoderResult.OVERFLOW;
100                        }
101                        while (true) {
102                                if (!in.hasRemaining()) {
103                                        if (!endOfInput) {
104                                                return CoderResult.UNDERFLOW;
105                                        }
106                                        return backing.decode(pending, out, endOfInput);
107                                }
108                                pending.put(in.get());
109                                pending.flip();
110                                result = backing.decode(pending, out, endOfInput);
111                                pending.compact();
112                                if (result.isOverflow() || result.isMalformed()) {
113                                        return result;
114                                }
115                                if (pending.position() > 0) {
116                                        continue;
117                                }
118                                break;
119                        }
120                }
121                // Handle rest of input
122                result = backing.decode(in, out, endOfInput);
123                if (result.isUnderflow() && in.hasRemaining()) {
124                        byte[] remaining = new byte[in.remaining()];
125                        in.get(remaining);
126                        pending.put(remaining);
127                }
128                return result;
129        }
130
131        /**
132         * @param in the data
133         * @return the result
134         * @throws CharacterCodingException if an error occurred
135         * @see java.nio.charset.CharsetDecoder#decode(java.nio.ByteBuffer)
136         */
137        public final CharBuffer decode(ByteBuffer in)
138                throws CharacterCodingException {
139                return backing.decode(in);
140        }
141
142        /**
143         * @return the result
144         * @see java.nio.charset.CharsetDecoder#detectedCharset()
145         */
146        public Charset detectedCharset() {
147                return backing.detectedCharset();
148        }
149
150        /**
151         * @param out the decoded data
152         * @return the result
153         * @see java.nio.charset.CharsetDecoder#flush(java.nio.CharBuffer)
154         */
155        public final CoderResult flush(CharBuffer out) {
156                if (pending.position() > 0) {
157                        // Shouldn't happen, hard to know what to do...
158                        pending.clear();
159                }
160                return backing.flush(out);
161        }
162
163        /**
164         * @return the result
165         * @see java.nio.charset.CharsetDecoder#isAutoDetecting()
166         */
167        public boolean isAutoDetecting() {
168                return backing.isAutoDetecting();
169        }
170
171        /**
172         * @return the result
173         * @see java.nio.charset.CharsetDecoder#isCharsetDetected()
174         */
175        public boolean isCharsetDetected() {
176                return backing.isCharsetDetected();
177        }
178
179        /**
180         * @return the result
181         * @see java.nio.charset.CharsetDecoder#malformedInputAction()
182         */
183        public CodingErrorAction malformedInputAction() {
184                return backing.malformedInputAction();
185        }
186
187        /**
188         * @return the result
189         * @see java.nio.charset.CharsetDecoder#maxCharsPerByte()
190         */
191        public final float maxCharsPerByte() {
192                return backing.maxCharsPerByte();
193        }
194
195        /**
196         * @param newAction the action
197         * @return the result
198         * @see java.nio.charset.CharsetDecoder#onMalformedInput
199         *      (java.nio.charset.CodingErrorAction)
200         */
201        public final CharsetDecoder onMalformedInput(CodingErrorAction newAction) {
202                return backing.onMalformedInput(newAction);
203        }
204
205        /**
206         * @param newAction the action
207         * @return the result
208         * @see java.nio.charset.CharsetDecoder#onUnmappableCharacter
209         *      (java.nio.charset.CodingErrorAction)
210         */
211        public final CharsetDecoder onUnmappableCharacter(
212                CodingErrorAction newAction) {
213                return backing.onUnmappableCharacter(newAction);
214        }
215
216        /**
217         * @param newReplacement the replacement
218         * @return the result
219         * @see java.nio.charset.CharsetDecoder#replaceWith(java.lang.String)
220         */
221        public final CharsetDecoder replaceWith(String newReplacement) {
222                return backing.replaceWith(newReplacement);
223        }
224
225        /**
226         * @return the result
227         * @see java.nio.charset.CharsetDecoder#replacement()
228         */
229        public final String replacement() {
230                return backing.replacement();
231        }
232
233        /**
234         * @return the result
235         * @see java.nio.charset.CharsetDecoder#reset()
236         */
237        public final CharsetDecoder reset() {
238                pending.clear();
239                return backing.reset();
240        }
241
242        /**
243         * @return the result
244         * @see java.lang.Object#toString()
245         */
246        public String toString() {
247                return backing.toString();
248        }
249
250        /**
251         * @return the result
252         * @see java.nio.charset.CharsetDecoder#unmappableCharacterAction()
253         */
254        public CodingErrorAction unmappableCharacterAction() {
255                return backing.unmappableCharacterAction();
256        }
257
258        /* (non-Javadoc)
259         * @see java.lang.Object#hashCode()
260         */
261        @Override
262        public int hashCode() {
263                final int prime = 31;
264                int result = 1;
265                result = prime * result + ((backing == null) ? 0 : backing.hashCode());
266                return result;
267        }
268
269        /* (non-Javadoc)
270         * @see java.lang.Object#equals(java.lang.Object)
271         */
272        @Override
273        public boolean equals(Object obj) {
274                if (this == obj) {
275                        return true;
276                }
277                if (obj == null) {
278                        return false;
279                }
280                if (getClass() != obj.getClass()) {
281                        return false;
282                }
283                OptimizedCharsetDecoder other = (OptimizedCharsetDecoder) obj;
284                if (backing == null) {
285                        if (other.backing != null) {
286                                return false;
287                        }
288                } else if (!backing.equals(other.backing)) {
289                        return false;
290                }
291                return true;
292        }
293
294}