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