View Javadoc

    1   /*
    2    *  Licensed to the Apache Software Foundation (ASF) under one
    3    *  or more contributor license agreements.  See the NOTICE file
    4    *  distributed with this work for additional information
    5    *  regarding copyright ownership.  The ASF licenses this file
    6    *  to you under the Apache License, Version 2.0 (the
    7    *  "License"); you may not use this file except in compliance
    8    *  with the License.  You may obtain a copy of the License at
    9    *
    10   *    http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   *  Unless required by applicable law or agreed to in writing,
    13   *  software distributed under the License is distributed on an
    14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    15   *  KIND, either express or implied.  See the License for the
    16   *  specific language governing permissions and limitations
    17   *  under the License.
    18   *
    19   */
    20  package org.apache.mina.filter.codec.textline;
    21
    22  import java.nio.ByteBuffer;
    23  import java.nio.CharBuffer;
    24  import java.nio.charset.CharacterCodingException;
    25  import java.nio.charset.Charset;
    26  import java.nio.charset.CharsetDecoder;
    27
    28  import org.apache.mina.core.buffer.BufferDataException;
    29  import org.apache.mina.core.buffer.IoBuffer;
    30  import org.apache.mina.core.session.AttributeKey;
    31  import org.apache.mina.core.session.IoSession;
    32  import org.apache.mina.filter.codec.ProtocolDecoder;
    33  import org.apache.mina.filter.codec.ProtocolDecoderException;
    34  import org.apache.mina.filter.codec.ProtocolDecoderOutput;
    35  import org.apache.mina.filter.codec.RecoverableProtocolDecoderException;
    36
    37  /**
    38   * A {@link ProtocolDecoder} which decodes a text line into a string.
    39   *
    40   * @author <a href="http://mina.apache.org">Apache MINA Project</a>
    41   */
    42  public class TextLineDecoder implements ProtocolDecoder {
    43      private final AttributeKey CONTEXT = new AttributeKey(getClass(), "context");
    44
    45      private final Charset charset;
    46
    47      /** The delimiter used to determinate when a line has been fully decoded */
    48      private final LineDelimiter delimiter;
    49
    50      /** An IoBuffer containing the delimiter */
    51      private IoBuffer delimBuf;
    52
    53      /** The default maximum Line length. Default to 1024. */
    54      private int maxLineLength = 1024;
    55
    56      /** The default maximum buffer length. Default to 128 chars. */
    57      private int bufferLength = 128;
    58
    59      /**
    60       * Creates a new instance with the current default {@link Charset}
    61       * and {@link LineDelimiter#AUTO} delimiter.
    62       */
    63      public TextLineDecoder() {
    64          this(LineDelimiter.AUTO);
    65      }
    66
    67      /**
    68       * Creates a new instance with the current default {@link Charset}
    69       * and the specified <tt>delimiter</tt>.
    70       */
    71      public TextLineDecoder(String delimiter) {
    72          this(new LineDelimiter(delimiter));
    73      }
    74
    75      /**
    76       * Creates a new instance with the current default {@link Charset}
    77       * and the specified <tt>delimiter</tt>.
    78       */
    79      public TextLineDecoder(LineDelimiter delimiter) {
    80          this(Charset.defaultCharset(), delimiter);
    81      }
    82
    83      /**
    84       * Creates a new instance with the spcified <tt>charset</tt>
    85       * and {@link LineDelimiter#AUTO} delimiter.
    86       */
    87      public TextLineDecoder(Charset charset) {
    88          this(charset, LineDelimiter.AUTO);
    89      }
    90
    91      /**
    92       * Creates a new instance with the spcified <tt>charset</tt>
    93       * and the specified <tt>delimiter</tt>.
    94       */
    95      public TextLineDecoder(Charset charset, String delimiter) {
    96          this(charset, new LineDelimiter(delimiter));
    97      }
    98
    99      /**
    100      * Creates a new instance with the specified <tt>charset</tt>
    101      * and the specified <tt>delimiter</tt>.
    102      */
    103     public TextLineDecoder(Charset charset, LineDelimiter delimiter) {
    104         if (charset == null) {
    105             throw new IllegalArgumentException("charset parameter shuld not be null");
    106         }
    107
    108         if (delimiter == null) {
    109             throw new IllegalArgumentException("delimiter parameter should not be null");
    110         }
    111
    112         this.charset = charset;
    113         this.delimiter = delimiter;
    114
    115         // Convert delimiter to ByteBuffer if not done yet.
    116         if (delimBuf == null) {
    117             IoBuffer tmp = IoBuffer.allocate(2).setAutoExpand(true);
    118
    119             try {
    120                 tmp.putString(delimiter.getValue(), charset.newEncoder());
    121             } catch (CharacterCodingException cce) {
    122
    123             }
    124
    125             tmp.flip();
    126             delimBuf = tmp;
    127         }
    128     }
    129
    130     /**
    131      * Returns the allowed maximum size of the line to be decoded.
    132      * If the size of the line to be decoded exceeds this value, the
    133      * decoder will throw a {@link BufferDataException}.  The default
    134      * value is <tt>1024</tt> (1KB).
    135      */
    136     public int getMaxLineLength() {
    137         return maxLineLength;
    138     }
    139
    140     /**
    141      * Sets the allowed maximum size of the line to be decoded.
    142      * If the size of the line to be decoded exceeds this value, the
    143      * decoder will throw a {@link BufferDataException}.  The default
    144      * value is <tt>1024</tt> (1KB).
    145      */
    146     public void setMaxLineLength(int maxLineLength) {
    147         if (maxLineLength <= 0) {
    148             throw new IllegalArgumentException("maxLineLength (" + maxLineLength + ") should be a positive value");
    149         }
    150
    151         this.maxLineLength = maxLineLength;
    152     }
    153
    154     /**
    155      * Sets the default buffer size. This buffer is used in the Context
    156      * to store the decoded line.
    157      *
    158      * @param bufferLength The default bufer size
    159      */
    160     public void setBufferLength(int bufferLength) {
    161         if (bufferLength <= 0) {
    162             throw new IllegalArgumentException("bufferLength (" + maxLineLength + ") should be a positive value");
    163
    164         }
    165
    166         this.bufferLength = bufferLength;
    167     }
    168
    169     /**
    170      * Returns the allowed buffer size used to store the decoded line
    171      * in the Context instance.
    172      */
    173     public int getBufferLength() {
    174         return bufferLength;
    175     }
    176
    177     /**
    178      * {@inheritDoc}
    179      */
    180     public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
    181         Context ctx = getContext(session);
    182
    183         if (LineDelimiter.AUTO.equals(delimiter)) {
    184             decodeAuto(ctx, session, in, out);
    185         } else {
    186             decodeNormal(ctx, session, in, out);
    187         }
    188     }
    189
    190     /**
    191      * Return the context for this session
    192      */
    193     private Context getContext(IoSession session) {
    194         Context ctx;
    195         ctx = (Context) session.getAttribute(CONTEXT);
    196
    197         if (ctx == null) {
    198             ctx = new Context(bufferLength);
    199             session.setAttribute(CONTEXT, ctx);
    200         }
    201
    202         return ctx;
    203     }
    204
    205     /**
    206      * {@inheritDoc}
    207      */
    208     public void finishDecode(IoSession session, ProtocolDecoderOutput out) throws Exception {
    209         // Do nothing
    210     }
    211
    212     /**
    213      * {@inheritDoc}
    214      */
    215     public void dispose(IoSession session) throws Exception {
    216         Context ctx = (Context) session.getAttribute(CONTEXT);
    217
    218         if (ctx != null) {
    219             session.removeAttribute(CONTEXT);
    220         }
    221     }
    222
    223     /**
    224      * Decode a line using the default delimiter on the current system
    225      */
    226     private void decodeAuto(Context ctx, IoSession session, IoBuffer in, ProtocolDecoderOutput out)
    227             throws CharacterCodingException, ProtocolDecoderException {
    228         int matchCount = ctx.getMatchCount();
    229
    230         // Try to find a match
    231         int oldPos = in.position();
    232         int oldLimit = in.limit();
    233
    234         while (in.hasRemaining()) {
    235             byte b = in.get();
    236             boolean matched = false;
    237
    238             switch (b) {
    239             case '\r':
    240                 // Might be Mac, but we don't auto-detect Mac EOL
    241                 // to avoid confusion.
    242                 matchCount++;
    243                 break;
    244
    245             case '\n':
    246                 // UNIX
    247                 matchCount++;
    248                 matched = true;
    249                 break;
    250
    251             default:
    252                 matchCount = 0;
    253             }
    254
    255             if (matched) {
    256                 // Found a match.
    257                 int pos = in.position();
    258                 in.limit(pos);
    259                 in.position(oldPos);
    260
    261                 ctx.append(in);
    262
    263                 in.limit(oldLimit);
    264                 in.position(pos);
    265
    266                 if (ctx.getOverflowPosition() == 0) {
    267                     IoBuffer buf = ctx.getBuffer();
    268                     buf.flip();
    269                     buf.limit(buf.limit() - matchCount);
    270
    271                     try {
    272                         byte[] data = new byte[buf.limit()];
    273                         buf.get(data);
    274                         CharsetDecoder decoder = ctx.getDecoder();
    275
    276                         CharBuffer buffer = decoder.decode(ByteBuffer.wrap(data));
    277                         String str = new String(buffer.array());
    278                         writeText(session, str, out);
    279                     } finally {
    280                         buf.clear();
    281                     }
    282                 } else {
    283                     int overflowPosition = ctx.getOverflowPosition();
    284                     ctx.reset();
    285                     throw new RecoverableProtocolDecoderException("Line is too long: " + overflowPosition);
    286                 }
    287
    288                 oldPos = pos;
    289                 matchCount = 0;
    290             }
    291         }
    292
    293         // Put remainder to buf.
    294         in.position(oldPos);
    295         ctx.append(in);
    296
    297         ctx.setMatchCount(matchCount);
    298     }
    299
    300     /**
    301      * Decode a line using the delimiter defined by the caller
    302      */
    303     private void decodeNormal(Context ctx, IoSession session, IoBuffer in, ProtocolDecoderOutput out)
    304             throws CharacterCodingException, ProtocolDecoderException {
    305         int matchCount = ctx.getMatchCount();
    306
    307         // Try to find a match
    308         int oldPos = in.position();
    309         int oldLimit = in.limit();
    310
    311         while (in.hasRemaining()) {
    312             byte b = in.get();
    313
    314             if (delimBuf.get(matchCount) == b) {
    315                 matchCount++;
    316
    317                 if (matchCount == delimBuf.limit()) {
    318                     // Found a match.
    319                     int pos = in.position();
    320                     in.limit(pos);
    321                     in.position(oldPos);
    322
    323                     ctx.append(in);
    324
    325                     in.limit(oldLimit);
    326                     in.position(pos);
    327
    328                     if (ctx.getOverflowPosition() == 0) {
    329                         IoBuffer buf = ctx.getBuffer();
    330                         buf.flip();
    331                         buf.limit(buf.limit() - matchCount);
    332
    333                         try {
    334                             writeText(session, buf.getString(ctx.getDecoder()), out);
    335                         } finally {
    336                             buf.clear();
    337                         }
    338                     } else {
    339                         int overflowPosition = ctx.getOverflowPosition();
    340                         ctx.reset();
    341                         throw new RecoverableProtocolDecoderException("Line is too long: " + overflowPosition);
    342                     }
    343
    344                     oldPos = pos;
    345                     matchCount = 0;
    346                 }
    347             } else {
    348                 // fix for DIRMINA-506 & DIRMINA-536
    349                 in.position(Math.max(0, in.position() - matchCount));
    350                 matchCount = 0;
    351             }
    352         }
    353
    354         // Put remainder to buf.
    355         in.position(oldPos);
    356         ctx.append(in);
    357
    358         ctx.setMatchCount(matchCount);
    359     }
    360
    361     /**
    362      * By default, this method propagates the decoded line of text to
    363      * {@code ProtocolDecoderOutput#write(Object)}.  You may override this method to modify
    364      * the default behavior.
    365      *
    366      * @param session  the {@code IoSession} the received data.
    367      * @param text  the decoded text
    368      * @param out  the upstream {@code ProtocolDecoderOutput}.
    369      */
    370     protected void writeText(IoSession session, String text, ProtocolDecoderOutput out) {
    371         out.write(text);
    372     }
    373
    374     /**
    375      * A Context used during the decoding of a lin. It stores the decoder,
    376      * the temporary buffer containing the decoded line, and other status flags.
    377      *
    378      * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
    379      * @version $Rev: 1387401 $, $Date: 2012-09-19 00:51:04 +0200 (Wed, 19 Sep 2012) $
    380      */
    381     private class Context {
    382         /** The decoder */
    383         private final CharsetDecoder decoder;
    384
    385         /** The temporary buffer containing the decoded line */
    386         private final IoBuffer buf;
    387
    388         /** The number of lines found so far */
    389         private int matchCount = 0;
    390
    391         /** A counter to signal that the line is too long */
    392         private int overflowPosition = 0;
    393
    394         /** Create a new Context object with a default buffer */
    395         private Context(int bufferLength) {
    396             decoder = charset.newDecoder();
    397             buf = IoBuffer.allocate(bufferLength).setAutoExpand(true);
    398         }
    399
    400         public CharsetDecoder getDecoder() {
    401             return decoder;
    402         }
    403
    404         public IoBuffer getBuffer() {
    405             return buf;
    406         }
    407
    408         public int getOverflowPosition() {
    409             return overflowPosition;
    410         }
    411
    412         public int getMatchCount() {
    413             return matchCount;
    414         }
    415
    416         public void setMatchCount(int matchCount) {
    417             this.matchCount = matchCount;
    418         }
    419
    420         public void reset() {
    421             overflowPosition = 0;
    422             matchCount = 0;
    423             decoder.reset();
    424         }
    425
    426         public void append(IoBuffer in) {
    427             if (overflowPosition != 0) {
    428                 discard(in);
    429             } else if (buf.position() > maxLineLength - in.remaining()) {
    430                 overflowPosition = buf.position();
    431                 buf.clear();
    432                 discard(in);
    433             } else {
    434                 getBuffer().put(in);
    435             }
    436         }
    437
    438         private void discard(IoBuffer in) {
    439             if (Integer.MAX_VALUE - in.remaining() < overflowPosition) {
    440                 overflowPosition = Integer.MAX_VALUE;
    441             } else {
    442                 overflowPosition += in.remaining();
    443             }
    444
    445             in.position(in.limit());
    446         }
    447     }
    448 }