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.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.codec.ProtocolDecoder;
    29  import org.apache.mina.codec.ProtocolDecoderException;
    30
    31  /**
    32   * A {@link ProtocolDecoder} which decodes a text line into a string.
    33   *
    34   * @author <a href="http://mina.apache.org">Apache MINA Project</a>
    35   */
    36  public class TextLineDecoder implements ProtocolDecoder<ByteBuffer, String, TextLineDecoder.Context> {
    37      private final Charset charset;
    38
    39      /** The delimiter used to determinate when a line has been fully decoded */
    40      private final LineDelimiter delimiter;
    41
    42      /** An ByteBuffer containing the delimiter */
    43      private ByteBuffer delimBuf;
    44
    45      /** The default maximum Line length. Default to 1024. */
    46      private int maxLineLength = 1024;
    47
    48      /** The default maximum buffer length. Default to 128 chars. */
    49      private int bufferLength = 128;
    50
    51      /**
    52       * Creates a new instance with the current default {@link Charset} and {@link LineDelimiter#AUTO} delimiter.
    53       */
    54      public TextLineDecoder() {
    55          this(LineDelimiter.AUTO);
    56      }
    57
    58      /**
    59       * Creates a new instance with the current default {@link Charset} and the specified <tt>delimiter</tt>.
    60       */
    61      public TextLineDecoder(String delimiter) {
    62          this(new LineDelimiter(delimiter));
    63      }
    64
    65      /**
    66       * Creates a new instance with the current default {@link Charset} and the specified <tt>delimiter</tt>.
    67       */
    68      public TextLineDecoder(LineDelimiter delimiter) {
    69          this(Charset.defaultCharset(), delimiter);
    70      }
    71
    72      /**
    73       * Creates a new instance with the spcified <tt>charset</tt> and {@link LineDelimiter#AUTO} delimiter.
    74       */
    75      public TextLineDecoder(Charset charset) {
    76          this(charset, LineDelimiter.AUTO);
    77      }
    78
    79      /**
    80       * Creates a new instance with the spcified <tt>charset</tt> and the specified <tt>delimiter</tt>.
    81       */
    82      public TextLineDecoder(Charset charset, String delimiter) {
    83          this(charset, new LineDelimiter(delimiter));
    84      }
    85
    86      /**
    87       * Creates a new instance with the specified <tt>charset</tt> and the specified <tt>delimiter</tt>.
    88       */
    89      public TextLineDecoder(Charset charset, LineDelimiter delimiter) {
    90          if (charset == null) {
    91              throw new IllegalArgumentException("charset parameter shuld not be null");
    92          }
    93
    94          if (delimiter == null) {
    95              throw new IllegalArgumentException("delimiter parameter should not be null");
    96          }
    97
    98          this.charset = charset;
    99          this.delimiter = delimiter;
    100
    101         // Convert delimiter to ByteBuffer if not done yet.
    102         if (delimBuf == null) {
    103             ByteBuffer tmp = charset.encode(CharBuffer.wrap(delimiter.getValue()));
    104             tmp.rewind();
    105             delimBuf = tmp;
    106         }
    107     }
    108
    109     /**
    110      * Returns the allowed maximum size of the line to be decoded. If the size of the line to be decoded exceeds this
    111      * value, the decoder will throw a {@link BufferDataException}. The default value is <tt>1024</tt> (1KB).
    112      */
    113     public int getMaxLineLength() {
    114         return maxLineLength;
    115     }
    116
    117     /**
    118      * Sets the allowed maximum size of the line to be decoded. If the size of the line to be decoded exceeds this
    119      * value, the decoder will throw a {@link BufferDataException}. The default value is <tt>1024</tt> (1KB).
    120      */
    121     public void setMaxLineLength(int maxLineLength) {
    122         if (maxLineLength <= 0) {
    123             throw new IllegalArgumentException("maxLineLength (" + maxLineLength + ") should be a positive value");
    124         }
    125
    126         this.maxLineLength = maxLineLength;
    127     }
    128
    129     /**
    130      * Sets the default buffer size. This buffer is used in the Context to store the decoded line.
    131      *
    132      * @param bufferLength The default bufer size
    133      */
    134     public void setBufferLength(int bufferLength) {
    135         if (bufferLength <= 0) {
    136             throw new IllegalArgumentException("bufferLength (" + maxLineLength + ") should be a positive value");
    137
    138         }
    139
    140         this.bufferLength = bufferLength;
    141     }
    142
    143     /**
    144      * Returns the allowed buffer size used to store the decoded line in the Context instance.
    145      */
    146     public int getBufferLength() {
    147         return bufferLength;
    148     }
    149
    150     @Override
    151     public Context createDecoderState() {
    152         return new Context(bufferLength);
    153     }
    154
    155     /**
    156      * {@inheritDoc}
    157      */
    158     @Override
    159     public String decode(ByteBuffer in, Context ctx) {
    160         if (LineDelimiter.AUTO.equals(delimiter)) {
    161             return decodeAuto(ctx, in);
    162         } else {
    163             return decodeNormal(ctx, in);
    164         }
    165     }
    166
    167     /**
    168      * {@inheritDoc}
    169      */
    170     @Override
    171     public void finishDecode(Context ctx) {
    172     }
    173
    174     /**
    175      * Decode a line using the default delimiter on the current system
    176      */
    177     private String decodeAuto(Context ctx, ByteBuffer in) {
    178         String decoded = null;
    179         int matchCount = ctx.getMatchCount();
    180
    181         // Try to find a match
    182         int oldPos = in.position();
    183         int oldLimit = in.limit();
    184
    185         while (in.hasRemaining() && decoded == null) {
    186             byte b = in.get();
    187             boolean matched = false;
    188
    189             switch (b) {
    190             case '\r':
    191                 // Might be Mac, but we don't auto-detect Mac EOL
    192                 // to avoid confusion.
    193                 matchCount++;
    194                 break;
    195
    196             case '\n':
    197                 // UNIX
    198                 matchCount++;
    199                 matched = true;
    200                 break;
    201
    202             default:
    203                 matchCount = 0;
    204             }
    205
    206             if (matched) {
    207                 // Found a match.
    208                 int pos = in.position();
    209                 in.limit(pos);
    210                 in.position(oldPos);
    211
    212                 ctx.append(in);
    213
    214                 in.limit(oldLimit);
    215                 in.position(pos);
    216
    217                 try {
    218                     if (ctx.getOverflowLength() == 0) {
    219                         ByteBuffer buf = ctx.getBuffer();
    220                         buf.flip();
    221                         buf.limit(buf.limit() - matchCount);
    222
    223                         CharsetDecoder decoder = ctx.getDecoder();
    224                         CharBuffer buffer = decoder.decode(buf);
    225                         decoded = new String(buffer.array());
    226                     } else {
    227                         int overflowPosition = ctx.getOverflowLength();
    228                         throw new IllegalStateException("Line is too long: " + overflowPosition);
    229                     }
    230                 } catch (CharacterCodingException cce) {
    231                     throw new ProtocolDecoderException(cce);
    232                 } finally {
    233                     ctx.reset();
    234                 }
    235                 oldPos = pos;
    236                 matchCount = 0;
    237             }
    238         }
    239
    240         // Put remainder to buf.
    241         in.position(oldPos);
    242         ctx.append(in);
    243
    244         ctx.setMatchCount(matchCount);
    245         return decoded;
    246     }
    247
    248     /**
    249      * Decode a line using the delimiter defined by the caller
    250      *
    251      * @return
    252      */
    253     private String decodeNormal(Context ctx, ByteBuffer in) {
    254         String decoded = null;
    255         int matchCount = ctx.getMatchCount();
    256
    257         // Try to find a match
    258         int oldPos = in.position();
    259         int oldLimit = in.limit();
    260
    261         while (in.hasRemaining() && decoded == null) {
    262             byte b = in.get();
    263
    264             if (delimBuf.get(matchCount) == b) {
    265                 matchCount++;
    266
    267                 if (matchCount == delimBuf.limit()) {
    268                     // Found a match.
    269                     int pos = in.position();
    270                     in.limit(pos);
    271                     in.position(oldPos);
    272
    273                     ctx.append(in);
    274
    275                     in.limit(oldLimit);
    276                     in.position(pos);
    277
    278                     try {
    279                         if (ctx.getOverflowLength() == 0) {
    280                             ByteBuffer buf = ctx.getBuffer();
    281                             buf.flip();
    282                             buf.limit(buf.limit() - matchCount);
    283
    284                             CharsetDecoder decoder = ctx.getDecoder();
    285                             CharBuffer buffer = decoder.decode(buf);
    286                             decoded = new String(buffer.array());
    287                         } else {
    288                             int overflowLength = ctx.getOverflowLength();
    289                             throw new IllegalStateException("Line is too long: " + overflowLength);
    290                         }
    291                     } catch (CharacterCodingException cce) {
    292                         throw new ProtocolDecoderException(cce);
    293                     } finally {
    294                         ctx.reset();
    295                     }
    296
    297                     oldPos = pos;
    298                     matchCount = 0;
    299                 }
    300             } else {
    301                 // fix for DIRMINA-506 & DIRMINA-536
    302                 in.position(Math.max(0, in.position() - matchCount));
    303                 matchCount = 0;
    304             }
    305         }
    306
    307         // Put remainder to buf.
    308         in.position(oldPos);
    309         ctx.append(in);
    310
    311         ctx.setMatchCount(matchCount);
    312         return decoded;
    313     }
    314
    315     /**
    316      * A Context used during the decoding of a lin. It stores the decoder, the temporary buffer containing the decoded
    317      * line, and other status flags.
    318      *
    319      * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
    320      * @version $Rev$, $Date$
    321      */
    322     public class Context {
    323         /** The decoder */
    324         private final CharsetDecoder decoder;
    325
    326         /** The temporary buffer containing the decoded line */
    327         private ByteBuffer buf;
    328
    329         /** The number of lines found so far */
    330         private int matchCount = 0;
    331
    332         /**
    333          * Overflow length
    334          */
    335         private int overflowLength = 0;
    336
    337         /** Create a new Context object with a default buffer */
    338         private Context(int bufferLength) {
    339             decoder = charset.newDecoder();
    340             buf = ByteBuffer.allocate(bufferLength);
    341         }
    342
    343         public CharsetDecoder getDecoder() {
    344             return decoder;
    345         }
    346
    347         public ByteBuffer getBuffer() {
    348             return buf;
    349         }
    350
    351         public int getMatchCount() {
    352             return matchCount;
    353         }
    354
    355         public void setMatchCount(int matchCount) {
    356             this.matchCount = matchCount;
    357         }
    358
    359         public int getOverflowLength() {
    360             return overflowLength;
    361         }
    362
    363         public void reset() {
    364             overflowLength = 0;
    365             matchCount = 0;
    366             decoder.reset();
    367             buf.clear();
    368         }
    369
    370         private void ensureSpace(int size) {
    371             if (buf.position() + size > buf.capacity()) {
    372                 ByteBuffer b = ByteBuffer.allocate(buf.position() + size + bufferLength);
    373                 buf.flip();
    374                 b.put(buf);
    375                 buf = b;
    376             }
    377         }
    378
    379         public void append(ByteBuffer in) {
    380             if (buf.position() > maxLineLength - in.remaining()) {
    381                 overflowLength = buf.position() + in.remaining();
    382                 buf.clear();
    383                 discard(in);
    384             } else {
    385                 ensureSpace(in.remaining());
    386                 getBuffer().put(in);
    387             }
    388         }
    389
    390         private void discard(ByteBuffer in) {
    391             in.position(in.limit());
    392         }
    393     }
    394
    395 }