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