/*
* Copyright (c) 2014 Oculus Info Inc.
* http://www.oculusinfo.com/
*
* Released under the MIT License.
*
* 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 com.oculusinfo.binning.io.impl;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import com.oculusinfo.binning.TileData;
import com.oculusinfo.binning.TileIndex;
import com.oculusinfo.binning.io.PyramidIO;
import com.oculusinfo.binning.io.serialization.TileSerializer;
import org.json.JSONObject;
/**
* JDBC-based implementation of PyramidIO.
*
* @author rcameron
*
*/
public class JDBCPyramidIO implements PyramidIO {
private static final String TABLE_METADATA = "metadata";
private static final int BATCH_SIZE = 10000;
private static final String COL_ZOOM_LVL = "zoom_level";
private static final String COL_TILE_COLUMN = "tile_column";
private static final String COL_TILE_ROW = "tile_row";
private static final String COL_TILE_DATA = "tile_data";
private static final String COL_METADATA = "metadata";
private static final String COL_PYRAMID_ID = "pyramid_id";
private Connection _connection;
public JDBCPyramidIO(String driverClassName, String dbUrl) throws ClassNotFoundException, SQLException {
Class.forName(driverClassName);
_connection = DriverManager.getConnection(dbUrl);
}
public void shutdown() {
try {
_connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void initializeForWrite(String pyramidId) throws IOException {
// Create the table and columns if necessary.
Statement stmt = null;
try {
if (!tableExists(pyramidId)) {
StringBuilder sb = new StringBuilder();
sb.append("CREATE TABLE ");
sb.append(toTableName(pyramidId));
sb.append(" (");
sb.append(COL_ZOOM_LVL);
sb.append(" INTEGER NOT NULL, ");
sb.append(COL_TILE_COLUMN);
sb.append(" INTEGER NOT NULL, ");
sb.append(COL_TILE_ROW);
sb.append(" INTEGER NOT NULL, ");
sb.append(COL_TILE_DATA);
sb.append(" BLOB,");
sb.append(" CONSTRAINT pk_TileIndex PRIMARY KEY (");
sb.append(COL_ZOOM_LVL);
sb.append(",");
sb.append(COL_TILE_COLUMN);
sb.append(",");
sb.append(COL_TILE_ROW);
sb.append("))");
stmt = _connection.createStatement();
stmt.executeUpdate(sb.toString());
}
if (!tableExists(TABLE_METADATA)) {
StringBuilder sb = new StringBuilder();
sb.append("CREATE TABLE ");
sb.append(TABLE_METADATA);
sb.append(" (");
sb.append(COL_PYRAMID_ID);
sb.append(" TEXT PRIMARY KEY, metadata TEXT)");
if (stmt == null) stmt = _connection.createStatement();
stmt.executeUpdate(sb.toString());
}
} catch (Exception e) {
throw new IOException("Error initializing for write: ", e);
} finally {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
// We wrap pyramid ids in [] so we can use a wider range of characters in
// pyramid IDs.
private String toTableName(String pyramidId) {
StringBuffer sb = new StringBuffer("\"");
sb.append(pyramidId).append("\"");
return sb.toString();
}
protected boolean tableExists(String pyramidId) throws SQLException {
StringBuilder sb = new StringBuilder();
sb.append("SELECT name FROM 'sqlite_master' WHERE type='table' AND name=");
sb.append(toTableName(pyramidId));
Statement stmt = null;
boolean exists = false;
try {
stmt = _connection.createStatement();
stmt.execute(sb.toString());
exists = stmt.getResultSet().next();
} finally {
if (stmt != null) {
stmt.close();
}
}
return exists;
}
@Override
public <T> void writeTiles(String pyramidId,
TileSerializer<T> serializer, Iterable<TileData<T>> data)
throws IOException {
PreparedStatement ps = null;
try {
_connection.setAutoCommit(false);
StringBuilder sb = new StringBuilder();
sb.append("INSERT INTO ");
sb.append(toTableName(pyramidId));
sb.append(" (");
sb.append(COL_ZOOM_LVL);
sb.append(",");
sb.append(COL_TILE_COLUMN);
sb.append(",");
sb.append(COL_TILE_ROW);
sb.append(",");
sb.append(COL_TILE_DATA);
sb.append(") ");
sb.append("VALUES (?,?,?,?)");
ps = _connection.prepareStatement(sb.toString());
int count = 0;
for (TileData<T> tile : data) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
serializer.serialize(tile, baos);
TileIndex index = tile.getDefinition();
ps.setInt(1, index.getLevel());
ps.setInt(2, index.getX());
ps.setInt(3, index.getY());
ps.setBytes(4, baos.toByteArray());
ps.addBatch();
++count;
if (count % BATCH_SIZE == 0) {
ps.executeBatch();
}
}
ps.executeBatch();
_connection.commit();
} catch (Exception e) {
throw new IOException("Error writing tiles.", e);
} finally {
try {
if (ps != null) {
ps.close();
}
_connection.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
protected boolean metaDataExistsFor(String pyramidId) {
String metadata = null;
try {
metadata = readMetaData(pyramidId);
} catch (IOException e) {
metadata = null;
}
return metadata != null;
}
@Override
public void writeMetaData(String pyramidId, String metaData)
throws IOException {
Statement stmt = null;
try {
StringBuilder sb = new StringBuilder();
if (metaDataExistsFor(pyramidId)) {
sb.append("UPDATE ");
sb.append(TABLE_METADATA);
sb.append(" SET ");
sb.append(COL_METADATA);
sb.append(" = '");
sb.append(metaData);
sb.append("' WHERE ");
sb.append(COL_PYRAMID_ID);
sb.append(" = '");
sb.append(toTableName(pyramidId));
sb.append("';");
}
else {
sb.append("INSERT INTO ");
sb.append(TABLE_METADATA);
sb.append(" (");
sb.append(COL_PYRAMID_ID);
sb.append(", ");
sb.append(COL_METADATA);
sb.append(") VALUES('");
sb.append(toTableName(pyramidId));
sb.append("','");
sb.append(metaData);
sb.append("')");
}
stmt = _connection.createStatement();
stmt.execute(sb.toString());
} catch (SQLException e) {
throw new IOException("Error writing metadata.", e);
} finally {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
@Override
public void initializeForRead(String pyramidId, int width, int height, Properties dataDescription) {
// Noop
}
@Override
public <T> List<TileData<T>> readTiles(String pyramidId,
TileSerializer<T> serializer, Iterable<TileIndex> tiles)
throws IOException {
PreparedStatement ps = null;
try {
if (!tableExists(pyramidId)) {
// TODO: Right thing to return when the table doesn't exist?
return null;
}
StringBuilder sb = new StringBuilder();
sb.append("SELECT ");
sb.append(COL_TILE_DATA);
sb.append(" FROM ");
sb.append(toTableName(pyramidId));
sb.append(" WHERE ");
sb.append(COL_ZOOM_LVL);
sb.append(" = ? AND ");
sb.append(COL_TILE_COLUMN);
sb.append(" = ? AND ");
sb.append(COL_TILE_ROW);
sb.append(" = ?");
ps = _connection.prepareStatement(sb.toString());
List<TileData<T>> results = new LinkedList<TileData<T>>();
for (TileIndex tile : tiles) {
ps.setInt(1, tile.getLevel());
ps.setInt(2, tile.getX());
ps.setInt(3, tile.getY());
ResultSet resultSet = ps.executeQuery();
if (!resultSet.next())
continue;
byte[] tileBytes = resultSet.getBytes(COL_TILE_DATA);
TileData<T> data = serializer.deserialize(tile,
new ByteArrayInputStream(tileBytes));
results.add(data);
}
return results;
} catch (Exception e) {
throw new IOException("Error reading tiles.", e);
} finally {
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
throw new IOException(e);
}
}
}
}
@Override
public <T> List<TileData<T>> readTiles (String pyramidId,
TileSerializer<T> serializer,
Iterable<TileIndex> tiles,
JSONObject properties ) throws IOException {
return readTiles( pyramidId, serializer, tiles );
}
@Override
public <T> InputStream getTileStream (String pyramidId,
TileSerializer<T> serializer,
TileIndex tile) throws IOException {
PreparedStatement ps = null;
try {
if (!tableExists(pyramidId)) {
// TODO: Right thing to return when the table doesn't exist?
return null;
}
StringBuilder sb = new StringBuilder();
sb.append("SELECT ");
sb.append(COL_TILE_DATA);
sb.append(" FROM ");
sb.append(toTableName(pyramidId));
sb.append(" WHERE ");
sb.append(COL_ZOOM_LVL);
sb.append(" = ? AND ");
sb.append(COL_TILE_COLUMN);
sb.append(" = ? AND ");
sb.append(COL_TILE_ROW);
sb.append(" = ?");
ps = _connection.prepareStatement(sb.toString());
ps.setInt(1, tile.getLevel());
ps.setInt(2, tile.getX());
ps.setInt(3, tile.getY());
ResultSet resultSet = ps.executeQuery();
if (resultSet.next()) {
byte[] tileBytes = resultSet.getBytes(COL_TILE_DATA);
return new ByteArrayInputStream(tileBytes);
}
} catch (Exception e) {
throw new IOException("Error reading tiles.", e);
} finally {
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
throw new IOException(e);
}
}
}
return null;
}
@Override
public String readMetaData(String pyramidId) throws IOException {
Statement stmt = null;
try {
if (!tableExists(TABLE_METADATA)) {
return null;
}
StringBuilder sb = new StringBuilder();
sb.append("SELECT ");
sb.append(COL_METADATA);
sb.append(" FROM ");
sb.append(TABLE_METADATA);
sb.append(" WHERE ");
sb.append(COL_PYRAMID_ID);
sb.append(" = '");
sb.append(toTableName(pyramidId));
sb.append("'");
stmt = _connection.createStatement();
ResultSet resultSet = stmt.executeQuery(sb
.toString());
if (!resultSet.next())
return null;
return resultSet.getString(TABLE_METADATA);
} catch (SQLException e) {
throw new IOException("Error reading tiles.", e);
} finally {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
@Override
public void removeTiles (String id, Iterable<TileIndex> tiles ) throws IOException {
throw new IOException("removeTiles not currently supported for JDBCPyramidIO");
}
}