001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 * 019 */ 020package org.apache.mina.filter.codec.textline; 021 022import java.nio.ByteBuffer; 023import java.nio.CharBuffer; 024import java.nio.charset.CharacterCodingException; 025import java.nio.charset.Charset; 026import java.nio.charset.CharsetDecoder; 027 028import org.apache.mina.core.buffer.BufferDataException; 029import org.apache.mina.core.buffer.IoBuffer; 030import org.apache.mina.core.session.AttributeKey; 031import org.apache.mina.core.session.IoSession; 032import org.apache.mina.filter.codec.ProtocolDecoder; 033import org.apache.mina.filter.codec.ProtocolDecoderException; 034import org.apache.mina.filter.codec.ProtocolDecoderOutput; 035import org.apache.mina.filter.codec.RecoverableProtocolDecoderException; 036 037/** 038 * A {@link ProtocolDecoder} which decodes a text line into a string. 039 * 040 * @author <a href="http://mina.apache.org">Apache MINA Project</a> 041 */ 042public class TextLineDecoder implements ProtocolDecoder { 043 private final AttributeKey CONTEXT = new AttributeKey(getClass(), "context"); 044 045 private final Charset charset; 046 047 /** The delimiter used to determinate when a line has been fully decoded */ 048 private final LineDelimiter delimiter; 049 050 /** An IoBuffer containing the delimiter */ 051 private IoBuffer delimBuf; 052 053 /** The default maximum Line length. Default to 1024. */ 054 private int maxLineLength = 1024; 055 056 /** The default maximum buffer length. Default to 128 chars. */ 057 private int bufferLength = 128; 058 059 /** 060 * Creates a new instance with the current default {@link Charset} 061 * and {@link LineDelimiter#AUTO} delimiter. 062 */ 063 public TextLineDecoder() { 064 this(LineDelimiter.AUTO); 065 } 066 067 /** 068 * Creates a new instance with the current default {@link Charset} 069 * and the specified <tt>delimiter</tt>. 070 * 071 * @param delimiter The line delimiter to use 072 */ 073 public TextLineDecoder(String delimiter) { 074 this(new LineDelimiter(delimiter)); 075 } 076 077 /** 078 * Creates a new instance with the current default {@link Charset} 079 * and the specified <tt>delimiter</tt>. 080 * 081 * @param delimiter The line delimiter to use 082 */ 083 public TextLineDecoder(LineDelimiter delimiter) { 084 this(Charset.defaultCharset(), delimiter); 085 } 086 087 /** 088 * Creates a new instance with the spcified <tt>charset</tt> 089 * and {@link LineDelimiter#AUTO} delimiter. 090 * 091 * @param charset The {@link Charset} to use 092 */ 093 public TextLineDecoder(Charset charset) { 094 this(charset, LineDelimiter.AUTO); 095 } 096 097 /** 098 * Creates a new instance with the spcified <tt>charset</tt> 099 * 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}