/*
* 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.addthis.hydra.job.store;
import javax.sql.rowset.serial.SerialBlob;
import java.util.Properties;
import java.nio.charset.StandardCharsets;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import com.addthis.basis.util.Parameter;
import com.ning.compress.lzf.LZFChunk;
import com.ning.compress.lzf.LZFDecoder;
import com.ning.compress.lzf.LZFException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A class for storing spawn configuration data into a mysql database. Reads and
* writes values from a single table.
*/
public class MysqlDataStore extends JdbcDataStore<Blob> {
private static final Logger log = LoggerFactory.getLogger(MysqlDataStore.class);
private static final String driverClass = Parameter.value("sql.datastore.driverclass", "org.drizzle.jdbc.DrizzleDriver");
/* There are known issues with Drizzle and InnoDB tables. Using the MyISAM type is strongly recommended. */
private static final String tableType = Parameter.value("sql.datastore.tabletype", "MyISAM");
private static final String description = "mysql";
public MysqlDataStore(String jdbcUrl, String dbName, String tableName, Properties properties) throws Exception {
super(jdbcUrl, dbName, tableName, properties);
log.info("Mysql Spawn Data Store Initialized");
}
@Override
protected void runSetupDatabaseCommand(final String dbName, final String jdbcUrl, final Properties properties) throws SQLException {
try (Connection connection = DriverManager.getConnection(jdbcUrl, properties)) {
// Create a connection that excludes the database from the jdbc url.
// This is necessary to create the database in the event that it does not exist.
String dbSetupCommand = String.format("CREATE DATABASE IF NOT EXISTS %s", dbName);
connection.prepareStatement(dbSetupCommand).execute();
}
}
/**
* {@inheritDoc}
*/
@Override
protected void runSetupTableCommand() throws SQLException {
try (Connection connection = cpds.getConnection()) {
String tableSetupCommand = String.format("CREATE TABLE IF NOT EXISTS %s ( "
+ "%s INT NOT NULL AUTO_INCREMENT, " + // Auto-incrementing int id
"%s VARCHAR(%d) NOT NULL, %s MEDIUMBLOB, %s VARCHAR(%d), " + // VARCHAR path, BLOB value, VARCHAR child
"PRIMARY KEY (%s), UNIQUE KEY (%s,%s)) " + // Use id as primary key, enforce unique (path, child) combo
"ENGINE=%s", // Use specified table type (MyISAM works best in practice)
tableName, getIdKey(), getPathKey(), getMaxPathLength(), getValueKey(), getChildKey(),
getMaxPathLength(), getIdKey(), getPathKey(), getChildKey(), tableType);
connection.prepareStatement(tableSetupCommand).execute();
}
}
/**
* {@inheritDoc}
*/
@Override
public String getDescription() {
return description;
}
/**
* {@inheritDoc}
*/
@Override
protected String getQueryTemplate() {
//TODO - cache this
return String.format("SELECT %s FROM %s WHERE %s=? AND %s=?", getValueKey(), tableName, getPathKey(), getChildKey());
}
/**
* {@inheritDoc}
*/
@Override
protected PreparedStatement getInsertTemplate(Connection connection, String path, String childId, String value) throws SQLException {
final PreparedStatement preparedStatement = connection.prepareStatement(
String.format("REPLACE INTO %s (%s,%s,%s) VALUES(?,?,?)", tableName, getPathKey(), getValueKey(), getChildKey()));
preparedStatement.setString(1, path);
preparedStatement.setBlob(2, valueToDBType(value));
preparedStatement.setString(3, childId);
return preparedStatement;
}
/**
* {@inheritDoc}
*/
@Override
protected String getDeleteTemplate() {
//TODO - cache this
return String.format("DELETE FROM %s WHERE %s=? AND %s=?", tableName, getPathKey(), getChildKey());
}
/**
* {@inheritDoc}
*/
@Override
protected String getGetChildNamesTemplate() {
//TODO - cache this
return String.format("SELECT DISTINCT %s FROM %s WHERE %s=? AND %s!=?", getChildKey(), tableName, getPathKey(), getChildKey());
}
/**
* {@inheritDoc}
*/
@Override
protected String getGetChildrenTemplate() {
return String.format("SELECT %s,%s FROM %s WHERE %s=? AND %s!=?",
getChildKey(), getValueKey(), tableName, getPathKey(), getChildKey());
}
/**
* {@inheritDoc}
*/
@Override
protected String getDriverClass() {
return driverClass;
}
@Override
protected Class<Blob> getValueType() {
return Blob.class;
}
@Override
protected Blob valueToDBType(String value) throws SQLException {
if (value != null) {
return new SerialBlob(value.getBytes(StandardCharsets.UTF_8));
} else {
return null;
}
}
@Override
protected String dbTypeToValue(Blob dbValue) throws SQLException {
try {
if (dbValue != null) {
byte[] blobBytes = dbValue.getBytes(1L, (int) dbValue.length());
// make best guess whether value was previously compressed with LZF
if ((blobBytes.length > 0) && (blobBytes[0] == LZFChunk.BYTE_Z) && (blobBytes[1] == LZFChunk.BYTE_V)) {
byte[] decodedBytes = LZFDecoder.decode(blobBytes);
return new String(decodedBytes, StandardCharsets.UTF_8);
} else {
return new String(blobBytes, StandardCharsets.UTF_8);
}
} else {
return null;
}
} catch (LZFException ex) {
//Couldn't deal with the data
throw new SQLException(ex);
}
}
}