/* * Copyright (c) 2013-2017 Cinchapi Inc. * * 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 com.cinchapi.concourse.util; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.concurrent.ConcurrentLinkedQueue; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.io.BaseEncoding; /** * Additional utility methods for ByteBuffers that are not found in the * {@link ByteBuffer} class. * * @author Jeff Nelson */ public abstract class ByteBuffers { /** * Return a ByteBuffer that is a new read-only buffer that shares the * content of {@code source} and has the same byte order, but maintains a * distinct position, mark and limit. * * @param source * @return the new, read-only byte buffer */ public static ByteBuffer asReadOnlyBuffer(ByteBuffer source) { int position = source.position(); source.rewind(); ByteBuffer duplicate = source.asReadOnlyBuffer(); duplicate.order(source.order()); // byte order is not natively preserved // when making duplicates: // http://blog.mustardgrain.com/2008/04/04/bytebufferduplicate-does-not-preserve-byte-order/ source.position(position); duplicate.rewind(); return duplicate; } /** * Return a clone of {@code buffer} that has a copy of <em>all</em> its * content and the same position and limit. Unlike the * {@link ByteBuffer#slice()} method, the returned clone * <strong>does not</strong> share its content with {@code buffer}, so * subsequent operations to {@code buffer} or its clone will be * completely independent and won't affect the other. * * @param buffer * @return a clone of {@code buffer} */ public static ByteBuffer clone(ByteBuffer buffer) { ByteBuffer clone = ByteBuffer.allocate(buffer.capacity()); int position = buffer.position(); int limit = buffer.limit(); buffer.rewind(); clone.put(buffer); buffer.position(position); clone.position(position); buffer.limit(limit); clone.limit(limit); return clone; } /** * Transfer the bytes from {@code source} to {@code destination} and resets * {@code source} so that its position remains unchanged. The position of * the {@code destination} is incremented by the number of bytes that are * transferred. * * @param source * @param destination */ public static void copyAndRewindSource(ByteBuffer source, ByteBuffer destination) { int position = source.position(); destination.put(source); source.position(position); } /** * Decode the {@code hex}adeciaml string and return the resulting binary * data. * * @param hex * @return the binary data */ public static ByteBuffer decodeFromHex(String hex) { return ByteBuffer.wrap(BaseEncoding.base16().decode(hex)); } /** * Encode the {@code bytes} as a hexadecimal string. * * @param bytes * @return the hex string */ public static String encodeAsHex(ByteBuffer bytes) { bytes.rewind(); return BaseEncoding.base16().encode(ByteBuffers.toByteArray(bytes)); } /** * Encode the remaining bytes in as {@link ByteBuffer} as a hex string and * maintain the current position. * * @param buffer * @return the hex string */ public static String encodeAsHexString(ByteBuffer buffer) { StringBuilder sb = new StringBuilder(); buffer.mark(); while (buffer.hasRemaining()) { sb.append(String.format("%02x", buffer.get())); } buffer.reset(); return sb.toString(); } /** * Copy the remaining bytes in the {@code source} buffer to the * {@code destination}, expanding if necessary in * order to accommodate the bytes from {@code source}. * * <p> * <strong>NOTE:</strong> This method may modify the {@code limit} for the * destination buffer. * </p> * * @param destination the buffer into which the {@code source} is copied * @param source the buffer that is copied into the {@code destination} * @return a possibly expanded copy of {@code destination} with the * {@code source} bytes copied */ public static ByteBuffer expand(ByteBuffer destination, ByteBuffer source) { destination = ensureRemainingCapacity(destination, source.remaining()); int newLimit = destination.position() + source.remaining(); if(destination.limit() < newLimit) { destination.limit(newLimit); } destination.put(source); return destination; } /** * Put the {@code value} into {@code destination}, expanding if necessary in * order to accommodate the new bytes. * * <p> * <strong>NOTE:</strong> This method may modify the {@code limit} for the * destination buffer. * </p> * * @param destination the buffer into which the {@code source} is copied * @param value the value to add to the {@code destination} * @return a possibly expanded copy of {@code destination} with the * {@code value} bytes copied */ public static ByteBuffer expandInt(ByteBuffer destination, int value) { destination = ensureRemainingCapacity(destination, 4); int newLimit = destination.position() + 4; if(destination.limit() < newLimit) { destination.limit(newLimit); } destination.putInt(value); return destination; } /** * Return a byte buffer that has the UTF-8 encoding for {@code string}. This * method uses some optimization techniques and is the preferable way to * convert strings to byte buffers than doing so manually. * * @param string * @return the byte buffer with the {@code string} data. */ public static ByteBuffer fromString(String string) { try { return ByteBuffer.wrap(string.getBytes(CHARSET)); } catch (Exception e) { throw Throwables.propagate(e); } } /** * Return a ByteBuffer that has a copy of {@code length} bytes from * {@code buffer} starting from the current position. This method will * advance the position of the source buffer. * * @param buffer * @param length * @return a ByteBuffer that has {@code length} bytes from {@code buffer} */ public static ByteBuffer get(ByteBuffer buffer, int length) { Preconditions .checkArgument(buffer.remaining() >= length, "The number of bytes remaining in the buffer cannot be less than length"); byte[] backingArray = new byte[length]; buffer.get(backingArray); return ByteBuffer.wrap(backingArray); } /** * Relative <em>get</em> method. Reads the byte at the current position in * {@code buffer} as a boolean, and then increments the position. * * @param buffer * @return the boolean value at the current position */ public static boolean getBoolean(ByteBuffer buffer) { return buffer.get() > 0 ? true : false; } /** * Relative <em>get</em> method. Reads the enum at the current position in * {@code buffer} and then increments the position by four. * * @param buffer * @param clazz * @return the enum value at the current position */ public static <T extends Enum<?>> T getEnum(ByteBuffer buffer, Class<T> clazz) { return clazz.getEnumConstants()[buffer.getInt()]; } /** * Return a ByteBuffer that has a copy of all the remaining bytes from * {@code buffer} starting from the current position. This method will * advance the position of the source buffer. * * @param buffer the source buffer * @return a ByteBuffer that has the remaining bytes from {@code buffer} */ public static ByteBuffer getRemaining(ByteBuffer buffer) { return get(buffer, buffer.remaining()); } /** * Relative <em>get</em> method. Reads the UTF-8 encoded string at * the current position in {@code buffer}. * * @param buffer * @return the string value at the current position */ public static String getString(ByteBuffer buffer) { return getString(buffer, StandardCharsets.UTF_8); } /** * Relative <em>get</em> method. Reads the {@code charset} encoded string at * the current position in {@code buffer}. * * @param buffer * @param charset * @return the string value at the current position */ public static String getString(ByteBuffer buffer, Charset charset) { CharsetDecoder decoder = null; try { if(charset == StandardCharsets.UTF_8) { while (decoder == null) { decoder = DECODERS.poll(); } } else { decoder = charset.newDecoder(); } decoder.onMalformedInput(CodingErrorAction.IGNORE); return decoder.decode(buffer).toString(); } catch (CharacterCodingException e) { throw Throwables.propagate(e); } finally { if(decoder != null && charset == StandardCharsets.UTF_8) { DECODERS.offer(decoder); } } } /** * Return a ByteBuffer that contains a single null byte. * * @return a null byte buffer */ public static ByteBuffer nullByteBuffer() { ByteBuffer nullByte = ByteBuffer.allocate(1); nullByte.put((byte) 0); nullByte.rewind(); return nullByte; } /** * Put the UTF-8 encoding for the {@code source} string into the * {@code destination} byte buffer and increment the position by the length * of the strings byte sequence. This method uses some optimization * techniques and is the preferable way to add strings to byte buffers than * doing so manually. * * @param source * @param destination */ public static void putString(String source, ByteBuffer destination) { try { byte[] bytes = source.getBytes(CHARSET); destination.put(bytes); } catch (Exception e) { throw Throwables.propagate(e); } } /** * The exact same as {@link ByteBuffer#rewind()} except it returns a typed * {@link ByteBuffer} instead of a generic {@link Buffer}. * * @param buffer * @return {@code buffer} */ public static ByteBuffer rewind(ByteBuffer buffer) { buffer.rewind(); return buffer; } /** * Return a new ByteBuffer whose content is a shared subsequence of the * content in {@code buffer} starting at the current position to * current position + {@code length} (non-inclusive). Invoking this method * has the same affect as doing the following: * * <pre> * buffer.mark(); * int oldLimit = buffer.limit(); * buffer.limit(buffer.position() + length); * * ByteBuffer slice = buffer.slice(); * * buffer.reset(); * buffer.limit(oldLimit); * </pre> * * @param buffer * @param length * @return the new ByteBuffer slice * @see ByteBuffer#slice() */ public static ByteBuffer slice(ByteBuffer buffer, int length) { return slice(buffer, buffer.position(), length); } /** * Return a new ByteBuffer whose content is a shared subsequence of the * content in {@code buffer} starting at {@code position} to * {@code position} + {@code length} (non-inclusive). Invoking this method * has the same affect as doing the following: * * <pre> * buffer.mark(); * int oldLimit = buffer.limit(); * buffer.position(position); * buffer.limit(position + length); * * ByteBuffer slice = buffer.slice(); * * buffer.reset(); * buffer.limit(oldLimit); * </pre> * * @param buffer * @param position * @param length * @return the new ByteBuffer slice * @see ByteBuffer#slice() */ public static ByteBuffer slice(ByteBuffer buffer, int position, int length) { int oldPosition = buffer.position(); int oldLimit = buffer.limit(); buffer.position(position); buffer.limit(position + length); ByteBuffer slice = buffer.slice(); buffer.limit(oldLimit); buffer.position(oldPosition); return slice; } /** * Return a byte array with the content of {@code buffer}. This method * returns the byte array that backs {@code buffer} if one exists, otherwise * it creates a new byte array with the content between the current position * of {@code buffer} and its limit. * * @param buffer * @return the byte array with the content of {@code buffer} */ public static byte[] toByteArray(ByteBuffer buffer) { if(buffer.hasArray()) { return buffer.array(); } else { buffer.mark(); byte[] array = new byte[buffer.remaining()]; buffer.get(array); buffer.reset(); return array; } } /** * Return a UTF-8 {@link CharBuffer} representation of the bytes in the * {@code buffer}. * * @param buffer * @return the char buffer */ public static CharBuffer toCharBuffer(ByteBuffer buffer) { return toCharBuffer(buffer, StandardCharsets.UTF_8); } /** * Return a {@link CharBuffer} representation of the bytes in the * {@code buffer} encoded with the {@code charset}. * * @param buffer * @param charset * @return the char buffer */ public static CharBuffer toCharBuffer(ByteBuffer buffer, Charset charset) { buffer.mark(); CharBuffer chars = charset.decode(buffer); buffer.reset(); return chars; } /** * Ensure that {@code buffer} has {@code capacity} bytes * {@link ByteBuffer#remaining() remaining} and return either {@code buffer} * or a copy that has enough capacity. * * @param buffer the buffer to check for remaining capacity * @param capacity the number of bytes required * @return a {@link ByteBuffer} with all the contents of {@code buffer} and * enough remaining room for {@code capacity} bytes */ private static ByteBuffer ensureRemainingCapacity(ByteBuffer buffer, int capacity) { if((buffer.capacity() - buffer.position()) < capacity) { ByteBuffer copy = ByteBuffer .allocate(((buffer.capacity() + capacity) * 3) / 2 + 1); buffer.limit(buffer.position()); buffer.rewind(); copy.put(buffer); buffer = copy; } return buffer; } /** * The name of the Charset to use for encoding/decoding. We use the name * instead of the charset object because Java caches encoders when * referencing them by name, but creates a new encorder object when * referencing them by Charset object. */ private static final String CHARSET = StandardCharsets.UTF_8.name(); /** * A collection of UTF-8 decoders that can be concurrently used. We use this * to avoid creating a new decoder every time we need to decode a string * while still allowing multi-threaded access. */ private static final ConcurrentLinkedQueue<CharsetDecoder> DECODERS = new ConcurrentLinkedQueue<CharsetDecoder>(); /** * The number of UTF-8 decoders to create for concurrent access. */ private static final int NUM_DECODERS = 10; static { try { for (int i = 0; i < NUM_DECODERS; ++i) { DECODERS.add(StandardCharsets.UTF_8.newDecoder()); } } catch (Exception e) { throw Throwables.propagate(e); } } }