/* * Copyright (C) 2012-2015 DataStax 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.datastax.driver.examples.datatypes; import com.datastax.driver.core.*; import com.datastax.driver.core.utils.Bytes; import java.io.*; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.HashMap; import java.util.Map; /** * Inserts and retrieves values in BLOB columns. * <p/> * By default, the Java driver maps this type to {@link java.nio.ByteBuffer}. The ByteBuffer API is a bit tricky to use * at times, so we will show common pitfalls as well. We strongly recommend that you read the {@link java.nio.Buffer} * and {@link ByteBuffer} API docs and become familiar with the capacity, limit and position properties. * <a href="http://tutorials.jenkov.com/java-nio/buffers.html">This tutorial</a> might also help. * <p/> * Preconditions: * - a Cassandra cluster is running and accessible through the contacts points identified by CONTACT_POINTS and PORT; * - FILE references an existing file. * <p/> * Side effects: * - creates a new keyspace "examples" in the cluster. If a keyspace with this name already exists, it will be reused; * - creates a table "examples.blobs". If it already exists, it will be reused; * - inserts data in the table. */ public class Blobs { static String[] CONTACT_POINTS = {"127.0.0.1"}; static int PORT = 9042; static File FILE = new File(Blobs.class.getResource("/cassandra_logo.png").getFile()); public static void main(String[] args) throws IOException { Cluster cluster = null; try { cluster = Cluster.builder() .addContactPoints(CONTACT_POINTS).withPort(PORT) .build(); Session session = cluster.connect(); createSchema(session); allocateAndInsert(session); retrieveSimpleColumn(session); retrieveMapColumn(session); insertConcurrent(session); insertFromAndRetrieveToFile(session); } finally { if (cluster != null) cluster.close(); } } private static void createSchema(Session session) { session.execute("CREATE KEYSPACE IF NOT EXISTS examples " + "WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}"); session.execute("CREATE TABLE IF NOT EXISTS examples.blobs(k int PRIMARY KEY, b blob, m map<text, blob>)"); } private static void allocateAndInsert(Session session) { // One way to get a byte buffer is to allocate it and fill it yourself: ByteBuffer buffer = ByteBuffer.allocate(16); while (buffer.hasRemaining()) buffer.put((byte) 0xFF); // Don't forget to flip! The driver expects a buffer that is ready for reading. That is, it will consider all // the data between buffer.position() and buffer.limit(). // Right now we are positioned at the end because we just finished writing, so if we passed the buffer as-is it // would appear to be empty: assert buffer.limit() - buffer.position() == 0; buffer.flip(); // Now position is back to the beginning, so the driver will see all 16 bytes. assert buffer.limit() - buffer.position() == 16; Map<String, ByteBuffer> map = new HashMap<String, ByteBuffer>(); map.put("test", buffer); session.execute("INSERT INTO examples.blobs (k, b, m) VALUES (1, ?, ?)", buffer, map); } private static void retrieveSimpleColumn(Session session) { Row row = session.execute("SELECT b, m FROM examples.blobs WHERE k = 1").one(); ByteBuffer buffer = row.getBytes("b"); // The driver always returns buffers that are ready for reading. assert buffer.limit() - buffer.position() == 16; // One way to read from the buffer is to use absolute getters. Do NOT start reading at index 0, as the buffer // might start at a different position (we'll see an example of that later). for (int i = buffer.position(); i < buffer.limit(); i++) { byte b = buffer.get(i); assert b == (byte) 0xFF; } // Another way is to use relative getters. while (buffer.hasRemaining()) { byte b = buffer.get(); assert b == (byte) 0xFF; } // Note that relative getters change the position, so when we're done reading we're at the end again. assert buffer.position() == buffer.limit(); // Reset the position for the next operation. buffer.flip(); // Yet another way is to convert the buffer to a byte array. Do NOT use buffer.array(), because it returns the // buffer's *backing array*, which is not the same thing as its contents: // - not all byte buffers have backing arrays // - even then, the backing array might be larger than the buffer's contents // // The driver provides a utility method that handles those details for you: byte[] array = Bytes.getArray(buffer); assert array.length == 16; for (byte b : array) { assert b == (byte) 0xFF; } } private static void retrieveMapColumn(Session session) { Row row = session.execute("SELECT b, m FROM examples.blobs WHERE k = 1").one(); // The map columns illustrates the pitfalls with position() and array(). Map<String, ByteBuffer> m = row.getMap("m", String.class, ByteBuffer.class); ByteBuffer buffer = m.get("test"); // We did get back a buffer that contains 16 bytes as expected. assert buffer.limit() - buffer.position() == 16; // However, it is not positioned at 0. And you can also see that its backing array contains more than 16 bytes. // What happens is that the buffer is a "view" of the last 16 of a 32-byte array. // This is an implementation detail and you shouldn't have to worry about it if you process the buffer correctly // (don't iterate from 0, use Bytes.getArray()). assert buffer.position() == 16; assert buffer.array().length == 32; } private static void insertConcurrent(Session session) { PreparedStatement preparedStatement = session.prepare("INSERT INTO examples.blobs (k, b) VALUES (1, :b)"); // This is another convenient utility provided by the driver. It's useful for tests. ByteBuffer buffer = Bytes.fromHexString("0xffffff"); // When you pass a byte buffer to a bound statement, it creates a shallow copy internally with the // buffer.duplicate() method. BoundStatement boundStatement = preparedStatement.bind(); boundStatement.setBytes("b", buffer); // This means you can now move in the original buffer, without affecting the insertion if it happens later. buffer.position(buffer.limit()); session.execute(boundStatement); Row row = session.execute("SELECT b FROM examples.blobs WHERE k = 1").one(); assert Bytes.toHexString(row.getBytes("b")).equals("0xffffff"); buffer.flip(); // HOWEVER duplicate() only performs a shallow copy. The two buffers still share the same contents. So if you // modify the contents of the original buffer, this will affect another execution of the bound statement. buffer.put(0, (byte) 0xaa); session.execute(boundStatement); row = session.execute("SELECT b FROM examples.blobs WHERE k = 1").one(); assert Bytes.toHexString(row.getBytes("b")).equals("0xaaffff"); // This will also happen if you use the async API, e.g. create the bound statement, call executeAsync() on it // and reuse the buffer immediately. // If you reuse buffers concurrently and want to avoid those issues, perform a deep copy of the buffer before // passing it to the bound statement. int startPosition = buffer.position(); ByteBuffer buffer2 = ByteBuffer.allocate(buffer.limit() - startPosition); buffer2.put(buffer); buffer.position(startPosition); buffer2.flip(); boundStatement.setBytes("b", buffer2); session.execute(boundStatement); // Note: unlike BoundStatement, SimpleStatement does not duplicate its arguments, so even the position will be // affected if you change it before executing the statement. Again, resort to deep copies if required. } private static void insertFromAndRetrieveToFile(Session session) throws IOException { ByteBuffer buffer = readAll(FILE); session.execute("INSERT INTO examples.blobs (k, b) VALUES (1, ?)", buffer); File tmpFile = File.createTempFile("blob", ".png"); System.out.printf("Writing retrieved buffer to %s%n", tmpFile.getAbsoluteFile()); Row row = session.execute("SELECT b FROM examples.blobs WHERE k = 1").one(); writeAll(row.getBytes("b"), tmpFile); } // Note: // - this is written with Java 6 APIs; if you're on a more recent version this can be improved (try-with-resources, // new-new io...) // - this reads the whole file in memory in one go. If your file does not fit in memory you should probably not // insert it into Cassandra either ;) private static ByteBuffer readAll(File file) throws IOException { FileInputStream inputStream = null; boolean threw = false; try { inputStream = new FileInputStream(file); FileChannel channel = inputStream.getChannel(); ByteBuffer buffer = ByteBuffer.allocate((int) channel.size()); channel.read(buffer); buffer.flip(); return buffer; } catch (IOException e) { threw = true; throw e; } finally { close(inputStream, threw); } } private static void writeAll(ByteBuffer buffer, File file) throws IOException { FileOutputStream outputStream = null; boolean threw = false; try { outputStream = new FileOutputStream(file); FileChannel channel = outputStream.getChannel(); channel.write(buffer); } catch (IOException e) { threw = true; throw e; } finally { close(outputStream, threw); } } private static void close(Closeable inputStream, boolean threw) throws IOException { if (inputStream != null) try { inputStream.close(); } catch (IOException e) { if (!threw) throw e; // else preserve original exception } } }