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 }