/*------------------------------------------------------------------------------
Name: XbMeatFactory.java
Project: xmlBlaster.org
Copyright: xmlBlaster.org, see xmlBlaster-LICENSE file
------------------------------------------------------------------------------*/
package org.xmlBlaster.util.queue.jdbc;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.logging.Logger;
import org.xmlBlaster.contrib.I_Info;
import org.xmlBlaster.util.XmlBlasterException;
/**
* @author <a href='mailto:mr@ruff.info'>Marcel Ruff</a>
* @author <a href='mailto:michele@laghi.eu'>Michele Laghi</a>
*/
public abstract class XBFactory extends XBFactoryBase {
private final static Logger log = Logger.getLogger(XBFactory.class.getName());
protected String insertSt;
protected String getSt;
protected String getAllSt;
protected String getCompleteSt;
protected String deleteAllSt;
protected String deleteCompleteSt;
protected String deleteTransientsSt;
protected String getFirstEntriesSt;
protected String getNumOfAllSt;
protected String deleteSt;
protected String createSt;
protected String countSt = "select count(*) from ${table} where xbstoreid=?";
protected String dropSt = "drop table ${table}";
protected String prefix = "queue.jdbc";
protected String table;
private String tableNameDefault;
protected String inList;
protected String base;
/**
*
* <pre>
* xbmeatid NUMBER(20) primary key,
* xbdurable char default 'F' not null,
* xbrefcount NUMBER(10),
* xbbytesize NUMBER(10),
* xbdatatype varchar(32) default '' not null,
* xbflag1 varchar(32) default '',
* xbmsgqos clob default '',
* xbmsgcont blob default '',
* xbmsgkey clob default ''
* </pre>
*
* @return
*/
public XBFactory(String prefix, String name) {
base = prefix;
tableNameDefault = name;
if (prefix != null)
this.prefix = prefix + "." + name;
else
prefix += "." + name;
}
public final I_Info init(I_Info origInfo) throws XmlBlasterException {
table = origInfo.get(prefix + ".table." + tableNameDefault, tableNameDefault);
I_Info info = super.init(origInfo);
prepareDefaultStatements();
info.put("table", table);
String tmp = info.get(base + ".table." + XBStoreFactory.getName(), XBStoreFactory.getName());
info.put(XBStoreFactory.getName(), tmp);
tmp = info.get(base + ".table." + XBMeatFactory.getName(), XBMeatFactory.getName());
info.put(XBMeatFactory.getName(), tmp);
doInit(info);
insertSt = info.get(prefix + ".insertStatement", insertSt);
deleteAllSt = info.get(prefix + ".deleteAllStatement", deleteAllSt);
deleteSt = info.get(prefix + ".deleteStatement", deleteSt);
deleteTransientsSt = info.get(prefix + ".deleteTransientsSttatement", deleteTransientsSt);
getSt = info.get(prefix + ".getStatement", getSt);
getAllSt = info.get(prefix + ".getAllStatement", getAllSt);
createSt = info.get(prefix + ".createStatement", createSt);
dropSt = info.get(prefix + ".dropStatement", dropSt);
countSt = info.get(prefix + ".countStatement", countSt);
getNumOfAllSt = info.get(prefix + ".getNumOfAllStatement", getNumOfAllSt);
return info;
}
public int delete(long storeId, long id, Connection conn, int timeout) throws SQLException {
if (conn == null)
return 0;
PreparedStatement preStatement = conn.prepareStatement(deleteSt);
if (timeout > 0)
preStatement.setQueryTimeout(timeout);
try {
preStatement.setLong(1, storeId);
if (id != 0)
preStatement.setLong(2, id);
return preStatement.executeUpdate();
}
finally {
if (preStatement != null)
preStatement.close();
}
}
public int deleteTransients(long storeId, Connection conn, int timeout) throws SQLException {
if (conn == null)
return 0;
PreparedStatement preStatement = conn.prepareStatement(deleteTransientsSt);
preStatement.setLong(1, storeId);
if (timeout > 0)
preStatement.setQueryTimeout(timeout);
try {
return preStatement.executeUpdate();
}
finally {
if (preStatement != null)
preStatement.close();
}
}
protected abstract XBEntry rsToEntry(XBStore store, ResultSet rs) throws SQLException, IOException;
/**
* Checks if the table already exists, the check is done against
* the meta data.
*
* @param conn
* @return
* @throws SQLException
*/
private final boolean exists(Connection conn) throws SQLException {
final String[] types = { "TABLE" };
ResultSet rs = null;
try {
// specifying a table does not seem to work with hsqldb
rs = conn.getMetaData().getTables(null, null, table, types);
if (rs.next())
return true;
rs.close();
rs = conn.getMetaData().getTables(null, null, null, types);
while (rs.next()) { // retrieve the result set ...
String tmp = rs.getString(3).toUpperCase();
if (tmp.equalsIgnoreCase(table))
return true;
}
return false;
}
finally {
if (rs != null)
rs.close();
}
}
/**
* Returns true if the table has been created, false otherwise. It returns false for example
* if the table existed already, in which case it is not newly created.
* @param conn
* @return
* @throws SQLException
*/
public boolean create(Connection conn) throws SQLException {
if (exists(conn))
return false;
Statement st = null;
try {
st = conn.createStatement();
StringTokenizer tokenizer = new StringTokenizer(createSt, ";");
while (tokenizer.hasMoreTokens()) {
String sql = tokenizer.nextToken();
if (sql.trim().length() > 0) {
log.info("Executing create statement >" + sql + "<");
st.executeUpdate(sql);
}
}
}
finally {
if (st != null)
st.close();
}
return true;
}
/**
* Returns true if it could delete the table, false otherwise. It would return false
* if the table did not exist.
* @param conn
* @return
* @throws SQLException
*/
public final boolean drop(Connection conn) throws SQLException {
if (!exists(conn))
return false;
Statement st = null;
try {
st = conn.createStatement();
st.executeUpdate(dropSt);
return true;
}
finally {
if (st != null)
st.close();
}
}
protected final static byte[] readStream(InputStream inStream) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int nmax = 262144; // max 200 kb at a time
int avail = 0;
byte[] buf = new byte[nmax];
while ((avail = inStream.read(buf, 0, nmax)) > -1) {
baos.write(buf, 0, avail);
}
return baos.toByteArray();
}
private final static boolean useString = true;
private final static boolean useString2 = false;
private final static boolean useBinary = false;
// private final static boolean useCharacter = false;
/**
* String/VARCHAR/CLOB helper to avoid NULL and to take care on Umlauts/UTF-8
*
* @param preStatement
* @param index
* @param value
* @throws SQLException
*/
public final static String getDbCol(ResultSet rs, int index) throws SQLException, IOException {
if (useString) {
return rs.getString(index);
} else if (useString2) {
byte[] bytes = rs.getBytes(index);
if (bytes != null) {
try {
return new String(rs.getBytes(index), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return rs.getString(index);
}
}
else
return "";
} else if (useBinary) {
InputStream stream = rs.getAsciiStream(index);
if (stream != null) {
try {
return new String(readStream(stream), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return new String(readStream(stream));
}
} else {
return "";
}
} else {
throw new IllegalArgumentException("NOT IMPLEMENTED");
}
}
/**
* String/VARCHAR/CLOB helper to avoid NULL and to take care on Umlauts/UTF-8
*
* @param preStatement
* @param index
* @param value
* @throws SQLException
* @throws UnsupportedEncodingException
*/
public final static void fillDbCol(PreparedStatement preStatement, int index, String value) throws SQLException,
UnsupportedEncodingException {
if (value == null) {
preStatement.setNull(index, Types.VARCHAR);
// preStatement.setNull(index QOS KEY, Types.CLOB);
return;
}
boolean isOracleNullFix = true;
if (isOracleNullFix) {
if (value == null || value.length() == 0)
value = " ";
}
if (useString) {
preStatement.setString(index, value);
} else if (useString2) {
byte[] bytes = value.getBytes("UTF-8");
preStatement.setBytes(index, bytes);
} else if (useBinary) {
byte[] bytes = value.getBytes("UTF-8");
InputStream stream = new ByteArrayInputStream(bytes);
preStatement.setAsciiStream(index, stream, bytes.length);
} else {
// preStatement.getCharacterStream(index, )
}
}
/**
* This method must be implemented in all underlying extentions to this class. It returns (for the different
* database vendors a default for the creation of the table.
* @return
*/
protected void prepareDefaultStatements() {
//if (getDbVendor().equals(POSTGRES)) {
}
/**
* The prefix is the initial part of the SQL update/query. Note that this
* method can be used both for SELECT statements as for updates such as
* DELETE or UPDATE.
* An example of prefix:
* "delete from tableName where dataId in(";
*/
protected final List<String> whereInStatement(String reqPrefix, XBEntry[] entries, int maxStatementLength,
int maxNumStatements) {
final String reqPostfix = ")";
boolean isFirst = true;
int initialLength = reqPrefix.length() + reqPostfix.length() + 2;
StringBuffer buf = new StringBuffer();
int length = initialLength;
int currentLength = 0;
List<String> ret = new ArrayList<String>();
int count = 0;
for (int i=0; i<entries.length; i++) {
String req = null;
String entryId = Long.toString(entries[i].getId());
currentLength = entryId.length();
length += currentLength;
if ((length > maxStatementLength) || (i == (entries.length-1)) || count >= maxNumStatements) { // then make the update
if (i == (entries.length-1)) {
if (!isFirst) buf.append(",");
count++;
buf.append(entryId);
}
req = reqPrefix + buf.toString() + reqPostfix;
if (count > 0)
ret.add(req);
length = initialLength + currentLength;
buf = new StringBuffer();
count = 0;
isFirst = true;
}
else
count++;
if (!isFirst) {
buf.append(",");
length++;
}
else
isFirst = false;
count++;
buf.append(entryId);
}
return ret;
}
/**
* Deletes the specified entries. Since all entry may not fit in one single operation,
* they are splitted over different operations.
* If you specified commitInBetween or the auto-commit flag is set to true,
* It always returns the number of deleted entries. If a batch could not be completely deleted,
* it returns the number of operations previously deleted.
*
* @param store the store to use.
* @param the connection to be used.
* @param ids the array containing all ids to delete.
* @return the number of entries successfully processed. These are the first. If an
* error occurs it stops.
*
*/
public long deleteList(XBStore store, Connection conn, XBEntry[] entries, int maxStLength, int maxNumSt, boolean commitInBetween, int timeout) throws SQLException {
String reqPrefix = deleteCompleteSt + " where xbstoreid=" + store.getId() + " " + inList;
List<String> reqList = whereInStatement(reqPrefix, entries, maxStLength, maxNumSt);
commitInBetween = commitInBetween && !conn.getAutoCommit();
long sum = 0;
try {
for (int i=0; i < reqList.size(); i++) {
String req = (String)reqList.get(i);
Statement st = null;
try {
st = conn.createStatement();
if (timeout > 0)
st.setQueryTimeout(timeout);
int num = st.executeUpdate(req);
if (commitInBetween)
conn.commit();
sum += num;
}
finally {
if (st != null)
st.close();
}
}
}
catch (SQLException ex) {
if (!commitInBetween)
sum = 0;
else
conn.rollback();
}
return sum;
}
/**
* Deletes the specified entries. Since all entry may not fit in one single operation,
* they are splitted over different operations.
* If you specified commitInBetween or the auto-commit flag is set to true,
* It always returns the number of deleted entries. If a batch could not be completely deleted,
* it returns the number of operations previously deleted.
*
* @param store the store to use.
* @param the connection to be used.
* @param ids the array containing all ids to delete.
* @return the number of entries successfully processed. These are the first. If an
* error occurs it stops.
*
*/
public long count(XBStore store, Connection conn, int timeout) throws SQLException {
PreparedStatement st = null;
try {
st = conn.prepareStatement(countSt);
if (timeout > 0)
st.setQueryTimeout(timeout);
st.setLong(1, store.getId());
ResultSet rs = st.executeQuery();
if (rs.next())
return rs.getLong(1);
return 0L;
}
finally {
if (st != null)
st.close();
}
}
/**
* Gets the specified entries. Since all entry may not fit in one single operation,
* they are splitted over different operations.
* If you specified commitInBetween or the auto-commit flag is set to true,
* It always returns the number of deleted entries. If a batch could not be completely deleted,
* it returns the number of operations previously deleted.
*
* @param store the store to use.
* @param the connection to be used.
* @param ids the array containing all ids to delete.
* @return the number of entries successfully processed. These are the first. If an
* error occurs it stops.
*
*/
public List<XBEntry> getList(XBStore store, Connection conn, XBEntry[] entries, int maxStLength, int maxNumSt,
int timeout) throws SQLException, IOException {
String reqPrefix = getCompleteSt + " where xbstoreid=" + store.getId() + " " + inList;
List<XBEntry> ret = new ArrayList<XBEntry>();
List<String> reqList = whereInStatement(reqPrefix, entries, maxStLength, maxNumSt);
try {
for (int i=0; i < reqList.size(); i++) {
String req = (String)reqList.get(i);
Statement st = null;
try {
st = conn.createStatement();
st.setQueryTimeout(timeout);
ResultSet rs = st.executeQuery(req);
while (rs.next()) {
XBEntry entry = rsToEntry(store, rs);
ret.add(entry);
}
}
finally {
if (st != null)
st.close();
}
}
}
catch (SQLException ex) {
}
return ret;
}
protected abstract long getByteSize(ResultSet rs, int offset) throws SQLException;
/**
* Gets the real number of entries.
* That is it really makes a call to the DB to find out
* how big the size is.
* @return never null
*/
public final EntryCount getNumOfAll(XBStore store, Connection conn)
throws SQLException {
PreparedStatement st = null;
try {
st = conn.prepareStatement(getNumOfAllSt);
st.setLong(1, store.getId());
ResultSet rs = st.executeQuery();
EntryCount entryCount = new EntryCount();
if (rs.next()) {
long transientOfEntries = 0;
long transientOfBytes = 0;
boolean persistent = isTrue(rs.getString(1));
if (persistent) {
entryCount.numOfPersistentEntries = rs.getLong(2);
entryCount.numOfPersistentBytes = rs.getLong(3);
}
else {
transientOfEntries = rs.getLong(2);
transientOfBytes = rs.getLong(3);
}
if (rs.next()) {
persistent = isTrue(rs.getString(1));
if (persistent) {
entryCount.numOfPersistentEntries = rs.getLong(2);
entryCount.numOfPersistentBytes = rs.getLong(3);
}
else {
transientOfEntries = rs.getLong(2);
transientOfBytes = rs.getLong(3);
}
}
entryCount.numOfEntries = transientOfEntries + entryCount.numOfPersistentEntries;
entryCount.numOfBytes = transientOfBytes + entryCount.numOfPersistentBytes;
}
return entryCount;
}
finally {
if (st != null)
st.close();
}
}
protected final static boolean isTrue(String asTxt) {
return "T".equalsIgnoreCase(asTxt);
}
protected final boolean checkSameStore(XBStore store, XBEntry entry) {
if (store == null)
return false;
if (entry == null)
return false;
boolean ret = entry.getStoreId() == store.getId();
if (!ret)
log.severe("Meat and Store are inconsistent " + entry.toXml("") + " store " + store.getId());
return ret;
}
}