/** * This file is part of Erjang - A JVM-based Erlang VM * * Copyright (c) 2010 by Trifork * * 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 erjang; import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CoderResult; import java.nio.CharBuffer; import java.nio.ByteBuffer; /** Used by the unicode BIFs characters_to_binary/2 and characters_to_list/2. */ public class CharCollector { public static ByteBuffer EMPTY = ByteBuffer.wrap(new byte[0]); public static int BUF_SIZE = 20486; CharsetDecoder decoder; CharBuffer buffer; Appendable output; boolean dirtyDecoder = false; public CharCollector(Charset charset, Appendable output) { buffer = CharBuffer.allocate(BUF_SIZE); this.output = output; this.decoder = charset.newDecoder(); this.decoder.reset(); } public ESeq addInteger(int value, ESeq rest) throws IOException, DecodingException { if (rest != ERT.NIL) { return rest.cons(ERT.box(value)); } if (dirtyDecoder) try { flushDecoder(); } catch (PartialDecodingException e) { // Incomplete binary was followed by an integer... // Convert exception. throw new DecodingException(); } char c = (char)value; if (c != value || !Character.isDefined(value)) fail(new DecodingException()); if (! buffer.hasRemaining()) flushBuffer(); /* Or write to output directly? Provided that we * flush the buffer when we do the decoder. */ buffer.put(c); return rest; } /** Add a byte array, interpreted as a list of integers. * Equivalent to (but faster than) calling add(int) for each of * the elements. * @return * @throws PartialDecodingException if these integers follow an * incomplete binary. */ public ESeq addIntegers(byte[] data, int offset, int length, ESeq rest) throws IOException, PartialDecodingException { if (rest != ERT.NIL) { return rest.cons(EBinary.make(data, offset, length, 0)); } if (length==0) return rest; if (dirtyDecoder) flushDecoder(); while (length > 0) { int free = buffer.remaining(); while (length > 0 && free > 0) { char c = (char)(data[offset] & 0xFF); buffer.put(c); offset++; length--; free--; // Could be smarter. } if (free==0) flushBuffer(); } return rest; } /** Add a byte array, interpreted as a binary to be decoded. * @param rest TODO * @return TODO*/ public ESeq addBinary(byte[] data, int offset, int length, ESeq rest) throws IOException, PartialDecodingException { if (rest != ERT.NIL) { return rest.cons(EBinary.make(data, offset, length, 0)); } return addBinary(ByteBuffer.wrap(data, offset, length), false, rest); } public ESeq addBinary(ByteBuffer data, boolean endOfInput, ESeq rest) throws IOException, PartialDecodingException { if (rest != ERT.NIL) { return rest.cons(EBinary.make(data)); } CoderResult res; do { res = decoder.decode(data, buffer, endOfInput); if (!handle(res)) fail(new PartialDecodingException(data.position())); } while (res == CoderResult.OVERFLOW); if (data.hasRemaining()) { if (!endOfInput) { decoder.decode(ByteBuffer.wrap(new byte[0]), buffer, true); // flush the decoder ... // decoder.flush(buffer); } return rest.cons(EBinary.make(data)); } // The decoder may have left some data. Save that in some fashion... dirtyDecoder = true; return rest; } protected boolean handle(CoderResult res) throws IOException { if (res == CoderResult.UNDERFLOW) { return true; } else if (res == CoderResult.OVERFLOW) { flushBuffer(); return true; } else return false; } public void end() throws IOException, PartialDecodingException { flushDecoder(); flushBuffer(); } protected void fail(DecodingException e) throws DecodingException, IOException { flushBuffer(); throw e; } protected void fail(PartialDecodingException e) throws PartialDecodingException, IOException { flushBuffer(); throw e; } protected void flushDecoder() throws IOException,PartialDecodingException { addBinary(EMPTY, true, ERT.NIL); decoder.flush(buffer); decoder.reset(); dirtyDecoder = false; } protected void flushBuffer() throws IOException { buffer.flip(); output.append(buffer); } @SuppressWarnings("serial") public static class DecodingException extends Exception { } @SuppressWarnings("serial") public static class PartialDecodingException extends Exception { public final int inputPos; public PartialDecodingException(int inputPos) { this.inputPos = inputPos; } } @SuppressWarnings("serial") public static class InvalidElementException extends Exception { } @SuppressWarnings("serial") public static class CollectingException extends Exception { public final EObject restOfInput; public CollectingException(EObject restOfInput) { this.restOfInput = restOfInput; } } /** * @param data * @param off * @param length * @throws PartialDecodingException * @throws IOException */ public ESeq addIntegers(char[] data, int offset, int length, ESeq rest) throws IOException, PartialDecodingException { if (length==0) return rest; if (dirtyDecoder) flushDecoder(); while (length > 0) { int free = buffer.remaining(); while (length > 0 && free > 0) { char c = (char)(data[offset] & 0xFFFF); buffer.put(c); offset++; length--; free--; // Could be smarter. } if (free==0) flushBuffer(); } return rest; } }