/*
 * Copyright (c) 1998-2006 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 SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.jms.message;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Collections;
import java.util.Enumeration;

import java.util.logging.Logger;
import java.util.logging.Level;

import java.lang.ref.SoftReference;

import javax.jms.Message;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageFormatException;
import javax.jms.MessageNotReadableException;
import javax.jms.MessageNotWriteableException;

import com.caucho.util.L10N;
import com.caucho.util.NullEnumeration;

import com.caucho.log.Log;

import com.caucho.jms.session.SessionImpl;

/**
 * A basic message.
 */
public class MessageImpl implements Message  {
  static final Logger log = Log.open(MessageImpl.class);
  static final L10N L = new L10N(MessageImpl.class);

  private static final HashSet<String> _reserved;

  private volatile SoftReference<SessionImpl> _sessionRef;
  
  private String _messageId;
  private String _correlationId;
  
  private long _timestamp;
  private long _expiration;
  
  private Destination _destination;
  private Destination _replyTo;

  private int _deliveryMode;
  private boolean _isRedelivered;

  private String _messageType;
  private int _priority = 4;

  private HashMap<String,Object> _properties;
  
  private boolean _isHeaderWriteable = true;
  private boolean _isBodyWriteable = true;

  public MessageImpl()
  {
  }

  /**
   * Sets the session.
   */
  public void setSession(SessionImpl session)
  {
    _sessionRef = new SoftReference<SessionImpl>(session);
  }

  /**
   * Returns the message id.
   */
  public String getJMSMessageID()
    throws JMSException
  {
    return _messageId;
  }

  /**
   * Sets the message id.
   *
   * @param id the new message id
   */
  public void setJMSMessageID(String id)
    throws JMSException
  {
    _messageId = id;
  }

  /**
   * Returns the time the message was sent.
   */
  public long getJMSTimestamp()
    throws JMSException
  {
    return _timestamp;
  }

  /**
   * Sets the time the message was sent.
   *
   * @param time the message timestamp
   */
  public void setJMSTimestamp(long time)
    throws JMSException
  {
    _timestamp = time;
  }

  /**
   * Returns the correlation id.
   */
  public byte []getJMSCorrelationIDAsBytes()
    throws JMSException
  {
    try {
      if (_correlationId == null)
        return null;
      else
        return _correlationId.getBytes("UTF8");
    } catch (Throwable e) {
      log.log(Level.WARNING, e.toString(), e);
      
      return null;
    }
  }

  /**
   * Sets the correlation id.
   *
   * @param id the correlation id
   */
  public void setJMSCorrelationIDAsBytes(byte []id)
    throws JMSException
  {
    try {
      _correlationId = new String(id, 0, id.length, "UTF8");
    } catch (Exception e) {
      log.log(Level.WARNING, e.toString(), e);
    }
  }

  /**
   * Returns the correlation id.
   */
  public String getJMSCorrelationID()
    throws JMSException
  {
    return _correlationId;
  }

  /**
   * Sets the correlation id.
   *
   * @param id the correlation id
   */
  public void setJMSCorrelationID(String id)
    throws JMSException
  {
    _correlationId = id;
  }

  /**
   * Gets the reply-to destination
   */
  public Destination getJMSReplyTo()
    throws JMSException
  {
    return _replyTo;
  }

  /**
   * Sets the reply-to destination
   *
   * @param replyTo the destination
   */
  public void setJMSReplyTo(Destination replyTo)
    throws JMSException
  {
    _replyTo = replyTo;
  }

  /**
   * Gets the destination
   */
  public Destination getJMSDestination()
    throws JMSException
  {
    return _destination;
  }

  /**
   * Sets the reply-to destination
   *
   * @param destination the destination
   */
  public void setJMSDestination(Destination destination)
    throws JMSException
  {
    _destination = destination;
  }

  /**
   * Gets the delivery model
   */
  public int getJMSDeliveryMode()
    throws JMSException
  {
    return _deliveryMode;
  }

  /**
   * Sets the delivery mode
   *
   * @param deliveryMode the delivery mode
   */
  public void setJMSDeliveryMode(int deliveryMode)
    throws JMSException
  {
    _deliveryMode = deliveryMode;
  }

  /**
   * Returns if the message is being redelivered.
   */
  public boolean getJMSRedelivered()
    throws JMSException
  {
    return _isRedelivered;
  }

  /**
   * Sets if the message is being redelivered.
   *
   * @param deliveryMode the delivery mode
   */
  public void setJMSRedelivered(boolean isRedelivered)
    throws JMSException
  {
    _isRedelivered = isRedelivered;
  }

  /**
   * Returns the message type
   */
  public String getJMSType()
    throws JMSException
  {
    return _messageType;
  }

  /**
   * Sets the message type.
   *
   * @param type the delivery mode
   */
  public void setJMSType(String type)
    throws JMSException
  {
    _messageType = type;
  }

  /**
   * Returns the message expiration time.
   */
  public long getJMSExpiration()
    throws JMSException
  {
    return _expiration;
  }

  /**
   * Sets the message expiration type.
   *
   * @param time the expiration time
   */
  public void setJMSExpiration(long time)
    throws JMSException
  {
    _expiration = time;
  }

  /**
   * Returns the message priority.
   */
  public int getJMSPriority()
    throws JMSException
  {
    return _priority;
  }

  /**
   * Sets the message priority.
   *
   * @param priority the priority
   */
  public void setJMSPriority(int priority)
    throws JMSException
  {
    _priority = priority;
  }

  /**
   * Clears the message properties, making them writeable.
   */
  public void clearProperties()
    throws JMSException
  {
    if (_properties != null)
      _properties.clear();
    _isHeaderWriteable = true;
  }

  /**
   * Returns true if the property exists.
   */
  public boolean propertyExists(String name)
    throws JMSException
  {
    if (_properties == null)
      return false;
    
    return _properties.keySet().contains(name);
  }

  /**
   * Returns a boolean property with the given name.
   */
  public boolean getBooleanProperty(String name)
    throws JMSException
  {
    return ObjectConverter.toBoolean(getObjectProperty(name));
  }

  /**
   * Returns a property as a byte
   */
  public byte getByteProperty(String name)
    throws JMSException
  {
    return ObjectConverter.toByte(getObjectProperty(name));
  }

  /**
   * Returns a property as a short
   */
  public short getShortProperty(String name)
    throws JMSException
  {
    return ObjectConverter.toShort(getObjectProperty(name));
  }

  /**
   * Returns a property as an integer
   */
  public int getIntProperty(String name)
    throws JMSException
  {
    return ObjectConverter.toInt(getObjectProperty(name));
  }

  /**
   * Returns a property as a long
   */
  public long getLongProperty(String name)
    throws JMSException
  {
    return ObjectConverter.toLong(getObjectProperty(name));
  }

  /**
   * Returns a property as a float
   */
  public float getFloatProperty(String name)
    throws JMSException
  {
    return ObjectConverter.toFloat(getObjectProperty(name));
  }

  /**
   * Returns a property as a double
   */
  public double getDoubleProperty(String name)
    throws JMSException
  {
    return ObjectConverter.toDouble(getObjectProperty(name));
  }

  /**
   * Returns a string property.
   */
  public String getStringProperty(String name)
    throws JMSException
  {
    Object prop = getObjectProperty(name);

    if (prop == null)
      return null;

    return String.valueOf(prop);
  }

  /**
   * Returns a string property.
   */
  public Object getObjectProperty(String name)
    throws JMSException
  {
    if (_properties == null || name == null)
      return null;
    else
      return _properties.get(name);
  }

  /**
   * Returns an enumeration of the message's properties.
   */
  public Enumeration getPropertyNames()
    throws JMSException
  {
    if (_properties == null)
      return NullEnumeration.create();
    else
      return Collections.enumeration(_properties.keySet());
  }

  /**
   * Sets a boolean property.
   *
   * @param name the property name
   * @param value the property's value
   */
  public void setBooleanProperty(String name, boolean value)
    throws JMSException
  {
    setObjectProperty(name, new Boolean(value));
  }

  /**
   * Sets a byte property.
   *
   * @param name the property name
   * @param value the property's value
   */
  public void setByteProperty(String name, byte value)
    throws JMSException
  {
    setObjectProperty(name, new Byte(value));
  }

  /**
   * Sets a short property.
   *
   * @param name the property name
   * @param value the property's value
   */
  public void setShortProperty(String name, short value)
    throws JMSException
  {
    setObjectProperty(name, new Short(value));
  }

  /**
   * Sets an integer property.
   *
   * @param name the property name
   * @param value the property's value
   */
  public void setIntProperty(String name, int value)
    throws JMSException
  {
    setObjectProperty(name, new Integer(value));
  }

  /**
   * Sets a long property.
   *
   * @param name the property name
   * @param value the property's value
   */
  public void setLongProperty(String name, long value)
    throws JMSException
  {
    setObjectProperty(name, new Long(value));
  }

  /**
   * Sets a float property.
   *
   * @param name the property name
   * @param value the property's value
   */
  public void setFloatProperty(String name, float value)
    throws JMSException
  {
    setObjectProperty(name, new Float(value));
  }

  /**
   * Sets a double property.
   *
   * @param name the property name
   * @param value the property's value
   */
  public void setDoubleProperty(String name, double value)
    throws JMSException
  {
    setObjectProperty(name, new Double(value));
  }

  /**
   * Sets a string property.
   *
   * @param name the property name
   * @param value the property's value
   */
  public void setStringProperty(String name, String value)
    throws JMSException
  {
    setObjectProperty(name, value);
  }

  /**
   * Sets an object property.
   *
   * @param name the property name
   * @param value the property's value
   */
  public void setObjectProperty(String name, Object value)
    throws JMSException
  {
    checkPropertyWriteable();

    if (name == null)
      throw new NullPointerException();
    if (isReserved(name))
      throw new JMSException(L.l("'{0}' is a reserved property name.",
				 name));

    if (! (value == null ||
	   value instanceof Number ||
	   value instanceof String ||
	   value instanceof Boolean))
      throw new MessageFormatException(L.l("{0} is an illegal object property value",
					   value.getClass().getName()));
    
    if (_properties == null)
      _properties = new HashMap<String,Object>();

    _properties.put(name, value);
  }
  
  /**
   * Acknowledge receipt of this message.
   */
  public void acknowledge()
    throws JMSException
  {
    SoftReference<SessionImpl> sessionRef = _sessionRef;
    SessionImpl session;

    if (sessionRef != null && (session = sessionRef.get()) != null) {
      session.acknowledge();
    }

    _sessionRef = null;
  }
  
  /**
   * Clears the body, setting write mode.
   */
  public void clearBody()
    throws JMSException
  {
    _isBodyWriteable = true;
  }

  /**
   * Sets the body for reading.
   */
  public void setReceive()
    throws JMSException
  {
    _isHeaderWriteable = false;
    _isBodyWriteable = false;
  }

  /**
   * Sets the body for reading.
   */
  protected void setBodyReadOnly()
    throws JMSException
  {
    _isBodyWriteable = false;
  }

  /**
   * Returns the properties.
   */
  public HashMap<String,Object> getProperties()
  {
    return _properties;
  }
  
  public MessageImpl copy()
  {
    MessageImpl msg = new MessageImpl();

    copy(msg);

    return msg;
  }

  protected void checkHeaderWriteable()
    throws JMSException
  {
    if (! _isHeaderWriteable)
      throw new MessageNotWriteableException(L.l("received messages can't be written."));
  }

  protected void checkPropertyWriteable()
    throws JMSException
  {
    if (! _isHeaderWriteable)
      throw new MessageNotWriteableException(L.l("properties for received messages are read-only."));
  }

  protected void checkBodyWriteable()
    throws JMSException
  {
    if (! _isBodyWriteable)
      throw new MessageNotWriteableException(L.l("received messages can't be written."));
  }

  protected void checkBodyReadable()
    throws JMSException
  {
    if (_isBodyWriteable)
      throw new MessageNotReadableException(L.l("write-only messages can't be read until reset() is called."));
  }

  protected void copy(MessageImpl newMsg)
  {
    if (_properties != null) {
      newMsg._properties = new HashMap<String,Object>(_properties);
    }

    newMsg._messageId = _messageId;
    newMsg._correlationId = _correlationId;
    
    newMsg._timestamp = _timestamp;
    newMsg._expiration = _expiration;
    
    newMsg._destination = _destination;
    newMsg._replyTo = _replyTo;

    newMsg._deliveryMode = _deliveryMode;
    newMsg._isRedelivered = _isRedelivered;
    
    newMsg._messageType = _messageType;
    newMsg._priority = _priority;
  }

  public String toString()
  {
    if (_messageId != null)
      return getClass().getName() + "[" + _messageId + "]";
    else
      return getClass().getName() + "@" + System.identityHashCode(this);
  }

  public static boolean isReserved(String name)
  {
    return _reserved.contains(name.toUpperCase());
  }

  static {
    _reserved = new HashSet<String>();
    _reserved.add("TRUE");
    _reserved.add("FALSE");
    _reserved.add("NULL");
    _reserved.add("NOT");
    _reserved.add("AND");
    _reserved.add("OR");
    _reserved.add("BETWEEN");
    _reserved.add("LIKE");
    _reserved.add("IN");
    _reserved.add("IS");
    _reserved.add("ESCAPE");
  }
}

