001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.mina.filter.codec.textline;
021
022import java.nio.ByteBuffer;
023import java.nio.CharBuffer;
024import java.nio.charset.CharacterCodingException;
025import java.nio.charset.Charset;
026import java.nio.charset.CharsetDecoder;
027
028import org.apache.mina.core.buffer.BufferDataException;
029import org.apache.mina.core.buffer.IoBuffer;
030import org.apache.mina.core.session.AttributeKey;
031import org.apache.mina.core.session.IoSession;
032import org.apache.mina.filter.codec.ProtocolDecoder;
033import org.apache.mina.filter.codec.ProtocolDecoderException;
034import org.apache.mina.filter.codec.ProtocolDecoderOutput;
035import org.apache.mina.filter.codec.RecoverableProtocolDecoderException;
036
037/**
038 * A {@link ProtocolDecoder} which decodes a text line into a string.
039 *
040 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
041 */
042public class TextLineDecoder implements ProtocolDecoder {
043    private final AttributeKey CONTEXT = new AttributeKey(getClass(), "context");
044
045    private final Charset charset;
046
047    /** The delimiter used to determinate when a line has been fully decoded */
048    private final LineDelimiter delimiter;
049
050    /** An IoBuffer containing the delimiter */
051    private IoBuffer delimBuf;
052
053    /** The default maximum Line length. Default to 1024. */
054    private int maxLineLength = 1024;
055
056    /** The default maximum buffer length. Default to 128 chars. */
057    private int bufferLength = 128;
058
059    /**
060     * Creates a new instance with the current default {@link Charset}
061     * and {@link LineDelimiter#AUTO} delimiter.
062     */
063    public TextLineDecoder() {
064        this(LineDelimiter.AUTO);
065    }
066
067    /**
068     * Creates a new instance with the current default {@link Charset}
069     * and the specified <tt>delimiter</tt>.
070     * 
071     * @param delimiter The line delimiter to use
072     */
073    public TextLineDecoder(String delimiter) {
074        this(new LineDelimiter(delimiter));
075    }
076
077    /**
078     * Creates a new instance with the current default {@link Charset}
079     * and the specified <tt>delimiter</tt>.
080     * 
081     * @param delimiter The line delimiter to use
082     */
083    public TextLineDecoder(LineDelimiter delimiter) {
084        this(Charset.defaultCharset(), delimiter);
085    }
086
087    /**
088     * Creates a new instance with the spcified <tt>charset</tt>
089     * and {@link LineDelimiter#AUTO} delimiter.
090     * 
091     * @param charset The {@link Charset} to use
092     */
093    public TextLineDecoder(Charset charset) {
094        this(charset, LineDelimiter.AUTO);
095    }
096
097    /**
098     * Creates a new instance with the spcified <tt>charset</tt>
099     * and the specified <tt>delimiter</tt>.
100     * 
101     * @param charset The {@link Charset} to use
102     * @param delimiter The line delimiter to use
103     */
104    public TextLineDecoder(Charset charset, String delimiter) {
105        this(charset, new LineDelimiter(delimiter));
106    }
107
108    /**
109     * Creates a new instance with the specified <tt>charset</tt>
110     * and the specified <tt>delimiter</tt>.
111     * 
112     * @param charset The {@link Charset} to use
113     * @param delimiter The line delimiter to use
114     */
115    public TextLineDecoder(Charset charset, LineDelimiter delimiter) {
116        if (charset == null) {
117            throw new IllegalArgumentException("charset parameter shuld not be null");
118        }
119
120        if (delimiter == null) {
121            throw new IllegalArgumentException("delimiter parameter should not be null");
122        }
123
124        this.charset = charset;
125        this.delimiter = delimiter;
126
127        // Convert delimiter to ByteBuffer if not done yet.
128        if (delimBuf == null) {
129            IoBuffer tmp = IoBuffer.allocate(2).setAutoExpand(true);
130
131            try {
132                tmp.putString(delimiter.getValue(), charset.newEncoder());
133            } catch (CharacterCodingException cce) {
134
135            }
136
137            tmp.flip();
138            delimBuf = tmp;
139        }
140    }
141
142    /**
143     * @return the allowed maximum size of the line to be decoded.
144     * If the size of the line to be decoded exceeds this value, the
145     * decoder will throw a {@link BufferDataException}.  The default
146     * value is <tt>1024</tt> (1KB).
147     */
148    public int getMaxLineLength() {
149        return maxLineLength;
150    }
151
152    /**
153     * Sets the allowed maximum size of the line to be decoded.
154     * If the size of the line to be decoded exceeds this value, the
155     * decoder will throw a {@link BufferDataException}.  The default
156     * value is <tt>1024</tt> (1KB).
157     * 
158     * @param maxLineLength The maximum line length
159     */
160    public void setMaxLineLength(int maxLineLength) {
161        if (maxLineLength <= 0) {
162            throw new IllegalArgumentException("maxLineLength (" + maxLineLength + ") should be a positive value");
163        }
164
165        this.maxLineLength = maxLineLength;
166    }
167
168    /**
169     * Sets the default buffer size. This buffer is used in the Context
170     * to store the decoded line.
171     *
172     * @param bufferLength The default bufer size
173     */
174    public void setBufferLength(int bufferLength) {
175        if (bufferLength <= 0) {
176            throw new IllegalArgumentException("bufferLength (" + maxLineLength + ") should be a positive value");
177
178        }
179
180        this.bufferLength = bufferLength;
181    }
182
183    /**
184     * @return the allowed buffer size used to store the decoded line
185     * in the Context instance.
186     */
187    public int getBufferLength() {
188        return bufferLength;
189    }
190
191    /**
192     * {@inheritDoc}
193     */
194    public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
195        Context ctx = getContext(session);
196
197        if (LineDelimiter.AUTO.equals(delimiter)) {
198            decodeAuto(ctx, session, in, out);
199        } else {
200            decodeNormal(ctx, session, in, out);
201        }
202    }
203
204    /**
205     * @return the context for this session
206     * 
207     * @param session The session for which we want the context
208     */
209    private Context getContext(IoSession session) {
210        Context ctx;
211        ctx = (Context) session.getAttribute(CONTEXT);
212
213        if (ctx == null) {
214            ctx = new Context(bufferLength);
215            session.setAttribute(CONTEXT, ctx);
216        }
217
218        return ctx;
219    }
220
221    /**
222     * {@inheritDoc}
223     */
224    public void finishDecode(IoSession session, ProtocolDecoderOutput out) throws Exception {
225        // Do nothing
226    }
227
228    /**
229     * {@inheritDoc}
230     */
231    public void dispose(IoSession session) throws Exception {
232        Context ctx = (Context) session.getAttribute(CONTEXT);
233
234        if (ctx != null) {
235            session.removeAttribute(CONTEXT);
236        }
237    }
238
239    /**
240     * Decode a line using the default delimiter on the current system
241     */
242    private void decodeAuto(Context ctx, IoSession session, IoBuffer in, ProtocolDecoderOutput out)
243            throws CharacterCodingException, ProtocolDecoderException {
244        int matchCount = ctx.getMatchCount();
245
246        // Try to find a match
247        int oldPos = in.position();
248        int oldLimit = in.limit();
249
250        while (in.hasRemaining()) {
251            byte b = in.get();
252            boolean matched = false;
253
254            switch (b) {
255            case '\r':
256                // Might be Mac, but we don't auto-detect Mac EOL
257                // to avoid confusion.
258                matchCount++;
259                break;
260
261            case '\n':
262                // UNIX
263                matchCount++;
264                matched = true;
265                break;
266
267            default:
268                matchCount = 0;
269            }
270
271            if (matched) {
272                // Found a match.
273                int pos = in.position();
274                in.limit(pos);
275                in.position(oldPos);
276
277                ctx.append(in);
278
279                in.limit(oldLimit);
280                in.position(pos);
281
282                if (ctx.getOverflowPosition() == 0) {
283                    IoBuffer buf = ctx.getBuffer();
284                    buf.flip();
285                    buf.limit(buf.limit() - matchCount);
286
287                    try {
288                        byte[] data = new byte[buf.limit()];
289                        buf.get(data);
290                        CharsetDecoder decoder = ctx.getDecoder();
291
292                        CharBuffer buffer = decoder.decode(ByteBuffer.wrap(data));
293                        String str = buffer.toString();
294                        writeText(session, str, out);
295                    } finally {
296                        buf.clear();
297                    }
298                } else {
299                    int overflowPosition = ctx.getOverflowPosition();
300                    ctx.reset();
301                    throw new RecoverableProtocolDecoderException("Line is too long: " + overflowPosition);
302                }
303
304                oldPos = pos;
305                matchCount = 0;
306            }
307        }
308
309        // Put remainder to buf.
310        in.position(oldPos);
311        ctx.append(in);
312
313        ctx.setMatchCount(matchCount);
314    }
315
316    /**
317     * Decode a line using the delimiter defined by the caller
318     */
319    private void decodeNormal(Context ctx, IoSession session, IoBuffer in, ProtocolDecoderOutput out)
320            throws CharacterCodingException, ProtocolDecoderException {
321        int matchCount = ctx.getMatchCount();
322
323        // Try to find a match
324        int oldPos = in.position();
325        int oldLimit = in.limit();
326
327        while (in.hasRemaining()) {
328            byte b = in.get();
329
330            if (delimBuf.get(matchCount) == b) {
331                matchCount++;
332
333                if (matchCount == delimBuf.limit()) {
334                    // Found a match.
335                    int pos = in.position();
336                    in.limit(pos);
337                    in.position(oldPos);
338
339                    ctx.append(in);
340
341                    in.limit(oldLimit);
342                    in.position(pos);
343
344                    if (ctx.getOverflowPosition() == 0) {
345                        IoBuffer buf = ctx.getBuffer();
346                        buf.flip();
347                        buf.limit(buf.limit() - matchCount);
348
349                        try {
350                            writeText(session, buf.getString(ctx.getDecoder()), out);
351                        } finally {
352                            buf.clear();
353                        }
354                    } else {
355                        int overflowPosition = ctx.getOverflowPosition();
356                        ctx.reset();
357                        throw new RecoverableProtocolDecoderException("Line is too long: " + overflowPosition);
358                    }
359
360                    oldPos = pos;
361                    matchCount = 0;
362                }
363            } else {
364                // fix for DIRMINA-506 & DIRMINA-536
365                in.position(Math.max(0, in.position() - matchCount));
366                matchCount = 0;
367            }
368        }
369
370        // Put remainder to buf.
371        in.position(oldPos);
372        ctx.append(in);
373
374        ctx.setMatchCount(matchCount);
375    }
376
377    /**
378     * By default, this method propagates the decoded line of text to
379     * {@code ProtocolDecoderOutput#write(Object)}.  You may override this method to modify
380     * the default behavior.
381     *
382     * @param session  the {@code IoSession} the received data.
383     * @param text  the decoded text
384     * @param out  the upstream {@code ProtocolDecoderOutput}.
385     */
386    protected void writeText(IoSession session, String text, ProtocolDecoderOutput out) {
387        out.write(text);
388    }
389
390    /**
391     * A Context used during the decoding of a lin. It stores the decoder,
392     * the temporary buffer containing the decoded line, and other status flags.
393     *
394     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
395     * @version $Rev$, $Date$
396     */
397    private class Context {
398        /** The decoder */
399        private final CharsetDecoder decoder;
400
401        /** The temporary buffer containing the decoded line */
402        private final IoBuffer buf;
403
404        /** The number of lines found so far */
405        private int matchCount = 0;
406
407        /** A counter to signal that the line is too long */
408        private int overflowPosition = 0;
409
410        /** Create a new Context object with a default buffer */
411        private Context(int bufferLength) {
412            decoder = charset.newDecoder();
413            buf = IoBuffer.allocate(bufferLength).setAutoExpand(true);
414        }
415
416        public CharsetDecoder getDecoder() {
417            return decoder;
418        }
419
420        public IoBuffer getBuffer() {
421            return buf;
422        }
423
424        public int getOverflowPosition() {
425            return overflowPosition;
426        }
427
428        public int getMatchCount() {
429            return matchCount;
430        }
431
432        public void setMatchCount(int matchCount) {
433            this.matchCount = matchCount;
434        }
435
436        public void reset() {
437            overflowPosition = 0;
438            matchCount = 0;
439            decoder.reset();
440        }
441
442        public void append(IoBuffer in) {
443            if (overflowPosition != 0) {
444                discard(in);
445            } else if (buf.position() > maxLineLength - in.remaining()) {
446                overflowPosition = buf.position();
447                buf.clear();
448                discard(in);
449            } else {
450                getBuffer().put(in);
451            }
452        }
453
454        private void discard(IoBuffer in) {
455            if (Integer.MAX_VALUE - in.remaining() < overflowPosition) {
456                overflowPosition = Integer.MAX_VALUE;
457            } else {
458                overflowPosition += in.remaining();
459            }
460
461            in.position(in.limit());
462        }
463    }
464}