/************************************************************************* * * * This file is part of the 20n/act project. * * 20n/act enables DNA prediction for synthetic biology/bioengineering. * * Copyright (C) 2017 20n Labs, Inc. * * * * Please direct all queries to act@20n.com. * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see <http://www.gnu.org/licenses/>. * * * *************************************************************************/ package com.act.lcms.v2.fullindex; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; /** * Package-private utility methods for de/serialization and byte buffer manipulation. */ public class Utils { /** * Seralize a Serializable object. * @param obj The object to serialize. * @param <T> The type of the object to serialize. Note that this is not bound by serializable for convenience. * @return The binary representation of that object. * @throws IOException */ static <T> byte[] serializeObject(T obj) throws IOException { try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oo = new ObjectOutputStream(bos)) { oo.writeObject(obj); oo.flush(); return bos.toByteArray(); } } /** * Deserialize an object that had been serialized via {@link #serializeObject(Object)}. * @param bytes The binary representation of an object. * @param <T> The type of the object to deserialize. * @return The deserialized object. * @throws IOException * @throws ClassNotFoundException */ static <T> T deserializeObject(byte[] bytes) throws IOException, ClassNotFoundException { try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) { // Assumes you know what you're getting into when deserializing. Don't use this blindly. return (T) ois.readObject(); } } /* ---------------------------------------- * Byte buffer utilities. * These are all package private so that sibling classes can read from the DB. * */ /** * Ensure that a buffer is prepared for reading from the beginning. * @param b The buffer to check. */ static void assertReadyForFreshRead(ByteBuffer b) { assert(b.limit() != 0); // A zero limit would cause an immediate buffer underflow. assert(b.position() == 0); // A non-zero position means either the buffer hasn't been flipped or has been read from. } /** * Convert a list of floating point numbers to a (compact) array of bytes. Note that this does not use object * output streams, but writes the floats' binary representations directly. * @param vals The values to serialize as raw bytes. * @return A byte array containing the floats in binary form. */ static byte[] floatListToByteArray(List<Float> vals) { ByteBuffer buffer = ByteBuffer.allocate(vals.size() * Float.BYTES); // Don't waste any bits! vals.forEach(buffer::putFloat); buffer.flip(); // Prep for reading. return toCompactArray(buffer); // Compact just to be safe, check invariants. } /** * Convert an array of float bytes from {@link #floatListToByteArray(List)} back to a list of floats. * @param bytes The bytes to read. * @return A list of Float objects constructed from those bytes. */ static List<Float> byteArrayToFloatList(byte[] bytes) { ByteBuffer buffer = ByteBuffer.wrap(bytes); List<Float> vals = new ArrayList<>(bytes.length / Float.BYTES); while (buffer.hasRemaining()) { vals.add(buffer.getFloat()); } return vals; } /** * Return an appropriately sized byte array filled with the contents of src. * * The {@link ByteBuffer#array()} call returns the array that backs the buffer, which is sometimes larger than the * data contained in the buffer. For instance, in this example: * <pre> * {@code * ByteBuffer buffer = ByteBuffer.allocate(16); * buffer.put("hello world!".getBytes()); * byte[] arr = buffer.array(); * } * </pre> * {@code arr} will always have length 16, even though we've only written 12 bytes to it. * * @param src * @return */ static byte[] toCompactArray(ByteBuffer src) { // Assume src is pre-flipped to avoid a double-flip. if (src.hasArray() && src.limit() >= src.capacity()) { // No need to compact if limit covers the full backing array. return src.array(); } assertReadyForFreshRead(src); // Otherwise, we need to allocate and copy into a new array of the correct size. // TODO: is Arrays.copyOf any faster than this? Ideally we want CoW semantics here... // Warning: do not use src.compact(). That does something entirely different. /* We use a byte array here rather than another byte buffer for two reasons: * 1) It's possible for byte buffers to reside off-heap (like in shared pages), and we definitely want the array on * the heap. * 2) If the byte-buffer resides on the heap, it's allocation will be identical, and we save the object management * overhead by not creating another ByteBuffer. */ byte[] dest = new byte[src.limit()]; src.get(dest); // Copy the contents of the buffer into our appropriately sized array. assert(src.position() == src.limit()); // All bytes should have been read. src.rewind(); // Be kind: rewind. return dest; } /** * Appends the contents of toAppend to dest if there is capacity, or does the equivalent of C's "realloc" by * allocating a larger buffer and copying both dest and toAppend into that new buffer. * * Always call this function as: * <pre> * {@code * dest = appendOrRealloc(dest, toAppend) * } * </pre> * as we can't actually resize or reallocate dest once it's been allocated. * * @param dest The buffer into which to write or to reallocate if necessary. * @param toAppend The data to write into dest. * @return dest or a new, larger buffer containing both dest and toAppend. */ static ByteBuffer appendOrRealloc(ByteBuffer dest, ByteBuffer toAppend) { /* Before reading this function, review this excellent explanation of the behaviors of ByteBuffers: * http://mindprod.com/jgloss/bytebuffer.html */ // Assume toAppend is pre-flipped to avoid a double flip, which would be Very Bad. if (dest.capacity() - dest.position() < toAppend.limit()) { // Whoops, we have to reallocate! ByteBuffer newDest = ByteBuffer.allocate(dest.capacity() << 1); // TODO: make sure these are page-divisible sizes. dest.flip(); // Switch dest to reading mode. newDest.put(dest); // Write all of dest into the new buffer. dest = newDest; } dest.put(toAppend); return dest; } }