/** 
 * 
 * Copyright 2004 Protique Ltd
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 * 
 **/

package org.codehaus.activemq.message;

import org.codehaus.activemq.filter.DestinationFilter;
import org.codehaus.activemq.filter.DestinationPath;
import org.codehaus.activemq.jndi.JNDIBaseStorable;
import org.codehaus.activemq.management.JMSDestinationStats;

import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Queue;
import javax.jms.TemporaryQueue;
import javax.jms.TemporaryTopic;
import javax.jms.Topic;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * A <CODE>Destination</CODE> object encapsulates a provider-specific
 * address.
 * The JMS API does not define a standard address syntax. Although a standard
 * address syntax was considered, it was decided that the differences in
 * address semantics between existing message-oriented middleware (MOM)
 * products were too wide to bridge with a single syntax.
 * <p/>
 * <P>Since <CODE>Destination</CODE> is an administered object, it may
 * contain
 * provider-specific configuration information in addition to its address.
 * <p/>
 * <P>The JMS API also supports a client's use of provider-specific address
 * names.
 * <p/>
 * <P><CODE>Destination</CODE> objects support concurrent use.
 * <p/>
 * <P>A <CODE>Destination</CODE> object is a JMS administered object.
 * <p/>
 * <P>JMS administered objects are objects containing configuration
 * information that are created by an administrator and later used by
 * JMS clients. They make it practical to administer the JMS API in the
 * enterprise.
 * <p/>
 * <P>Although the interfaces for administered objects do not explicitly
 * depend on the Java Naming and Directory Interface (JNDI) API, the JMS API
 * establishes the convention that JMS clients find administered objects by
 * looking them up in a JNDI namespace.
 * <p/>
 * <P>An administrator can place an administered object anywhere in a
 * namespace. The JMS API does not define a naming policy.
 * <p/>
 * <P>It is expected that JMS providers will provide the tools an
 * administrator needs to create and configure administered objects in a
 * JNDI namespace. JMS provider implementations of administered objects
 * should implement the <CODE>javax.naming.Referenceable</CODE> and
 * <CODE>java.io.Serializable</CODE> interfaces so that they can be stored in
 * all JNDI naming contexts. In addition, it is recommended that these
 * implementations follow the JavaBeans<SUP><FONT SIZE="-2">TM</FONT></SUP>
 * design patterns.
 * <p/>
 * <P>This strategy provides several benefits:
 * <p/>
 * <UL>
 * <LI>It hides provider-specific details from JMS clients.
 * <LI>It abstracts JMS administrative information into objects in the Java
 * programming language ("Java objects")
 * that are easily organized and administered from a common
 * management console.
 * <LI>Since there will be JNDI providers for all popular naming
 * services, JMS providers can deliver one implementation
 * of administered objects that will run everywhere.
 * </UL>
 * <p/>
 * <P>An administered object should not hold on to any remote resources.
 * Its lookup should not use remote resources other than those used by the
 * JNDI API itself.
 * <p/>
 * <P>Clients should think of administered objects as local Java objects.
 * Looking them up should not have any hidden side effects or use surprising
 * amounts of local resources.
 */

public abstract class ActiveMQDestination extends JNDIBaseStorable implements Destination, Comparable, Serializable {

    /**
     * Topic Destination object
     */
    public static final int ACTIVEMQ_TOPIC = 1;
    /**
     * Temporary Topic Destination object
     */
    public static final int ACTIVEMQ_TEMPORARY_TOPIC = 2;

    /**
     * Queue Destination object
     */
    public static final int ACTIVEMQ_QUEUE = 3;
    /**
     * Temporary Queue Destination object
     */
    public static final int ACTIVEMQ_TEMPORARY_QUEUE = 4;

    private static final int NULL_DESTINATION = 10;

    private static final String TEMP_PREFIX = "{TD{";
    private static final String TEMP_POSTFIX = "}TD}";
    private static final String COMPOSITE_SEPARATOR = ",";
    private static final String QUEUE_PREFIX = "queue://";
    private static final String TOPIC_PREFIX = "topic://";

    private String physicalName;

    // Cached transient data
    private transient DestinationFilter filter;
    private transient JMSDestinationStats stats;
    private transient String[] paths;
    // Used track consumers of temporary topics.
    private transient int consumerCounter;
    private transient boolean deleted;

    /**
     * A helper method to return a descriptive string for the topic or queue
     *
     * @return a descriptive string for this queue or topic
     */
    public static String inspect(Destination destination) {
        if (destination instanceof Topic) {
            return "Topic(" + destination.toString() + ")";
        }
        else {
            return "Queue(" + destination.toString() + ")";
        }
    }

    /**
     * @param destination
     * @return @throws JMSException
     * @throws javax.jms.JMSException
     */
    public static ActiveMQDestination transformDestination(Destination destination) throws JMSException {
        ActiveMQDestination result = null;
        if (destination != null) {
            if (destination instanceof ActiveMQDestination) {
                result = (ActiveMQDestination) destination;
            }
            else {
                if (destination instanceof TemporaryQueue) {
                    result = new ActiveMQTemporaryQueue(((Queue) destination).getQueueName());
                }
                else if (destination instanceof TemporaryTopic) {
                    result = new ActiveMQTemporaryTopic(((Topic) destination).getTopicName());
                }
                else if (destination instanceof Queue) {
                    result = new ActiveMQTemporaryQueue(((Queue) destination).getQueueName());
                }
                else if (destination instanceof Topic) {
                    result = new ActiveMQTemporaryTopic(((Topic) destination).getTopicName());
                }
            }
        }
        return result;
    }

    /**
     * Write an ActiveMQDestination to a Stream
     *
     * @param destination
     * @param dataOut
     * @throws IOException
     */

    public static void writeToStream(ActiveMQDestination destination, DataOutput dataOut) throws IOException {
        if (destination != null) {
            dataOut.write(destination.getDestinationType());
            dataOut.writeUTF(destination == null ? "" : destination.getPhysicalName());
        }
        else {
            dataOut.write(NULL_DESTINATION);
        }
    }

    /**
     * Read an ActiveMQDestination  from a Stream
     *
     * @param dataIn
     * @return the ActiveMQDestination
     * @throws IOException
     */

    public static ActiveMQDestination readFromStream(DataInput dataIn) throws IOException {

        int type = dataIn.readByte();
        if (type == NULL_DESTINATION) {
            return null;
        }
        ActiveMQDestination result = null;
        if (type == ACTIVEMQ_TOPIC) {
            result = new ActiveMQTopic();
        }
        else if (type == ACTIVEMQ_TEMPORARY_TOPIC) {
            result = new ActiveMQTemporaryTopic();
        }
        else if (type == ACTIVEMQ_QUEUE) {
            result = new ActiveMQQueue();
        }
        else {
            result = new ActiveMQTemporaryQueue();
        }
        result.setPhysicalName(dataIn.readUTF());
        return result;

    }

    /**
     * Create a temporary name from the clientId
     *
     * @param clientId
     * @return
     */
    public static String createTemporaryName(String clientId) {
        return TEMP_PREFIX + clientId + TEMP_POSTFIX;
    }

    /**
     * From a temporary destination find the clientId of the Connection that created it
     *
     * @param destination
     * @return the clientId or null if not a temporary destination
     */
    public static String getClientId(ActiveMQDestination destination) {
        String answer = null;
        if (destination != null && destination.isTemporary()) {
            String name = destination.getPhysicalName();
            int start = name.indexOf(TEMP_PREFIX);
            if (start >= 0) {
                start += TEMP_PREFIX.length();
                int stop = name.lastIndexOf(TEMP_POSTFIX);
                if (stop > start && stop < name.length()) {
                    answer = name.substring(start, stop);
                }
            }
        }
        return answer;
    }


    /**
     * The Default Constructor
     */
    protected ActiveMQDestination() {
    }

    /**
     * Construct the ActiveMQDestination with a defined physical name;
     *
     * @param name
     */

    protected ActiveMQDestination(String name) {
        this.physicalName = name;
    }

    /**
     * @param o object to compare
     * @return 1 if this > o else 0 if they are equal or -1 if this < o
     */
    public int compareTo(Object o) {
        if (o instanceof ActiveMQDestination) {
            return compareTo((ActiveMQDestination) o);
        }
        return -1;
    }

    /**
     * Lets sort by name first then lets sort topics greater than queues
     *
     * @param that another destination to compare against
     * @return 1 if this > that else 0 if they are equal or -1 if this < that
     */
    public int compareTo(ActiveMQDestination that) {
        int answer = 0;
        if (physicalName != that.physicalName) {
            if (physicalName == null) {
                return -1;
            }
            else if (that.physicalName == null) {
                return 1;
            }
            answer = physicalName.compareTo(that.physicalName);
        }
        if (answer == 0) {
            if (isTopic()) {
                if (that.isQueue()) {
                    return 1;
                }
            }
            else {
                if (that.isTopic()) {
                    return -1;
                }
            }
        }
        return answer;
    }


    /**
     * @return Returns the Destination type
     */

    public abstract int getDestinationType();


    /**
     * @return Returns the physicalName.
     */
    public String getPhysicalName() {
        return this.physicalName;
    }

    /**
     * @param newPhysicalName The physicalName to set.
     */
    public void setPhysicalName(String newPhysicalName) {
        this.physicalName = newPhysicalName;
    }

    /**
     * Returns true if a temporary Destination
     *
     * @return true/false
     */

    public boolean isTemporary() {
        return getDestinationType() == ACTIVEMQ_TEMPORARY_TOPIC ||
                getDestinationType() == ACTIVEMQ_TEMPORARY_QUEUE;
    }

    /**
     * Returns true if a Topic Destination
     *
     * @return true/false
     */

    public boolean isTopic() {
        return getDestinationType() == ACTIVEMQ_TOPIC ||
                getDestinationType() == ACTIVEMQ_TEMPORARY_TOPIC;
    }

    /**
     * Returns true if a Queue Destination
     *
     * @return true/false
     */
    public boolean isQueue() {
        return !isTopic();
    }

    /**
     * Returns true if this destination represents a collection of
     * destinations; allowing a set of destinations to be published to or subscribed
     * from in one JMS operation.
     * <p/>
     * If this destination is a composite then you can call {@link #getChildDestinations()}
     * to return the list of child destinations.
     *
     * @return true if this destination represents a collection of child destinations.
     */
    public boolean isComposite() {
        return physicalName.indexOf(COMPOSITE_SEPARATOR) > 0;
    }

    /**
     * Returns a list of child destinations if this destination represents a composite
     * destination.
     *
     * @return
     */
    public List getChildDestinations() {
        List answer = new ArrayList();
        StringTokenizer iter = new StringTokenizer(physicalName, COMPOSITE_SEPARATOR);
        while (iter.hasMoreTokens()) {
            String name = iter.nextToken();
            Destination child = null;
            if (name.startsWith(QUEUE_PREFIX)) {
                child = new ActiveMQQueue(name.substring(QUEUE_PREFIX.length()));
            }
            else if (name.startsWith(TOPIC_PREFIX)) {
                child = new ActiveMQTopic(name.substring(TOPIC_PREFIX.length()));
            }
            else {
                child = createDestination(name);
            }
            answer.add(child);
        }
        if (answer.size() == 1) {
            // lets put ourselves inside the collection
            // as we are not really a composite destination
            answer.set(0, this);
        }
        return answer;
    }

    /**
     * @return string representation of this instance
     */

    public String toString() {
        return this.physicalName;
    }

    /**
     * @return hashCode for this instance
     */

    public int hashCode() {
        int answer = 0xcafebabe;

        if (this.physicalName != null) {
            answer = physicalName.hashCode();
        }
        if (isTopic()) {
            answer ^= 0xfabfab;
        }
        return answer;
    }

    /**
     * if the object passed in is equivalent, return true
     *
     * @param obj the object to compare
     * @return true if this instance and obj are equivalent
     */

    public boolean equals(Object obj) {
        boolean result = this == obj;
        if (!result && obj != null && obj instanceof ActiveMQDestination) {
            ActiveMQDestination other = (ActiveMQDestination) obj;
            result = this.getDestinationType() == other.getDestinationType() &&
                    this.physicalName.equals(other.physicalName);
        }
        return result;
    }


    /**
     * @return true if the destination matches multiple possible destinations
     */
    public boolean isWildcard() {
        if (physicalName != null) {
            return physicalName.indexOf(DestinationFilter.ANY_CHILD) >= 0
                    || physicalName.indexOf(DestinationFilter.ANY_DESCENDENT) >= 0;
        }
        return false;
    }

    /**
     * Returns true if the given destination matches this destination; including wildcards
     */
    public boolean matches(ActiveMQDestination destination) {
        if (isWildcard()) {
            return getDestinationFilter().matches(destination);
        }
        else {
            return equals(destination);
        }
    }

    public DestinationFilter getDestinationFilter() {
        if (filter == null) {
            filter = DestinationFilter.parseFilter(this);
        }
        return filter;
    }

    public String[] getDestinationPaths() {
        if (paths == null) {
            paths = DestinationPath.getDestinationPaths(physicalName);
        }
        return paths;
    }

    public JMSDestinationStats getStats() {
        if (stats == null) {
            stats = createDestinationStats();
        }
        return stats;
    }


    public void setStats(JMSDestinationStats stats) {
        this.stats = stats;
    }

    synchronized public void incrementConsumerCounter() {
    	consumerCounter++;
    }

    synchronized public void decrementConsumerCounter() {
    	consumerCounter--;
    }
    
    synchronized public boolean isDeleted() {
    	return deleted;
    }
    
    /**
     * Used to Deletes a temporary destination. If there are existing consumers
     * still using it, a <CODE>JMSException</CODE> will be thrown.
     *
     * @throws JMSException if the JMS provider fails to delete the
     *                      temporary queue due to some internal error.
     */
    synchronized public void delete() throws JMSException {
    	if( consumerCounter!= 0 ) {
    		throw new JMSException("A consumer is still using this temporary queue.");
    	}
    	deleted=true;
    }    
    
    // Implementation methods
    //-------------------------------------------------------------------------


    /**
     * Factory method to create a child destination if this destination is a composite
     */
    protected abstract Destination createDestination(String name);

    /**
     * Factory method to create a statistics counter object
     *
     * @return
     */
    protected abstract JMSDestinationStats createDestinationStats();

    /**
     * Set the properties that will represent the instance in JNDI
     *
     * @param props
     */
    protected void buildFromProperties(Properties props) {
        this.physicalName = props.getProperty("physicalName", this.physicalName);

    }

    /**
     * Initialize the instance from properties stored in JNDI
     *
     * @param props
     */

    protected void populateProperties(Properties props) {
        props.put("physicalName", this.physicalName);
    }
    
}
