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 static final AttributeKeyAttributeKey.html#AttributeKey">AttributeKey CONTEXT = new AttributeKey(TextLineDecoder.class, "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       * @param delimiter The line delimiter to use
72       */
73      public TextLineDecoder(String delimiter) {
74          this(new LineDelimiter(delimiter));
75      }
76  
77      /**
78       * Creates a new instance with the current default {@link Charset}
79       * and the specified <tt>delimiter</tt>.
80       * 
81       * @param delimiter The line delimiter to use
82       */
83      public TextLineDecoder(LineDelimiter delimiter) {
84          this(Charset.defaultCharset(), delimiter);
85      }
86  
87      /**
88       * Creates a new instance with the spcified <tt>charset</tt>
89       * and {@link LineDelimiter#AUTO} delimiter.
90       * 
91       * @param charset The {@link Charset} to use
92       */
93      public TextLineDecoder(Charset charset) {
94          this(charset, LineDelimiter.AUTO);
95      }
96  
97      /**
98       * Creates a new instance with the spcified <tt>charset</tt>
99       * 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     @Override
195     public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
196         Context ctx = getContext(session);
197 
198         if (LineDelimiter.AUTO.equals(delimiter)) {
199             decodeAuto(ctx, session, in, out);
200         } else {
201             decodeNormal(ctx, session, in, out);
202         }
203     }
204 
205     /**
206      * @return the context for this session
207      * 
208      * @param session The session for which we want the context
209      */
210     private Context getContext(IoSession session) {
211         Context ctx;
212         ctx = (Context) session.getAttribute(CONTEXT);
213 
214         if (ctx == null) {
215             ctx = new Context(bufferLength);
216             session.setAttribute(CONTEXT, ctx);
217         }
218 
219         return ctx;
220     }
221 
222     /**
223      * {@inheritDoc}
224      */
225     @Override
226     public void finishDecode(IoSession session, ProtocolDecoderOutput out) throws Exception {
227         // Do nothing
228     }
229 
230     /**
231      * {@inheritDoc}
232      */
233     @Override
234     public void dispose(IoSession session) throws Exception {
235         Context ctx = (Context) session.getAttribute(CONTEXT);
236 
237         if (ctx != null) {
238             session.removeAttribute(CONTEXT);
239         }
240     }
241 
242     /**
243      * Decode a line using the default delimiter on the current system
244      */
245     private void decodeAuto(Context ctx, IoSession session, IoBuffer in, ProtocolDecoderOutput out)
246             throws CharacterCodingException, ProtocolDecoderException {
247         int matchCount = ctx.getMatchCount();
248 
249         // Try to find a match
250         int oldPos = in.position();
251         int oldLimit = in.limit();
252 
253         while (in.hasRemaining()) {
254             byte b = in.get();
255             boolean matched = false;
256 
257             switch (b) {
258             case '\r':
259                 // Might be Mac, but we don't auto-detect Mac EOL
260                 // to avoid confusion.
261                 matchCount++;
262                 break;
263 
264             case '\n':
265                 // UNIX
266                 matchCount++;
267                 matched = true;
268                 break;
269 
270             default:
271                 matchCount = 0;
272             }
273 
274             if (matched) {
275                 // Found a match.
276                 int pos = in.position();
277                 in.limit(pos);
278                 in.position(oldPos);
279 
280                 ctx.append(in);
281 
282                 in.limit(oldLimit);
283                 in.position(pos);
284 
285                 if (ctx.getOverflowPosition() == 0) {
286                     IoBuffer buf = ctx.getBuffer();
287                     buf.flip();
288                     buf.limit(buf.limit() - matchCount);
289 
290                     try {
291                         byte[] data = new byte[buf.limit()];
292                         buf.get(data);
293                         CharsetDecoder decoder = ctx.getDecoder();
294 
295                         CharBuffer buffer = decoder.decode(ByteBuffer.wrap(data));
296                         String str = buffer.toString();
297                         writeText(session, str, out);
298                     } finally {
299                         buf.clear();
300                     }
301                 } else {
302                     int overflowPosition = ctx.getOverflowPosition();
303                     ctx.reset();
304                     throw new RecoverableProtocolDecoderException("Line is too long: " + overflowPosition);
305                 }
306 
307                 oldPos = pos;
308                 matchCount = 0;
309             }
310         }
311 
312         // Put remainder to buf.
313         in.position(oldPos);
314         ctx.append(in);
315 
316         ctx.setMatchCount(matchCount);
317     }
318 
319     /**
320      * Decode a line using the delimiter defined by the caller
321      */
322     private void decodeNormal(Context ctx, IoSession session, IoBuffer in, ProtocolDecoderOutput out)
323             throws CharacterCodingException, ProtocolDecoderException {
324         int matchCount = ctx.getMatchCount();
325 
326         // Try to find a match
327         int oldPos = in.position();
328         int oldLimit = in.limit();
329 
330         while (in.hasRemaining()) {
331             byte b = in.get();
332 
333             if (delimBuf.get(matchCount) == b) {
334                 matchCount++;
335 
336                 if (matchCount == delimBuf.limit()) {
337                     // Found a match.
338                     int pos = in.position();
339                     in.limit(pos);
340                     in.position(oldPos);
341 
342                     ctx.append(in);
343 
344                     in.limit(oldLimit);
345                     in.position(pos);
346 
347                     if (ctx.getOverflowPosition() == 0) {
348                         IoBuffer buf = ctx.getBuffer();
349                         buf.flip();
350                         buf.limit(buf.limit() - matchCount);
351 
352                         try {
353                             writeText(session, buf.getString(ctx.getDecoder()), out);
354                         } finally {
355                             buf.clear();
356                         }
357                     } else {
358                         int overflowPosition = ctx.getOverflowPosition();
359                         ctx.reset();
360                         throw new RecoverableProtocolDecoderException("Line is too long: " + overflowPosition);
361                     }
362 
363                     oldPos = pos;
364                     matchCount = 0;
365                 }
366             } else {
367                 // fix for DIRMINA-506 & DIRMINA-536
368                 in.position(Math.max(0, in.position() - matchCount));
369                 matchCount = 0;
370             }
371         }
372 
373         // Put remainder to buf.
374         in.position(oldPos);
375         ctx.append(in);
376 
377         ctx.setMatchCount(matchCount);
378     }
379 
380     /**
381      * By default, this method propagates the decoded line of text to
382      * {@code ProtocolDecoderOutput#write(Object)}.  You may override this method to modify
383      * the default behavior.
384      *
385      * @param session  the {@code IoSession} the received data.
386      * @param text  the decoded text
387      * @param out  the upstream {@code ProtocolDecoderOutput}.
388      */
389     protected void writeText(IoSession session, String text, ProtocolDecoderOutput out) {
390         out.write(text);
391     }
392 
393     /**
394      * A Context used during the decoding of a lin. It stores the decoder,
395      * the temporary buffer containing the decoded line, and other status flags.
396      *
397      * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
398      * @version $Rev$, $Date$
399      */
400     private class Context {
401         /** The decoder */
402         private final CharsetDecoder decoder;
403 
404         /** The temporary buffer containing the decoded line */
405         private final IoBuffer buf;
406 
407         /** The number of lines found so far */
408         private int matchCount = 0;
409 
410         /** A counter to signal that the line is too long */
411         private int overflowPosition = 0;
412 
413         /** Create a new Context object with a default buffer */
414         private Context(int bufferLength) {
415             decoder = charset.newDecoder();
416             buf = IoBuffer.allocate(bufferLength).setAutoExpand(true);
417         }
418 
419         public CharsetDecoder getDecoder() {
420             return decoder;
421         }
422 
423         public IoBuffer getBuffer() {
424             return buf;
425         }
426 
427         public int getOverflowPosition() {
428             return overflowPosition;
429         }
430 
431         public int getMatchCount() {
432             return matchCount;
433         }
434 
435         public void setMatchCount(int matchCount) {
436             this.matchCount = matchCount;
437         }
438 
439         public void reset() {
440             overflowPosition = 0;
441             matchCount = 0;
442             decoder.reset();
443         }
444 
445         public void append(IoBuffer in) {
446             if (overflowPosition != 0) {
447                 discard(in);
448             } else if (buf.position() > maxLineLength - in.remaining()) {
449                 overflowPosition = buf.position();
450                 buf.clear();
451                 discard(in);
452             } else {
453                 getBuffer().put(in);
454             }
455         }
456 
457         private void discard(IoBuffer in) {
458             if (Integer.MAX_VALUE - in.remaining() < overflowPosition) {
459                 overflowPosition = Integer.MAX_VALUE;
460             } else {
461                 overflowPosition += in.remaining();
462             }
463 
464             in.position(in.limit());
465         }
466     }
467 }