/**
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
 *
 * 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.jivesoftware.openfire.pubsub;

import java.io.Serializable;
import java.io.StringReader;
import java.util.Date;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.pep.PEPServiceManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;

/**
 * A published item to a node. Once an item was published to a node, node subscribers will be
 * notified of the new published item. The item publisher may be allowed to delete published
 * items. After a published item was deleted node subscribers will get an event notification.<p>
 *
 * Published items may be persisted to the database depending on the node configuration.
 * Actually, even when the node is configured to not persist items the last published
 * item is going to be persisted to the database. The reason for this is that the node
 * may need to send the last published item to new subscribers.
 *
 * @author Matt Tucker
 */
public class PublishedItem implements Serializable {

    private static final Logger log = LoggerFactory.getLogger(PublishedItem.class);

    private static final int POOL_SIZE = 50;
    /**
     * Pool of SAX Readers. SAXReader is not thread safe so we need to have a pool of readers.
     */
    private static BlockingQueue<SAXReader> xmlReaders = new LinkedBlockingQueue<>(POOL_SIZE);

    private static final long serialVersionUID = 7012925993623144574L;
    
    static {
        // Initialize the pool of sax readers
        for (int i=0; i<POOL_SIZE; i++) {
            SAXReader xmlReader = new SAXReader();
            xmlReader.setEncoding("UTF-8");
            xmlReaders.add(xmlReader);
        }    	
    }
	
	/**
     * JID of the entity that published the item to the node. This is the full JID
     * of the publisher.
     */
    private JID publisher;
    /**
     * The node where the item was published.
     */
	private volatile transient LeafNode node;
    /**
     * The id for the node where the item was published.
     */
    private String nodeId;
    /**
     * The id for the service hosting the node for this item
     */
    private String serviceId;
    /**
     * ID that uniquely identifies the published item in the node.
     */
    private String id;
    /**
     * The datetime when the items was published.
     */
    private Date creationDate;
    /**
     * The optional payload is included when publishing the item. This value
     * is created from the payload XML and cached as/when needed.
     */
	private volatile transient Element payload;
    /**
     * XML representation of the payload (for serialization)
     */
    private String payloadXML;
    
    /**
     * Creates a published item
     * @param node
     * @param publisher
     * @param id
     * @param creationDate
     */
    PublishedItem(LeafNode node, JID publisher, String id, Date creationDate) {
        this.node = node;
        this.nodeId = node.getNodeID();
        this.serviceId = node.getService().getServiceID();
        this.publisher = publisher;
        this.id = id;
        this.creationDate = creationDate;
    }

    /**
     * Returns the id for the {@link LeafNode} where this item was published.
     *
     * @return the ID for the leaf node where this item was published.
     */
    public String getNodeID() {
        return nodeId;
    }

    /**
     * Returns the {@link LeafNode} where this item was published.
     *
     * @return the leaf node where this item was published.
     */
    public LeafNode getNode() {
    	if (node == null) {
			synchronized (this) {
				if (node == null) {
					if (XMPPServer.getInstance().getPubSubModule().getServiceID().equals(serviceId))
					{
						node = (LeafNode) XMPPServer.getInstance().getPubSubModule().getNode(nodeId);
					}
					else
					{
						PEPServiceManager serviceMgr = XMPPServer.getInstance().getIQPEPHandler().getServiceManager();
						node = serviceMgr.hasCachedService(new JID(serviceId)) ? (LeafNode) serviceMgr.getPEPService(
								serviceId).getNode(nodeId) : null;
					}
				}
			}
    	}
    	return node;
    }

    /**
     * Returns the ID that uniquely identifies the published item in the node.
     *
     * @return the ID that uniquely identifies the published item in the node.
     */
    public String getID() {
        return id;
    }

    /**
     * Returns the JID of the entity that published the item to the node.
     *
     * @return the JID of the entity that published the item to the node.
     */
    public JID getPublisher() {
        return publisher;
    }

    /**
     * Returns the datetime when the items was published.
     *
     * @return the datetime when the items was published.
     */
    public Date getCreationDate() {
        return creationDate;
    }

    /**
     * Returns the payload included when publishing the item. A published item may or may not
     * have a payload. Transient nodes that are configured to not broadcast payloads may allow
     * published items to have no payload.
     *
     * @return the payload included when publishing the item or <tt>null</tt> if none was found.
     */
    public Element getPayload() {
    	if (payload == null && payloadXML != null) {
    		synchronized (this) {
				if (payload == null) {
		    		// payload initialized as XML string from DB
		            SAXReader xmlReader = null;
		    		try {
		    			xmlReader = xmlReaders.take();
		    			payload = xmlReader.read(new StringReader(payloadXML)).getRootElement(); 
		    		} catch (Exception ex) {
		    			 log.error("Failed to parse payload XML", ex);
		    		} finally {
		    			if (xmlReader != null) {
		    				xmlReaders.add(xmlReader);
		    			}
		    		}
				}
			}
    	}
        return payload;
    }

    /**
     * Returns a textual representation of the payload or <tt>null</tt> if no payload
     * was specified with the item.
     *
     * @return a textual representation of the payload or null if no payload was specified
     *         with the item.
     */
    public String getPayloadXML() {
        return payloadXML;
    }

    /**
     * Sets the payload included when publishing the item. A published item may or may not
     * have a payload. Transient nodes that are configured to not broadcast payloads may allow
     * published items to have no payload.
     *
     * @param payloadXML the payload included when publishing the item or <tt>null</tt>
     *        if none was found.
     */
    void setPayloadXML(String payloadXML) {
    	this.payloadXML = payloadXML;
    	this.payload = null; // will be recreated only if needed
    }

    /**
     * Sets the payload included when publishing the item. A published item may or may not
     * have a payload. Transient nodes that are configured to not broadcast payloads may allow
     * published items to have no payload.
     *
     * @param payload the payload included when publishing the item or <tt>null</tt>
     *        if none was found.
     */
    void setPayload(Element payload) {
        this.payload = payload;
        // Update XML representation of the payload
        if (payload == null) {
            payloadXML = null;
        } else {
            payloadXML = payload.asXML();
        }
    }

    /**
     * Returns true if payload contains the specified keyword. If the item has no payload
     * or keyword is <tt>null</tt> then return true.
     *
     * @param keyword the keyword to look for in the payload.
     * @return true if payload contains the specified keyword.
     */
    boolean containsKeyword(String keyword) {
        if (getPayloadXML() == null || keyword == null) {
            return true;
        }
        return payloadXML.contains(keyword);
    }

    /**
     * Returns true if the user that is trying to delete an item is allowed to delete it.
     * Only the publisher or node admins (i.e. owners and sysadmins) are allowed to delete items.
     *
     * @param user the full JID of the user trying to delete the item.
     * @return true if the user that is trying to delete an item is allowed to delete it.
     */
    public boolean canDelete(JID user) {
        if (publisher.equals(user) || publisher.toBareJID().equals(user.toBareJID()) ||
                getNode().isAdmin(user)) {
            return true;
        }
        return false;
    }

    /**
     * Returns a string that uniquely identifies this published item
     * in the following format: <i>nodeId:itemId</i>
     * @return Unique identifier for this item
     */
    public String getItemKey() {
    	return getItemKey(nodeId,id);
    }

	/**
     * Returns a string that uniquely identifies this published item
     * in the following format: <i>nodeId:itemId</i>
     * @param node Node for the published item
     * @param itemId Id for the published item (unique within the node)
     * @return Unique identifier for this item
     */
    public static String getItemKey(LeafNode node, String itemId) {
    	return getItemKey(node.getNodeID(), itemId);
    }

    /**
     * Returns a string that uniquely identifies this published item
     * in the following format: <i>nodeId:itemId</i>
     * @param nodeId Node id for the published item
     * @param itemId Id for the published item (unique within the node)
     * @return Unique identifier for this item
     */
    public static String getItemKey(String nodeId, String itemId) {
    	return new StringBuilder(nodeId)
    		.append(':').append(itemId).toString();
    }
}