/FileExtraction/Java_unseen/dlitz_resin/modules/resin/src/com/caucho/jms/jdbc/JdbcMessage.java
/*
 * 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 + "]";
  }
}

