/*
* JBoss, Home of Professional Open Source
* Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @authors tag. All rights reserved.
*/
package org.searchisko.persistence.service;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.naming.NamingException;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import org.searchisko.api.ContentObjectFields;
import org.searchisko.api.util.CdiHelper;
import org.searchisko.api.util.SearchUtils;
/**
* JDBC based implementation of {@link ContentPersistenceService}. We use raw JDBC here because we dynamically create
* and use tables for distinct sys_content_types to handle big numbers of documents.<br>
* It uses select from SQL standard <code>information_schema.tables</code> view to check table existence, which may be
* incompatible with some DB engines who do not follow SQL exactly (like Oracle)! It also uses <code>LONGTEXT</code>
* data type for one column, which may be incompatible with some DB engines also.<br>
* It's session bean to work with transactions.
*
* @author Vlastimil Elias (velias at redhat dot com)
* @author Jason Porter (jporter@redhat.com)
* @author Lukas Vlcek
*/
@Named
@Stateless
@LocalBean
public class JdbcContentPersistenceService implements ContentPersistenceService {
@Inject
protected Logger log;
@Inject
protected EntityManager em;
protected DataSource searchiskoDs;
@PostConstruct
public void init() throws NamingException {
searchiskoDs = CdiHelper.getDefaultDataSource(em);
}
@Override
public Map<String, Object> get(String id, String sysContentType) {
String tableName = getTableName(sysContentType);
if (!checkTableExists(tableName))
return null;
String sqlString = String.format("select json_data from %s where id = ?", tableName);
String jsonData = executeStringReturningSql(sqlString, id);
if (SearchUtils.trimToNull(jsonData) != null) {
try {
return SearchUtils.convertToJsonMap(jsonData);
} catch (Exception e) {
log.warning(String.format("Persisted JSON data are not valid for sys_content_type '%s' and id '%s': %s",
sysContentType, id, e.getMessage()));
}
}
return null;
}
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void store(String id, String sysContentType, Map<String, Object> content) {
String tableName = getTableName(sysContentType);
ensureTableExists(tableName);
Date updated = null;
if (content != null) {
Object o = content.get(ContentObjectFields.SYS_UPDATED);
if (o instanceof Date) {
updated = (Date) o;
} else if (o instanceof String) {
try {
updated = SearchUtils.dateFromISOString((String) o, true);
} catch (Exception e) {
// ignore exception here
}
}
}
if (updated == null)
updated = new Date();
String jsonString;
try {
jsonString = SearchUtils.convertJsonMapToString(content);
} catch (IOException e) {
throw new RuntimeException(e);
}
try (final Connection conn = searchiskoDs.getConnection()) {
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "Start Store. "
+ " Auto commit: " + conn.getAutoCommit()
+ " isolation: " + conn.getTransactionIsolation()
+ " Db name: " + conn.getMetaData().getDatabaseProductName());
}
if (SearchUtils.isMysqlDialect(conn.getMetaData().getDatabaseProductName())) {
log.log(Level.FINE, "Store data via insert on duplicate key update technique");
executeNonReturningSql(conn,
String.format("insert into %s (id, json_data, sys_content_type, updated) values (?, ?, ?, ?) ON DUPLICATE KEY UPDATE json_data=?", tableName),
id, jsonString, sysContentType, updated, jsonString);
} else {
try {
log.log(Level.FINE, "Try insert data");
executeNonReturningSql(conn,
String.format("insert into %s (id, json_data, sys_content_type, updated) values (?, ?, ?, ?)", tableName),
id, jsonString, sysContentType, updated);
} catch (SQLException e) {
// insert failed, so record is in DB already, so we try to upgrade it
log.log(Level.FINE, "Insert failed. Try update row");
executeNonReturningSql(conn,
String.format("update %s set json_data=?, sys_content_type=?, updated=? where id=?", tableName),
jsonString, sysContentType, updated, id);
}
}
log.log(Level.FINEST, "Store completed");
} catch (SQLException e) {
log.severe(String.format("Error while storing content of type '" + sysContentType + "' with id '" + id
+ "' in the DB -- %s", e.getMessage()));
throw new RuntimeException(e);
}
}
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void delete(String id, String sysContentType) {
String tableName = getTableName(sysContentType);
if (!checkTableExists(tableName))
return;
executeNonReturningSql(String.format("delete from %s where id = ?", tableName), id);
}
/**
* Get table name for given sys_content_type
*
* @param sysContentType to get table name for
* @return name of table.
*/
protected String getTableName(String sysContentType) {
return "data_" + sysContentType;
}
/**
* We need to ensure that keys are always treated as UPPERCASE values.
* Note: this class does not correctly implement whole Map API, right
* now only used methods are reimplemented to use UPPERCASE values.
*/
static class ConcurrentUpperCaseHashMap extends ConcurrentHashMap<String, Boolean> {
public ConcurrentUpperCaseHashMap(int size) {
super(size);
}
public Boolean putIfAbsent(String key, Boolean value) {
return super.putIfAbsent(key.toUpperCase(Locale.US), value);
}
public Boolean put(String key, Boolean value) {
return super.put(key.toUpperCase(Locale.US), value);
}
public boolean containsKey(Object key) {
return super.containsKey(((String) key).toUpperCase(Locale.US));
}
public Boolean get(Object key) {
return super.get(((String)key).toUpperCase(Locale.US));
}
}
protected static final ConcurrentUpperCaseHashMap TABLES_EXISTS = new ConcurrentUpperCaseHashMap(10);
/**
* Check if table exists in DB for given table name.
*
* @param tableName to check
* @return true if table exists, false if not
*/
protected boolean checkTableExists(String tableName) {
if (TABLES_EXISTS.isEmpty()) {
cacheAllTableNames();
}
boolean exists = TABLES_EXISTS.containsKey(tableName);
// Refresh table names map if table now found
// Double check is important in case of cluster deployment - should happen rarely.
if (!exists) {
cacheAllTableNames();
exists = TABLES_EXISTS.containsKey(tableName);
}
return exists;
}
private void cacheAllTableNames() {
for (String table : getAllTableNames()) {
TABLES_EXISTS.putIfAbsent(table, Boolean.TRUE);
}
}
/**
* Get list of all tables (excluding 'INFORMATION_SCHEMA', 'PERFORMANCE_SCHEMA' or 'MYSQL' table if it exists).
*
* @return list of all tables
*/
public List<String> getAllTableNames() {
String sql = "select table_name from INFORMATION_SCHEMA.tables " +
"where upper(table_schema) not in ('INFORMATION_SCHEMA', 'PERFORMANCE_SCHEMA', 'MYSQL')";
return executeListReturningSql(sql);
}
/**
* Check if table exists in DB for given table name and create it if not.
*
* @param tableName to check/create
*/
protected void ensureTableExists(String tableName) {
synchronized (TABLE_STRUCTURE_DDL) {
if (!checkTableExists(tableName)) {
executeNonReturningSql(String.format("create table %s%s", tableName, TABLE_STRUCTURE_DDL));
TABLES_EXISTS.put(tableName, Boolean.TRUE);
}
}
}
private static final String TABLE_STRUCTURE_DDL = " ( id varchar(200) not null primary key, json_data longtext, sys_content_type varchar(100) not null, updated timestamp )";
protected void executeNonReturningSql(final String sql, final Object... params) {
try (final Connection conn = searchiskoDs.getConnection()) {
executeNonReturningSql(conn, sql, params);
} catch (SQLException e) {
log.severe(String.format("Error executing SQL statement -- %s -- Error -- %s", sql, e.getMessage()));
throw new RuntimeException(e);
}
}
protected void executeNonReturningSql(final Connection conn, final String sql, final Object... params)
throws SQLException {
try (final PreparedStatement statement = conn.prepareStatement(sql)) {
setParams(statement, params);
statement.execute();
}
}
protected List<String> executeListReturningSql(final String sql, final Object... params) {
final List<String> returnList = new ArrayList<>(10);
try (Connection conn = searchiskoDs.getConnection(); PreparedStatement statement = conn.prepareStatement(sql)) {
setParams(statement, params);
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
returnList.add(rs.getString(1));
}
}
return returnList;
} catch (SQLException e) {
log.severe(String.format("Error executing statement -- %s -- Error -- %s", sql, e.getMessage()));
throw new RuntimeException(e);
}
}
protected String executeStringReturningSql(final String sql, final Object... params) {
try (Connection conn = searchiskoDs.getConnection(); PreparedStatement statement = conn.prepareStatement(sql)) {
setParams(statement, params);
try (final ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
return rs.getString(1);
}
}
} catch (SQLException e) {
log.severe(String.format("Error executing statement -- %s -- Error -- %s", sql, e.getMessage()));
throw new RuntimeException(e);
}
return null;
}
protected int executeIntegerReturningSql(final String sql, final Object... params) {
try (Connection conn = searchiskoDs.getConnection(); PreparedStatement statement = conn.prepareStatement(sql)) {
setParams(statement, params);
try (final ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
return rs.getInt(1);
}
}
} catch (SQLException e) {
log.severe(String.format("Error executing statement -- %s -- Error -- %s", sql, e.getMessage()));
throw new RuntimeException(e);
}
return 0;
}
private void setParams(PreparedStatement statement, Object... params) throws SQLException {
if (params == null || params.length == 0)
return;
int i = 1;
for (Object param : params) {
if (param instanceof String) {
statement.setString(i, (String) param);
} else if (param instanceof Date) {
statement.setTimestamp(i, new Timestamp(((Date) param).getTime()));
} else if (param instanceof Integer) {
statement.setInt(i, (Integer) param);
} else if (param instanceof Long) {
statement.setLong(i, (Long) param);
}
i++;
}
}
protected int LIST_PAGE_SIZE = 1000;
protected static class ListRequestImpl implements ListRequest {
List<ContentTuple<String, Map<String, Object>>> content;
String sysContentType;
int beginIndex = 0;
protected ListRequestImpl(String sysContentType, int beginIndex,
List<ContentTuple<String, Map<String, Object>>> content) {
super();
this.sysContentType = sysContentType;
this.beginIndex = beginIndex;
this.content = content;
}
@Override
public boolean hasContent() {
return content != null && !content.isEmpty();
}
@Override
public List<ContentTuple<String, Map<String, Object>>> content() {
return content;
}
}
@Override
public ListRequest listRequestInit(String sysContentType) {
return listRequestImpl(sysContentType, 0);
}
@Override
public ListRequest listRequestNext(ListRequest previous) {
ListRequestImpl lr = (ListRequestImpl) previous;
return listRequestImpl(lr.sysContentType, lr.beginIndex + LIST_PAGE_SIZE);
}
protected ListRequest listRequestImpl(String sysContentType, int beginIndex) {
List<ContentTuple<String, Map<String, Object>>> content = new ArrayList<>(10);
String tableName = getTableName(sysContentType);
if (checkTableExists(tableName)) {
final String sql = String.format("select json_data, id from %s order by id limit %d offset %d", tableName,
LIST_PAGE_SIZE, beginIndex);
try (Connection conn = searchiskoDs.getConnection(); PreparedStatement statement = conn.prepareStatement(sql)) {
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
String id = rs.getString(2);
try {
content.add(new ContentTuple<>(id, SearchUtils.convertToJsonMap(rs.getString(1))));
} catch (IOException e) {
log.severe("Could not convert content to JSON object for contentType='" + sysContentType + "' and id='"
+ id + "' due: " + e.getMessage());
}
}
}
} catch (SQLException e) {
log.severe(String.format("Error executing statement '%s' due error %s", sql, e.getMessage()));
throw new RuntimeException(e);
}
}
return new ListRequestImpl(sysContentType, beginIndex, content);
}
public DataSource getDataSource() {
return searchiskoDs;
}
@Override
public int countRecords(String sysContentType) {
return rowCount(getTableName(sysContentType));
}
/**
* Return count of records in the table.
* Can throw unchecked exception (special system table names ... etc).
*
* @param tableName
* @return count of records in the table
*/
public int rowCount(String tableName) {
if (!checkTableExists(tableName))
return 0;
String sqlString = String.format("select count(*) from %s", tableName);
return executeIntegerReturningSql(sqlString);
}
}