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 }