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