/** * Copyright 2007-2015, Kaazing Corporation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.kaazing.k3po.driver.internal.control.handler; import static java.lang.String.format; import static org.jboss.netty.util.CharsetUtil.UTF_8; import static org.kaazing.k3po.lang.internal.parser.ScriptParseStrategy.PROPERTY_NODE; import java.util.ArrayList; import java.util.List; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.handler.codec.replay.ReplayingDecoder; import org.kaazing.k3po.driver.internal.control.AbortMessage; import org.kaazing.k3po.driver.internal.control.AwaitMessage; import org.kaazing.k3po.driver.internal.control.ControlMessage; import org.kaazing.k3po.driver.internal.control.DisposeMessage; import org.kaazing.k3po.driver.internal.control.ErrorMessage; import org.kaazing.k3po.driver.internal.control.FinishedMessage; import org.kaazing.k3po.driver.internal.control.NotifyMessage; import org.kaazing.k3po.driver.internal.control.PrepareMessage; import org.kaazing.k3po.driver.internal.control.PreparedMessage; import org.kaazing.k3po.driver.internal.control.StartMessage; import org.kaazing.k3po.lang.internal.parser.ScriptParseException; import org.kaazing.k3po.lang.internal.parser.ScriptParserImpl; public class ControlDecoder extends ReplayingDecoder<ControlDecoder.State> { enum State { READ_INITIAL, READ_HEADER, READ_CONTENT } private final int maxInitialLineLength; private final int maxHeaderLineLength; private final int maxContentLength; private ControlMessage message; private int contentLength; public ControlDecoder() { this(1024, 1024, 32768); } public ControlDecoder(int maxInitialLineLength, int maxHeaderLineLength, int maxContentLength) { super(false); this.maxInitialLineLength = maxInitialLineLength; this.maxHeaderLineLength = maxHeaderLineLength; this.maxContentLength = maxContentLength; setState(State.READ_INITIAL); } @Override protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, State state) throws Exception { switch (state) { case READ_INITIAL: { String initialLine = readLine(buffer, maxInitialLineLength); if (initialLine != null) { message = createMessage(initialLine); contentLength = 0; checkpoint(State.READ_HEADER); } return null; } case READ_HEADER: { State nextState = readHeader(buffer); checkpoint(nextState); if (nextState == State.READ_INITIAL) { return message; } return null; } case READ_CONTENT: { State nextState = readContent(buffer); checkpoint(nextState); if (nextState != State.READ_CONTENT) { return message; } return null; } default: throw new IllegalArgumentException(String.format("Unrecognized decoder state: %s", state)); } } private static String readLine(ChannelBuffer buffer, int maxLineLength) { int readableBytes = buffer.readableBytes(); if (readableBytes == 0) { return null; } int readerIndex = buffer.readerIndex(); int endOfLineAt = buffer.indexOf(readerIndex, Math.min(readableBytes, maxLineLength) + 1, (byte) 0x0a); // end-of-line not found if (endOfLineAt == -1) { if (readableBytes >= maxLineLength) { throw new IllegalArgumentException("Initial line too long"); } return null; } // end-of-line found StringBuilder sb = new StringBuilder(endOfLineAt); for (int i = readerIndex; i < endOfLineAt; i++) { sb.append((char) buffer.readByte()); } byte endOfLine = buffer.readByte(); assert endOfLine == 0x0a; return sb.toString(); } private ControlMessage createMessage(String initialLine) { ControlMessage.Kind messageKind = ControlMessage.Kind.valueOf(initialLine); switch (messageKind) { case PREPARE: return new PrepareMessage(); case START: return new StartMessage(); case ABORT: return new AbortMessage(); case NOTIFY: return new NotifyMessage(); case AWAIT: return new AwaitMessage(); case DISPOSE: return new DisposeMessage(); default: throw new IllegalArgumentException(format("Unrecognized message kind: %s", messageKind)); } } private State readHeader(ChannelBuffer buffer) { int readableBytes = buffer.readableBytes(); if (readableBytes == 0) { return null; } int endOfLineSearchFrom = buffer.readerIndex(); // int endOfLineSearchTo = endOfLineSearchFrom + Math.min(readableBytes, maxHeaderLineLength); int endOfLineSearchTo = Math.min(readableBytes, maxHeaderLineLength); int endOfLineAt = buffer.indexOf(endOfLineSearchFrom, endOfLineSearchTo + 1, (byte) 0x0a); // end-of-line not found if (endOfLineAt == -1) { if (readableBytes >= maxHeaderLineLength) { throw new IllegalArgumentException("Header line too long"); } return null; } if (endOfLineAt == endOfLineSearchFrom) { byte endOfLine = buffer.readByte(); assert endOfLine == 0x0a; if (contentLength == 0) { return State.READ_INITIAL; } switch (message.getKind()) { case PREPARE: case FINISHED: case ERROR: // content for these message kinds return State.READ_CONTENT; default: return State.READ_INITIAL; } } // end-of-line found int colonSearchFrom = buffer.readerIndex(); // int colonSearchTo = colonSearchFrom + Math.min(readableBytes, endOfLineAt); int colonSearchTo = Math.min(readableBytes, endOfLineAt); int colonAt = buffer.indexOf(colonSearchFrom, colonSearchTo + 1, (byte) 0x3a); // colon not found if (colonAt == -1) { throw new IllegalArgumentException("Colon not found in header line"); } // colon found int headerNameLength = colonAt - colonSearchFrom; StringBuilder headerNameBuilder = new StringBuilder(headerNameLength); for (int i = 0; i < headerNameLength; i++) { headerNameBuilder.append((char) buffer.readByte()); } String headerName = headerNameBuilder.toString(); byte colon = buffer.readByte(); assert colon == 0x3a; int headerValueLength = endOfLineAt - colonAt - 1; StringBuilder headerValueBuilder = new StringBuilder(headerValueLength); for (int i = 0; i < headerValueLength; i++) { headerValueBuilder.append((char) buffer.readByte()); } String headerValue = headerValueBuilder.toString(); // add kind-specific headers switch (message.getKind()) { case PREPARE: PrepareMessage prepareMessage = (PrepareMessage) message; switch (headerName) { case "version": prepareMessage.setVersion(headerValue); break; case "name": prepareMessage.getNames().add(headerValue); break; case "origin": prepareMessage.setOrigin(headerValue); break; case "content-length": contentLength = Integer.parseInt(headerValue); if (contentLength > maxContentLength) { throw new IllegalArgumentException("Content too long"); } break; } break; case PREPARED: case ERROR: case FINISHED: switch (headerName) { case "content-length": contentLength = Integer.parseInt(headerValue); if (contentLength > maxContentLength) { throw new IllegalArgumentException("Content too long"); } break; } break; case NOTIFY: NotifyMessage notifyMessage = (NotifyMessage) message; switch (headerName) { case "barrier": notifyMessage.setBarrier(headerValue); break; } break; case AWAIT: AwaitMessage awaitMessage = (AwaitMessage) message; switch (headerName) { case "barrier": awaitMessage.setBarrier(headerValue); break; } break; default: break; } byte endOfLine = buffer.readByte(); assert endOfLine == 0x0a; return State.READ_HEADER; } private State readContent(ChannelBuffer buffer) throws ScriptParseException { assert contentLength > 0; if (buffer.readableBytes() < contentLength) { return State.READ_CONTENT; } String content = buffer.readBytes(contentLength).toString(UTF_8); switch (message.getKind()) { case PREPARE: PrepareMessage prepareMessage = (PrepareMessage) message; ScriptParserImpl parser = new ScriptParserImpl(); List<String> properties = new ArrayList<>(); for (String scriptFragment : content.split("\\r?\\n")) { // confirm parse-able parser.parseWithStrategy(scriptFragment, PROPERTY_NODE); properties.add(scriptFragment); } prepareMessage.setProperties(properties); break; case PREPARED: PreparedMessage preparedMessage = (PreparedMessage) message; preparedMessage.setScript(content); break; case FINISHED: FinishedMessage finishedMessage = (FinishedMessage) message; finishedMessage.setScript(content); break; case ERROR: ErrorMessage errorMessage = (ErrorMessage) message; errorMessage.setDescription(content); break; default: throw new IllegalStateException("Unexpected message kind: " + message.getKind()); } return State.READ_INITIAL; } }