/*
* Tigase Jabber/XMPP Server
* Copyright (C) 2004-2012 "Artur Hefczyc" <artur.hefczyc@tigase.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. Look for COPYING file in the top folder.
* If not, see http://www.gnu.org/licenses/.
*
* $Rev$
* Last modified by $Author$
* $Date$
*/
package tigase.server.ssender;
import java.io.IOException;
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.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <code>JDBCTask</code> implements tasks for cyclic retrieving stanzas from
* database and sending them to the StanzaHandler object.
* <p>
* Database table format:
* <ul>
* <li><b>id</b> - numerical unique record indetifier.</li>
* <li><b>stanza</b> - text field containing valid XML data with XMPP stanza to
* send.</li>
* </ul>
* Any record in this table is treated the same way - Tigase assmes it contains
* valid XML data with XMPP stanza to send. No other data are allowed in this
* table. All stanzas must be complete including correct <em>"from"</em>
* and <em>"to"</em> attriutes.
* </p>
* <p>
* By default it looks for stanzas in <code>xmpp_stanza</code> table but you can
* specify different table name in connection string. Sample connection string:
* <pre>jdbc:mysql://localhost/tigasedb?user=tigase&password=pass&table=xmpp_stanza</pre>
* </p>
* <p>
* Created: Fri Apr 20 12:10:55 2007
* </p>
* @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a>
* @version $Rev$
*/
public class JDBCTask extends SenderTask {
/**
* Variable <code>log</code> is a class logger.
*/
private static final Logger log =
Logger.getLogger("tigase.server.ssender.JDBCTask");
/**
* <code>handler</code> is a reference to object processing stanza
* read from database.
*/
private StanzaHandler handler = null;
/**
* <code>db_conn</code> field stores database connection string.
*/
private String db_conn = null;
/**
* <code>conn</code> variable keeps connection to database.
*/
private Connection conn = null;
/**
* <code>get_all_stanzas</code> is a prepared statement for retrieving all
* stanzas from database:
* <pre>select id, stanza from <tableName></pre>
*/
private PreparedStatement get_all_stanzas = null;
/**
* <code>remove_stanza</code> is a prepared statement for query removing
* processed stanzas from database:
* <pre>delete from <tableName> where id = ?</pre>
*/
private PreparedStatement remove_stanza = null;
/**
* <code>conn_valid_st</code> is a prepared statement keeping query used
* to validate connection to database:
* <pre>select 1</pre>
*/
private PreparedStatement conn_valid_st = null;
/**
* <code>tableName</code> keeps a table name where are stanza packets waiting
* for sending. Default is <code>xmpp_stanza</code>
*/
private String tableName = "xmpp_stanza";
/**
* <code>lastConnectionValidated</code> variable keeps time where the
* connection was validated for the last time.
*/
private long lastConnectionValidated = 0;
/**
* <code>connectionValidateInterval</code> is kind of constant keeping minimum
* interval for validating connection to database.
*/
private long connectionValidateInterval = 1000*60;
/**
* <code>release</code> method releases some SQL query variables.
*
* @param rs a <code>ResultSet</code> value
*/
private void release(ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException sqlEx) { }
}
}
/**
* <code>checkConnection</code> method checks whether connection to database
* is still valid, if not it simply reconnect and reinitializes database
* backend.
*
* @return a <code>boolean</code> value
* @exception SQLException if an error occurs
*/
private boolean checkConnection() throws SQLException {
try {
long tmp = System.currentTimeMillis();
if ((tmp - lastConnectionValidated) >= connectionValidateInterval) {
conn_valid_st.executeQuery();
lastConnectionValidated = tmp;
} // end of if ()
} catch (Exception e) {
initRepo();
} // end of try-catch
return true;
}
/**
* <code>findTableName</code> method parses database connection string to find
* table name where stanza packets are waiting for sending.
*
* @param db_str a <code>String</code> value
*/
private void findTableName(String db_str) {
String[] params = db_str.split("&");
for (String par: params) {
if (par.startsWith("table=")) {
tableName = par.substring("table=".length(), par.length());
break;
}
}
}
/**
* <code>initRepo</code> method initializes database backend - connects to
* database, creates prepared statements and sets basic variables.
*
* @exception SQLException if an error occurs
*/
private void initRepo() throws SQLException {
conn = DriverManager.getConnection(db_conn);
conn.setAutoCommit(true);
String query = "select 1;";
conn_valid_st = conn.prepareStatement(query);
query = "select id, stanza from " + tableName;
get_all_stanzas = conn.prepareStatement(query);
query = "delete from " + tableName + " where id = ?";
remove_stanza = conn.prepareStatement(query);
}
/**
* <code>init</code> method is a task specific initialization rountine.
*
* @param handler a <code>StanzaHandler</code> value is a reference to object
* which handles all stanza retrieved from data source. The handler is
* responsible for delivering stanza to destination address.
* @param initString a <code>String</code> value is an initialization string
* for this task. For example database tasks would expect database connection
* string here, filesystem task would expect directory here.
* @exception IOException if an error occurs during task or data storage
* initialization.
*/
public void init(StanzaHandler handler, String initString) throws IOException {
this.handler = handler;
db_conn = initString;
findTableName(db_conn);
try {
initRepo();
} catch (SQLException e) {
throw new IOException("Problem initializing SenderTask.", e);
}
}
/**
* <code>getInitString</code> method returns initialization string passed
* to it in <code>init()</code> method.
*
* @return a <code>String</code> value of initialization string.
*/
public String getInitString() {
return db_conn;
}
public boolean cancel() {
boolean result = super.cancel();
try {
conn_valid_st.close();
get_all_stanzas.close();
remove_stanza.close();
conn.close();
} catch (Exception e) {
// Ignore.
}
return result;
}
/**
* <code>run</code> method is where all task work is done.
*/
public void run() {
ResultSet rs = null;
try {
checkConnection();
rs = get_all_stanzas.executeQuery();
// Place to store all data ids to remove them later
Set<Long> ids_to_delete = new HashSet<Long>();
while (rs.next()) {
// Collect all data ids to remove them later
// it can be done simultanously as most JDBC drivers
// don't support concurrent query execution
// I would need to establish another connection to database
ids_to_delete.add(rs.getLong("id"));
// Handle stanza to the StanzaSender....
String stanza = rs.getString("stanza");
handler.handleStanza(stanza);
if (log.isLoggable(Level.FINEST)) {
log.finest("Sent stanza found in database: " + stanza);
}
}
// Remove all processed stanzas
for (long id: ids_to_delete) {
remove_stanza.setLong(1, id);
remove_stanza.executeUpdate();
}
} catch (SQLException e) {
// Let's ignore it for now.
log.log(Level.WARNING, "Error retrieving stanzas from database: ", e);
// It should probably do kind of auto-stop???
// if so just uncomment below line:
//this.cancel();
} finally {
release(rs);
rs = null;
}
}
}