diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServer.java b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServer.java new file mode 100644 index 0000000000..67cb476e98 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you 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.jboss.netty.example.http.websocketx.autobahn; + +import java.net.InetSocketAddress; +import java.util.concurrent.Executors; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jboss.netty.bootstrap.ServerBootstrap; +import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; + +/** + * A Web Socket echo server for running the autobahn + * test suite + * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServer { + public static void main(String[] args) { + ConsoleHandler ch = new ConsoleHandler(); + ch.setLevel(Level.FINE); + Logger.getLogger("").addHandler(ch); + Logger.getLogger("").setLevel(Level.FINE); + + // Configure the server. + ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory( + Executors.newCachedThreadPool(), Executors.newCachedThreadPool())); + + //bootstrap.setOption("child.tcpNoDelay", true); + + // Set up the event pipeline factory. + bootstrap.setPipelineFactory(new WebSocketServerPipelineFactory()); + + // Bind and start to accept incoming connections. + bootstrap.bind(new InetSocketAddress(9000)); + + System.out.println("Web Socket Server started on 9000. Open your browser and navigate to http://localhost:9000/"); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerHandler.java b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerHandler.java new file mode 100644 index 0000000000..f3a084282f --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerHandler.java @@ -0,0 +1,139 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you 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.jboss.netty.example.http.websocketx.autobahn; + +import static org.jboss.netty.handler.codec.http.HttpHeaders.*; +import static org.jboss.netty.handler.codec.http.HttpMethod.*; +import static org.jboss.netty.handler.codec.http.HttpResponseStatus.*; +import static org.jboss.netty.handler.codec.http.HttpVersion.*; + +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.ChannelFuture; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelUpstreamHandler; +import org.jboss.netty.handler.codec.http.DefaultHttpResponse; +import org.jboss.netty.handler.codec.http.HttpHeaders; +import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; +import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; +import org.jboss.netty.util.CharsetUtil; + +/** + * Handles handshakes and messages + * + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServerHandler extends SimpleChannelUpstreamHandler { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandler.class); + + private WebSocketServerHandshaker handshaker = null; + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + Object msg = e.getMessage(); + if (msg instanceof HttpRequest) { + handleHttpRequest(ctx, (HttpRequest) msg); + } else if (msg instanceof WebSocketFrame) { + handleWebSocketFrame(ctx, (WebSocketFrame) msg); + } + } + + private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req) throws Exception { + // Allow only GET methods. + if (req.getMethod() != GET) { + sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN)); + return; + } + + // Handshake + WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( + this.getWebSocketLocation(req), null, false); + this.handshaker = wsFactory.newHandshaker(ctx, req); + if (this.handshaker == null) { + wsFactory.sendUnsupportedWebSocketVersionResponse(ctx); + } else { + this.handshaker.executeOpeningHandshake(ctx, req); + } + return; + } + + private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { + logger.debug(String + .format("Channel %s received %s", ctx.getChannel().getId(), frame.getClass().getSimpleName())); + + if (frame instanceof CloseWebSocketFrame) { + this.handshaker.executeClosingHandshake(ctx, (CloseWebSocketFrame) frame); + } else if (frame instanceof PingWebSocketFrame) { + ctx.getChannel().write( + new PongWebSocketFrame(frame.isFinalFragment(), frame.getRsv(), frame.getBinaryData())); + } else if (frame instanceof TextWebSocketFrame) { + //String text = ((TextWebSocketFrame) frame).getText(); + ctx.getChannel().write(new TextWebSocketFrame(frame.isFinalFragment(), frame.getRsv(), frame.getBinaryData())); + } else if (frame instanceof BinaryWebSocketFrame) { + ctx.getChannel().write( + new BinaryWebSocketFrame(frame.isFinalFragment(), frame.getRsv(), frame.getBinaryData())); + } else if (frame instanceof ContinuationWebSocketFrame) { + ctx.getChannel().write( + new ContinuationWebSocketFrame(frame.isFinalFragment(), frame.getRsv(), frame.getBinaryData())); + } else if (frame instanceof PongWebSocketFrame) { + // Ignore + } else { + throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass() + .getName())); + } + } + + private void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) { + // Generate an error page if response status code is not OK (200). + if (res.getStatus().getCode() != 200) { + res.setContent(ChannelBuffers.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8)); + setContentLength(res, res.getContent().readableBytes()); + } + + // Send the response and close the connection if necessary. + ChannelFuture f = ctx.getChannel().write(res); + if (!isKeepAlive(req) || res.getStatus().getCode() != 200) { + f.addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { + e.getCause().printStackTrace(); + e.getChannel().close(); + } + + private String getWebSocketLocation(HttpRequest req) { + return "ws://" + req.getHeader(HttpHeaders.Names.HOST); + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerPipelineFactory.java b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerPipelineFactory.java new file mode 100644 index 0000000000..e7bfd52999 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/WebSocketServerPipelineFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you 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.jboss.netty.example.http.websocketx.autobahn; + +import static org.jboss.netty.channel.Channels.*; + +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineFactory; +import org.jboss.netty.handler.codec.http.HttpChunkAggregator; +import org.jboss.netty.handler.codec.http.HttpRequestDecoder; +import org.jboss.netty.handler.codec.http.HttpResponseEncoder; + +/** + * @author The Netty Project + * @author Trustin Lee + * @author Vibul Imtarnasan + * + * @version $Rev$, $Date$ + */ +public class WebSocketServerPipelineFactory implements ChannelPipelineFactory { + @Override + public ChannelPipeline getPipeline() throws Exception { + // Create a default pipeline implementation. + ChannelPipeline pipeline = pipeline(); + pipeline.addLast("decoder", new HttpRequestDecoder()); + pipeline.addLast("aggregator", new HttpChunkAggregator(65536)); + pipeline.addLast("encoder", new HttpResponseEncoder()); + pipeline.addLast("handler", new WebSocketServerHandler()); + return pipeline; + } +} diff --git a/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/package-info.java b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/package-info.java new file mode 100644 index 0000000000..46de920b06 --- /dev/null +++ b/src/main/java/org/jboss/netty/example/http/websocketx/autobahn/package-info.java @@ -0,0 +1,54 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat licenses this file to you 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. + */ + +/** + * This package is intended for use with testing against the Python + * AutoBahn test suite. + * + *

How to run the tests on Ubuntu

+ * + *

01. Add ppa:twisted-dev/ppa to your system's Software Sources + * + *

02. Install Twisted V11: sudo apt-get install python-twisted + * + *

03. Intall Python Setup Tools: sudo apt-get install python-setuptools + * + *

04. Install AutoBahn: sudo easy_install Autobahn + * + *

05. Get AutoBahn testsuite source code: git clone git@github.com:oberstet/Autobahn.git + * + *

06. Go to AutoBahn directory: cd Autobahn + * + *

07. Checkout stable version: git checkout v0.4.2 + * + *

08. Go to test suite directory: cd testsuite/websockets + * + *

09. Edit fuzzing_clinet_spec.json and set the version to 10. + * + * { + * "servers": [{"agent": "Netty", "hostname": "localhost", "port": 9000, "version": 10}], + * "cases": ["*"] + * } + * + * + *

10. Run the test python fuzzing_client.py. Note that the actual test case python code is + * located in /usr/local/lib/python2.6/dist-packages/autobahn-0.4.2-py2.6.egg/autobahn/cases + * and not in the checked out git repository. + * + *

11. See the results in reports/servers/index.html + */ +package org.jboss.netty.example.http.websocketx.autobahn; + diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java new file mode 100644 index 0000000000..ec7db870f7 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java @@ -0,0 +1,144 @@ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you 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.jboss.netty.handler.codec.http.websocketx; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.util.CharsetUtil; + +/** + * Web Socket continuation frame containing continuation text or binary data. + * This is used for fragmented messages where the contents of a messages is + * contained more than 1 frame. + * + * @author Vibul Imtarnasan + */ +public class ContinuationWebSocketFrame extends WebSocketFrame { + + private String aggregatedText = null; + + /** + * Creates a new empty continuation frame. + */ + public ContinuationWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new continuation frame with the specified binary data. The + * final fragment flag is set to true. + * + * @param binaryData + * the content of the frame. + */ + public ContinuationWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new continuation frame with the specified binary data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. + */ + public ContinuationWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + /** + * Creates a new continuation frame with the specified binary data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. + * @param aggregatedText + * Aggregated text set by decoder on the final continuation frame + * of a fragmented text message + */ + public ContinuationWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData, String aggregatedText) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + this.aggregatedText = aggregatedText; + } + + /** + * Creates a new continuation frame with the specified text data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param text + * text content of the frame. + */ + public ContinuationWebSocketFrame(boolean finalFragment, int rsv, String text) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setText(text); + } + + /** + * Returns the text data in this frame + */ + public String getText() { + if (this.getBinaryData() == null) { + return null; + } + return this.getBinaryData().toString(CharsetUtil.UTF_8); + } + + /** + * Sets the string for this frame + * + * @param text + * text to store + */ + public void setText(String text) { + if (text == null || text.isEmpty()) { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } else { + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; + } + + /** + * Aggregated text returned by decoder on the final continuation frame of a + * fragmented text message + */ + public String getAggregatedText() { + return aggregatedText; + } + + public void setAggregatedText(String aggregatedText) { + this.aggregatedText = aggregatedText; + } + +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java new file mode 100644 index 0000000000..358f52684a --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Exception.java @@ -0,0 +1,35 @@ +/* + * Adaptation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + * + * Copyright (c) 2008-2009 Bjoern Hoehrmann + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions + * of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +/** + * Invalid UTF8 bytes encountered + * + * @author Bjoern Hoehrmann + * @author https://github.com/joewalnes/webbit + * @author Vibul Imtarnasan + */ +public class UTF8Exception extends RuntimeException { + private static final long serialVersionUID = 1L; + + public UTF8Exception(String reason) { + super(reason); + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java new file mode 100644 index 0000000000..9e33df9bc0 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/websocketx/UTF8Output.java @@ -0,0 +1,84 @@ +/* + * Adaptation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + * + * Copyright (c) 2008-2009 Bjoern Hoehrmann + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions + * of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +package org.jboss.netty.handler.codec.http.websocketx; + +/** + * Checks UTF8 bytes for validity before converting it into a string + * + * @author Bjoern Hoehrmann + * @author https://github.com/joewalnes/webbit + * @author Vibul Imtarnasan + */ +public class UTF8Output { + private static final int UTF8_ACCEPT = 0; + private static final int UTF8_REJECT = 12; + + private static final byte[] TYPES = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 11, + 6, 6, 6, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 }; + + private static final byte[] STATES = { 0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 0, 12, 12, 12, 12, 12, 0, 12, 0, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 24, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12 }; + + private int state = UTF8_ACCEPT; + private int codep = 0; + + private final StringBuilder stringBuilder; + + public UTF8Output(byte[] bytes) { + stringBuilder = new StringBuilder(bytes.length); + write(bytes); + } + + public void write(byte[] bytes) { + for (byte b : bytes) { + write(b); + } + } + + public void write(int b) { + byte type = TYPES[b & 0xFF]; + + codep = (state != UTF8_ACCEPT) ? (b & 0x3f) | (codep << 6) : (0xff >> type) & (b); + + state = STATES[state + type]; + + if (state == UTF8_ACCEPT) { + stringBuilder.append((char) codep); + } else if (state == UTF8_REJECT) { + throw new UTF8Exception("bytes are not UTF-8"); + } + } + + public String toString() { + if (state != UTF8_ACCEPT) { + throw new UTF8Exception("bytes are not UTF-8"); + } + return stringBuilder.toString(); + } +}