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