/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (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.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
package com.arsdigita.caching;

import com.arsdigita.domain.DomainCollection;
import com.arsdigita.util.servlet.HttpHost;
import com.arsdigita.web.Host;
import com.arsdigita.web.ParameterMap;
import com.arsdigita.web.Web;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URLDecoder;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;

/**
 *  <p> A simple servlet which accepts the cache notifications issued by other
 * webserver to implement <em>coherent caching</em>.  Since there can be any
 * number of webservers attached to the same database instance, we must make
 * sure that caches among different webservers contain either the same,
 * consistent value for a particular cache entry, or no value at all. </p>
 *
 *  <p> Whenever a {@link CacheTable#put(String,Object)} is invoked, a HTTP
 * request is sent to all the other JVMs running this service, (except the
 * current server of course).
 *
 *  <p> The HTTP notification for newly added cache entry carries the hashcode
 * of the new entry, so that the peer caches can decide whether the entry they
 * already have (if at all) is outdated or not.  For
 * {@link CacheTable#remove(String)} invocations, no hash code is produced, and
 * peer caches will remove this item unconditionally. </p>
 *
 * @author Matthew Booth
 * @author Sebastian Skracic
 *
 * @version $Revision: #18 $ $DateTime: 2004/04/07 16:07:11 $
 */
public class CacheServlet extends HttpServlet {
    private static final Logger s_log =
        Logger.getLogger( CacheServlet.class );

    private static final String ID = "id";
    private static final String KEY = "key";
    private static final String HASH = "hash";

    // If you change this, make sure that web.xml is changed as well
    static final String SERVLET_URL = "/expireCache";

    /**
     *  This is executed when foreign server asked us to drop an entry
     * from our cache.  Make sure that we don't end up in recursion.
     */
    protected void doGet( HttpServletRequest req, HttpServletResponse res ) {
        String id = req.getParameter( ID );
        String key = req.getParameter( KEY );

        if (s_log.isInfoEnabled()) {
            s_log.info("Got remove request from " + req.getRemoteHost());
        }

        if (id == null || key == null) {
            return;
        }

        id = URLDecoder.decode(id);
        key = URLDecoder.decode(key);

        final CacheTable cache = CacheTable.getCache( id );
        if (cache == null) {
            s_log.debug("No cache with id " + id);
            return;
        }

        s_log.debug("Removing " + key + " from cache " + id);

        final String hash = req.getParameter( HASH );
        final Integer hashCode = getHashCode(hash);
        if (hashCode == null) {
            // unconditionally remove
            cache.removeLocally(key);
        } else {
            cache.removeLocallyIfOutdated(key, hashCode.intValue());
        }
    }

    private Integer getHashCode(final String hash) {
        if (hash == null) {
            return null;
        }
        Integer hashCode = null;
        try {
            hashCode = new Integer(hash);
        } catch (NumberFormatException nfe) {
            // just ignore and pretend that no hash value was supplied at all
            s_log.warn("format exception on hash " + hash + " : " + nfe.getMessage() );
        }
        return hashCode;
    }


    /**
     *  Complete removal - first get rid of entry in local cache,
     * then annoy other servers.
     */
    static void remove( String cache_id, String key ) {

        CacheTable cache = CacheTable.getCache( cache_id );
        if (cache == null) {
            return;
        }

        cache.removeLocally(key);
        removeFromPeers(cache_id, key);
    }


    /**
     *  Sometimes we need to remove entries only from peer webservers.
     */
    static void removeFromPeers(String cache_id,
                                String key) {
        notifyPeers(cache_id, key, null);
    }

    /**
     *  Notifies peers on adding a new cache entry.  Deletes the peer's cache entry
     * if its contain the object with hashcode not matching <tt>newHashCode</tt>.
     */
    static void removeOutdatedFromPeers(String cache_id,
                                        String key,
                                        int newHashCode) {
        notifyPeers(cache_id, key, String.valueOf(newHashCode));
    }

    /**
     *  Sends "GET /expireCache?" + params to all peer webservers.
     */
    private static void notifyPeers(final String id,
                                    final String key,
                                    final String hash) {
//         // XXX unpleasant hack to get around bootstrapping problem

//         // XXX temporarily disabaling multi JVM support in order to
//         // work around bootstrapping issues on the packaging branch.
//         if (true) { return; }

//         try {
//             DomainObjectFactory.newInstance(new OID(Host.BASE_DATA_OBJECT_TYPE,
//                                                     0));
//         } catch (InstantiatorNotFoundException nfe) {
//             s_log.warn("Not notifying peers; server isn't bootstrapped yet");
//             return;
//         }

        s_log.debug("notifying peers");
        final DomainCollection hosts = Host.retrieveAll();
        final HttpHost current = Web.getConfig().getHost();
        hosts.addNotEqualsFilter(Host.SERVER_PORT, new Integer(current.getPort()));
        while (hosts.next()) {
            final Host host = (Host) hosts.getDomainObject();
            notifyPeer(host, makeParameterMap(id, key, hash));
        }
    }

    private static void notifyPeer(Host host, ParameterMap params) {
        final String url = "http://" + host + SERVLET_URL + params;

        try {
            s_log.debug("sending notification to " + url);
            java.net.URL netURL = new java.net.URL(url);
            new Thread(new HTTPRequester(netURL)).start();
        } catch(MalformedURLException e) {
            s_log.error("malformed URL: " + url);
        }
    }

    private static ParameterMap makeParameterMap(final String id,
                                                 final String key,
                                                 final String hash) {
        final ParameterMap params = new ParameterMap();

        params.setParameter(ID, id);

        if (key != null) {
            params.setParameter(KEY, key);
        }

        if (hash != null) {
            params.setParameter(HASH, hash);
        }

        return params;
    }

    private static class HTTPRequester implements Runnable {
        private static final Logger s_log =
            Logger.getLogger( HTTPRequester.class );

        private final java.net.URL m_url;

        public HTTPRequester(java.net.URL url) {
            m_url = url;
        }

        public void run() {
            try {
                if (s_log.isDebugEnabled()) {
                    s_log.debug("Sending invalidate to " + m_url);
                }
                m_url.openStream();

                // XXX check status is 200, or rather, not an error code
            } catch (IOException e) {
                s_log.error("Failure sending cache invalidate: " + m_url, e);
            }
        }
    }
}

