/*
* Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
*
* This file is part of Resin(R) Open Source
*
* Each copy or derived work must preserve the copyright notice and this
* notice unmodified.
*
* Resin Open Source is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Resin Open Source 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, or any warranty
* of NON-INFRINGEMENT. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with Resin Open Source; if not, write to the
*
* Free Software Foundation, Inc.
* 59 Temple Place, Suite 330
* Boston, MA 02111-1307 USA
*
* @author Scott Ferguson
*/
package com.caucho.jms.jdbc;
import com.caucho.config.ConfigException;
import com.caucho.jdbc.JdbcMetaData;
import com.caucho.jdbc.OracleMetaData;
import com.caucho.jms.JmsExceptionWrapper;
import com.caucho.jms.message.BytesMessageImpl;
import com.caucho.jms.message.MapMessageImpl;
import com.caucho.jms.message.MessageImpl;
import com.caucho.jms.message.ObjectMessageImpl;
import com.caucho.jms.message.StreamMessageImpl;
import com.caucho.jms.message.TextMessageImpl;
import com.caucho.jms.selector.Selector;
import com.caucho.util.CharBuffer;
import com.caucho.util.L10N;
import com.caucho.vfs.*;
import javax.jms.*;
import javax.sql.DataSource;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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.Enumeration;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Represents a JDBC message.
*/
public class JdbcMessage
{
static final Logger log = Logger.getLogger(JdbcMessage.class.getName());
static final L10N L = new L10N(JdbcMessage.class);
private static final int MESSAGE = 0;
private static final int TEXT = 1;
private static final int BYTES = 2;
private static final int STREAM = 3;
private static final int OBJECT = 4;
private static final int MAP = 5;
private final JdbcManager _jdbcManager;
private DataSource _dataSource;
private String _messageTable;
private String _messageSequence;
private boolean _isOracle;
public JdbcMessage(JdbcManager jdbcManager)
{
_jdbcManager = jdbcManager;
}
/**
* Initializes the JdbcMessage
*/
public void init()
throws ConfigException, SQLException
{
_messageTable = _jdbcManager.getMessageTable();
_dataSource = _jdbcManager.getDataSource();
JdbcMetaData metaData = _jdbcManager.getMetaData();
_isOracle = metaData instanceof OracleMetaData;
String longType = _jdbcManager.getLongType();
String identity = longType + " PRIMARY KEY";
if (metaData.supportsIdentity())
identity = metaData.createIdentitySQL(identity);
else
_messageSequence = _messageTable + "_cseq";
Connection conn = _dataSource.getConnection();
try {
Statement stmt = conn.createStatement();
String sql = "SELECT 1 FROM " + _messageTable + " WHERE 1=0";
try {
ResultSet rs = stmt.executeQuery(sql);
rs.next();
rs.close();
stmt.close();
return;
} catch (SQLException e) {
log.finest(e.toString());
}
String blob = _jdbcManager.getBlob();
log.info(L.l("creating JMS message table {0}", _messageTable));
sql = ("CREATE TABLE " + _messageTable + " (" +
" m_id " + identity + "," +
" queue INTEGER NOT NULL," +
" conn VARCHAR(255)," +
" consumer " + longType + "," +
" delivered INTEGER NOT NULL," +
" msg_type INTEGER NOT NULL," +
" msg_id VARCHAR(64) NOT NULL," +
" priority INTEGER NOT NULL," +
" expire " + longType + " NOT NULL," +
" header " + blob + "," +
" body " + blob +
")");
if (_isOracle) {
String extent = "";
if (_jdbcManager.getTablespace() != null) {
extent = " tablespace " + _jdbcManager.getTablespace();
}
// oracle recommends using retention (over pctversion) for performance
// Oracle will keep deleted lobs for the retention time before
// releasing them (e.g. 900 seconds)
sql += (" LOB(header) STORE AS (cache retention" + extent + ")");
sql += (" LOB(body) STORE AS (cache retention" + extent + ")");
}
stmt.executeUpdate(sql);
if (_messageSequence != null) {
stmt.executeUpdate(metaData.createSequenceSQL(_messageSequence, 1));
}
} finally {
conn.close();
}
}
/**
* Sends the message to the queue.
*/
public long send(Message message, int queue, int priority, long expireTime)
throws SQLException, IOException, JMSException
{
if (log.isLoggable(Level.FINE))
log.fine("jms jdbc queue:" + queue + " send message " + message);
String msgId = message.getJMSMessageID();
TempStream header = new TempStream();
header.openWrite();
WriteStream ws = new WriteStream(header);
writeMessageHeader(ws, message);
ws.close();
TempStream body = null;
int type = MESSAGE;
if (message instanceof TextMessage) {
TextMessage text = (TextMessage) message;
type = TEXT;
if (text.getText() != null) {
body = new TempStream();
body.openWrite();
ws = new WriteStream(body);
ws.setEncoding("UTF-8");
ws.print(text.getText());
ws.close();
}
}
else if (message instanceof BytesMessage) {
BytesMessage bytes = (BytesMessage) message;
type = BYTES;
body = writeBytes(bytes);
}
else if (message instanceof StreamMessage) {
StreamMessage stream = (StreamMessage) message;
type = STREAM;
body = writeStream(stream);
}
else if (message instanceof ObjectMessage) {
ObjectMessage obj = (ObjectMessage) message;
type = OBJECT;
body = writeObject(obj);
}
else if (message instanceof MapMessage) {
MapMessage obj = (MapMessage) message;
type = MAP;
body = writeMap(obj);
}
Connection conn = _dataSource.getConnection();
try {
String sql;
if (_messageSequence != null) {
sql = _jdbcManager.getMetaData().selectSequenceSQL(_messageSequence);
PreparedStatement pstmt = conn.prepareStatement(sql);;
long mId = -1;
ResultSet rs = pstmt.executeQuery();
if (rs.next())
mId = rs.getLong(1);
else
throw new RuntimeException("can't create message");
sql = ("INSERT INTO " + _messageTable +
"(m_id, queue, msg_type, msg_id, priority, expire, delivered, header, body) " +
"VALUES (?,?,?,?,?,?,0,?,?)");
pstmt = conn.prepareStatement(sql);
int i = 1;
pstmt.setLong(i++, mId);
pstmt.setInt(i++, queue);
pstmt.setInt(i++, type);
pstmt.setString(i++, msgId);
pstmt.setInt(i++, priority);
pstmt.setLong(i++, expireTime);
int headerLength = header.getLength();
if (headerLength > 0)
pstmt.setBinaryStream(i++, header.openRead(), headerLength);
else
pstmt.setNull(i++, Types.BINARY);
if (body != null) {
int bodyLength = body.getLength();
pstmt.setBinaryStream(i++, body.openRead(), bodyLength);
}
else
pstmt.setString(i++, "");
pstmt.executeUpdate();
}
else {
sql = ("INSERT INTO " + _messageTable +
"(queue, msg_type, msg_id, priority, expire, delivered, header, body) " +
"VALUES (?,?,?,?,?,0,?,?)");
PreparedStatement pstmt;
pstmt = conn.prepareStatement(sql);
int i = 1;
pstmt.setInt(i++, queue);
pstmt.setInt(i++, type);
pstmt.setString(i++, msgId);
pstmt.setInt(i++, priority);
pstmt.setLong(i++, expireTime);
int headerLength = header.getLength();
pstmt.setBinaryStream(i++, header.openRead(), headerLength);
if (body != null) {
int bodyLength = body.getLength();
pstmt.setBinaryStream(i++, body.openRead(), bodyLength);
}
else
pstmt.setString(i++, "");
pstmt.executeUpdate();
}
return 0;
} finally {
conn.close();
}
}
/**
* Receives a message from the queue.
*/
MessageImpl receive(int queue, int session)
throws SQLException, IOException, JMSException
{
long minId = -1;
Connection conn = _dataSource.getConnection();
try {
String sql = ("SELECT m_id, msg_type, msg_id, delivered, body, header" +
" FROM " + _messageTable +
" WHERE ?<id AND queue=? AND consumer IS NULL" +
" ORDER BY priority DESC, id");
PreparedStatement selectStmt = conn.prepareStatement(sql);
sql = ("UPDATE " + _messageTable + " SET consumer=?, delivered=1 " +
"WHERE m_id=? AND consumer IS NULL");
PreparedStatement updateStmt = conn.prepareStatement(sql);
long id = -1;
while (true) {
id = -1;
selectStmt.setLong(1, minId);
selectStmt.setInt(2, queue);
MessageImpl msg = null;
ResultSet rs = selectStmt.executeQuery();
while (rs.next()) {
id = rs.getLong(1);
minId = id;
msg = readMessage(rs);
}
rs.close();
if (msg == null)
return null;
updateStmt.setInt(1, session);
updateStmt.setLong(2, id);
int updateCount = updateStmt.executeUpdate();
if (updateCount == 1)
return msg;
else if (log.isLoggable(Level.FINE)) {
log.fine("JdbcMessageQueue[" + queue + "] can't update received message " + id + " for session " + session +".");
}
}
} finally {
conn.close();
}
}
/**
* Acknowledges all received messages from the session.
*/
void acknowledge(int session)
throws SQLException
{
Connection conn = _dataSource.getConnection();
try {
String sql = ("DELETE FROM " + _messageTable + " " +
"WHERE consumer=?");
PreparedStatement pstmt;
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, session);
pstmt.executeUpdate();
pstmt.close();
} finally {
conn.close();
}
}
/**
* Reads the message from the result stream.
*/
MessageImpl readMessage(ResultSet rs)
throws SQLException, IOException, JMSException
{
int msgType = rs.getInt(2);
String msgId = rs.getString(3);
boolean redelivered = rs.getInt(4) == 1;
MessageImpl msg;
switch (msgType) {
case TEXT:
{
InputStream is = rs.getBinaryStream(5);
try {
msg = readTextMessage(is);
} finally {
if (is != null)
is.close();
}
break;
}
case BYTES:
{
InputStream is = rs.getBinaryStream(5);
try {
msg = readBytesMessage(is);
} finally {
if (is != null)
is.close();
}
break;
}
case STREAM:
{
InputStream is = rs.getBinaryStream(5);
try {
msg = readStreamMessage(is);
} finally {
if (is != null)
is.close();
}
break;
}
case OBJECT:
{
InputStream is = rs.getBinaryStream(5);
try {
msg = readObjectMessage(is);
} finally {
if (is != null)
is.close();
}
break;
}
case MAP:
{
InputStream is = rs.getBinaryStream(5);
try {
msg = readMapMessage(is);
} finally {
if (is != null)
is.close();
}
break;
}
case MESSAGE:
default:
{
msg = new MessageImpl();
break;
}
}
InputStream is = rs.getBinaryStream(6);
if (is != null) {
try {
readMessageHeader(is, msg);
} finally {
is.close();
}
}
msg.setJMSMessageID(msgId);
msg.setJMSRedelivered(redelivered);
return msg;
}
/**
* Writes the message header for a Resin message.
*/
private void writeMessageHeader(WriteStream ws, Message msg)
throws IOException, JMSException
{
Enumeration names = msg.getPropertyNames();
CharBuffer cb = new CharBuffer();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
writeValue(ws, cb, name);
String value = msg.getStringProperty(name);
writeValue(ws, cb, value);
}
}
/**
* Writes a value to the output stream.
*/
private void writeValue(WriteStream ws, CharBuffer cb, Object value)
throws IOException
{
if (value == null)
ws.write('N');
else {
cb.clear();
cb.append(value);
int length = cb.length();
char []buf = cb.getBuffer();
ws.write('S');
ws.write(length >> 24);
ws.write(length >> 16);
ws.write(length >> 8);
ws.write(length);
for (int i = 0; i < length; i++) {
int ch = buf[i];
ws.write(ch >> 8);
ws.write(ch);
}
}
}
/**
* Writes the bytes message.
*/
private TempStream writeBytes(BytesMessage bytes)
throws IOException, JMSException
{
TempStream body = new TempStream();
body.openWrite();
WriteStream ws = new WriteStream(body);
int data;
//bytes.reset();
TempBuffer tb = TempBuffer.allocate();
byte []buffer = tb.getBuffer();
int len;
while ((len = bytes.readBytes(buffer, buffer.length)) >= 0) {
ws.write(buffer, 0, len);
}
TempBuffer.free(tb);
tb = null;
ws.close();
return body;
}
/**
* Writes the stream message.
*/
private TempStream writeStream(StreamMessage stream)
throws IOException, JMSException
{
TempStream body = new TempStream();
body.openWrite();
WriteStream ws = new WriteStream(body);
ObjectOutputStream out = new ObjectOutputStream(ws);
try {
while (true) {
Object data = stream.readObject();
out.writeObject(data);
}
} catch (MessageEOFException e) {
}
out.close();
ws.close();
return body;
}
/**
* Writes the object message.
*/
private TempStream writeObject(ObjectMessage obj)
throws IOException, JMSException
{
TempStream body = new TempStream();
body.openWrite();
WriteStream ws = new WriteStream(body);
ObjectOutputStream out = new ObjectOutputStream(ws);
out.writeObject(obj.getObject());
out.close();
ws.close();
return body;
}
/**
* Writes the map message.
*/
private TempStream writeMap(MapMessage map)
throws IOException, JMSException
{
TempStream body = new TempStream();
body.openWrite();
WriteStream ws = new WriteStream(body);
ObjectOutputStream out = new ObjectOutputStream(ws);
try {
Enumeration e = map.getMapNames();
while (e.hasMoreElements()) {
String name = (String) e.nextElement();
out.writeUTF(name);
Object data = map.getObject(name);
out.writeObject(data);
}
} catch (MessageEOFException e) {
}
out.close();
ws.close();
return body;
}
/**
* Writes the message header for a Resin message.
*/
private void readMessageHeader(InputStream is, Message msg)
throws IOException, JMSException
{
CharBuffer cb = new CharBuffer();
int type;
while ((type = is.read()) > 0) {
String name = (String) readValue(is, type, cb);
Object value = readValue(is, is.read(), cb);
msg.setObjectProperty(name, value);
}
}
/**
* Writes the message header for a Resin message.
*/
private TextMessageImpl readTextMessage(InputStream is)
throws IOException, JMSException
{
TextMessageImpl text = new TextMessageImpl();
if (is == null)
return text;
ByteToChar byteToChar = ByteToChar.create();
int ch;
byteToChar.setEncoding("UTF-8");
while ((ch = is.read()) >= 0) {
byteToChar.addByte(ch);
}
text.setText(byteToChar.getConvertedString());
return text;
}
/**
* Reads a bytes message.
*/
private BytesMessageImpl readBytesMessage(InputStream is)
throws IOException, JMSException
{
BytesMessageImpl bytes = new BytesMessageImpl();
if (is == null) {
bytes.reset();
return bytes;
}
int data;
while ((data = is.read()) >= 0) {
bytes.writeByte((byte) data);
}
bytes.reset();
return bytes;
}
/**
* Reads a stream message.
*/
private StreamMessageImpl readStreamMessage(InputStream is)
throws IOException, JMSException
{
StreamMessageImpl stream = new StreamMessageImpl();
if (is == null)
return stream;
ObjectInputStream in = new ContextLoaderObjectInputStream(is);
try {
while (true) {
Object obj = in.readObject();
stream.writeObject(obj);
}
} catch (EOFException e) {
} catch (Exception e) {
throw new JmsExceptionWrapper(e);
}
in.close();
stream.reset();
return stream;
}
/**
* Reads a map message.
*/
private MapMessageImpl readMapMessage(InputStream is)
throws IOException, JMSException
{
MapMessageImpl map = new MapMessageImpl();
if (is == null)
return map;
ObjectInputStream in = new ContextLoaderObjectInputStream(is);
try {
while (true) {
String name = in.readUTF();
Object obj = in.readObject();
map.setObject(name, obj);
}
} catch (EOFException e) {
} catch (Exception e) {
throw new JmsExceptionWrapper(e);
}
in.close();
return map;
}
/**
* Reads an object message.
*/
private ObjectMessageImpl readObjectMessage(InputStream is)
throws IOException, JMSException
{
ObjectMessageImpl msg = new ObjectMessageImpl();
if (is == null)
return msg;
ObjectInputStream in = new ContextLoaderObjectInputStream(is);
try {
Object obj = in.readObject();
msg.setObject((java.io.Serializable) obj);
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new JmsExceptionWrapper(e);
}
in.close();
return msg;
}
/**
* Writes a value to the output stream.
*/
private Object readValue(InputStream is, int type, CharBuffer cb)
throws IOException
{
switch (type) {
case 'N':
return null;
case 'S':
{
cb.clear();
int length = readInt(is);
for (int i = 0; i < length; i++) {
char ch = (char) ((is.read() << 8) + is.read());
cb.append(ch);
}
return cb.toString();
}
default:
throw new IOException(L.l("unknown header type"));
}
}
/**
* Reads an integer value.
*/
private int readInt(InputStream is)
throws IOException
{
return ((is.read() << 24) +
(is.read() << 16) +
(is.read() << 8) +
(is.read()));
}
/**
* Removes the first message matching the selector.
*/
private boolean hasMessage(Selector selector)
throws JMSException
{
return false;
}
/**
* Returns a printable view of the queue.
*/
public String toString()
{
return "JdbcMessage[" + _messageTable + "]";
}
}