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