org.wicketstuff.chat.channel.TimerChannelBehavior.java Source code

Java tutorial

Introduction

Here is the source code for org.wicketstuff.chat.channel.TimerChannelBehavior.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.wicketstuff.chat.channel;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.wicket.Application;
import org.apache.wicket.Component;
import org.apache.wicket.MetaDataKey;
import org.apache.wicket.ThreadContext;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.ajax.AbstractAjaxTimerBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.head.IHeaderResponse;
//import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.util.time.Duration;
import org.apache.wicket.util.time.Time;
import org.wicketstuff.chat.channel.api.IChannelService;
import org.wicketstuff.chat.channel.api.IPushTarget;

/**
 * Behavior used to enqueue triggers and send them to the client using timer
 * based polling.
 * <p>
 * The polling interval is configured in the constructor. The more frequent is
 * the polling, the more quickly your client will be updated, but also the more
 * you will load your server and your network.
 * <p>
 * A timeout can also be configured to indicate when the behavior should
 * consider the page has been disconnected. This is important to clean
 * appropriately the resources associated with the page.
 * 
 * @author Xavier Hanin
 * 
 * @see IChannelService
 * @see TimerChannelService
 * @see TimerPushService
 */
public class TimerChannelBehavior extends AbstractAjaxTimerBehavior implements Serializable {

    private static final long serialVersionUID = 1L;

    private static final AtomicLong COUNTER = new AtomicLong();

    private static Method[] methods;

    private static final int ADD_COMPONENT_METHOD = 0;

    private static final int ADD_COMPONENT_WITH_MARKUP_ID_METHOD = 1;

    private static final int APPEND_JAVASCRIPT_METHOD = 2;

    private static final int PREPEND_JAVASCRIPT_METHOD = 3;

    private static final int FOCUS_COMPONENT_METHOD = 4;

    /**
     * The default margin after a polling interval to consider the page is
     * disconnected
     */
    static final Duration TIMEOUT_MARGIN = Duration.seconds(5);

    static {
        try {
            methods = new Method[] { AjaxRequestTarget.class.getMethod("add", new Class[] { Component[].class }),
                    AjaxRequestTarget.class.getMethod("add", new Class[] { Component.class, String.class }),
                    AjaxRequestTarget.class.getMethod("appendJavaScript", new Class[] { CharSequence.class }),
                    AjaxRequestTarget.class.getMethod("prependJavaScript", new Class[] { CharSequence.class }),
                    AjaxRequestTarget.class.getMethod("focusComponent", new Class[] { Component.class }), };
        } catch (final Exception e) {
            throw new WicketRuntimeException("Unable to initialize DefaultAjaxPushBehavior", e);
        }
    }

    /**
     * This class is used to store a list of delayed method calls.
     * 
     * The method calls are actually calls to methods on
     * {@link AjaxRequestTarget}, which are invoked when the client polls the
     * server.
     * 
     * @author Xavier Hanin
     */
    private static class DelayedMethodCallList implements Serializable {
        private static final long serialVersionUID = 1L;

        private final Application _application;

        /**
         * Used to store a method and its parameters to be later invoked on an
         * object.
         * 
         * @author Xavier Hanin
         */
        private class DelayedMethodCall implements Serializable {
            private static final long serialVersionUID = 1L;

            /**
             * The index of the method to invoke We store only an index to avoid
             * serialization issues
             */
            private final int m;
            /**
             * the parameters to use when the method is called
             */
            private final Object[] parameters;

            /**
             * Construct.
             * 
             * @param m
             *            the index of the method to be called
             * @param parameters
             *            the parameters to use when the method is called
             */
            public DelayedMethodCall(final int m, final Object[] parameters) {
                this.m = m;
                this.parameters = parameters;
            }

            /**
             * Invokes the method with the parameters on the given object.
             * 
             * @see java.lang.reflect.Method#invoke(Object, Object[])
             * @param o
             *            the object on which the method should be called
             * @throws IllegalArgumentException
             * @throws IllegalAccessException
             * @throws InvocationTargetException
             */
            public void invoke(final Object o)
                    throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
                final Application originalApplication = Application.get();
                try {
                    ThreadContext.setApplication(_application);
                    methods[m].invoke(o, parameters);
                } finally {
                    ThreadContext.setApplication(originalApplication);
                }
            }
        }

        /**
         * stores the list of {@link DelayedMethodCall} to invoke
         */
        private final List<DelayedMethodCall> calls;

        /**
         * Construct.
         */
        public DelayedMethodCallList() {
            _application = Application.get();
            calls = new ArrayList<DelayedMethodCall>();
        }

        /**
         * Construct a copy of the given {@link DelayedMethodCallList}.
         * 
         * @param dmcl
         */
        public DelayedMethodCallList(final DelayedMethodCallList dmcl) {
            _application = Application.get();
            calls = new ArrayList<DelayedMethodCall>(dmcl.calls);
        }

        /**
         * Add a {@link DelayedMethodCall} to the list
         * 
         * @param m
         *            the index of the method to be later invoked
         * @param parameters
         *            the parameters to use when the method will be invoked
         */
        public void addCall(final int m, final Object[] parameters) {
            calls.add(new DelayedMethodCall(m, parameters));
        }

        /**
         * Invokes all the {@link DelayedMethodCall} in the list on the given
         * Object
         * 
         * @see java.lang.reflect.Method#invoke(Object, Object[])
         * @param o
         *            the object on which delayed methods should be called
         * @throws IllegalArgumentException
         * @throws IllegalAccessException
         * @throws InvocationTargetException
         */
        public void invoke(final Object o)
                throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
            for (final DelayedMethodCall dmc : calls) {
                dmc.invoke(o);
            }
        }

        /**
         * Indicates if this list is empty or not
         * 
         * @return true if this list is empty, false otherwise
         */
        public boolean isEmpty() {
            return calls.isEmpty();
        }

        /**
         * Used to remove all the delayed methods from this list
         */
        public void clear() {
            calls.clear();
        }
    }

    /**
     * An {@link IPushTarget} implementation which enqueue
     * {@link DelayedMethodCallList}, also called triggers, for a
     * {@link TimerChannelBehavior} identified by its id.
     * 
     * TimerPushTarget are thread safe, and can be used from any thread. Since
     * it is not serializable, it is not intended to be stored in a wicket
     * component.
     * 
     * @author Xavier Hanin
     */
    public static class TimerPushTarget implements IPushTarget {
        /**
         * A trigger currently being constructed, waiting for a call to trigger
         * to go to the triggers list.
         */
        private final DelayedMethodCallList currentTrigger = new DelayedMethodCallList();
        /**
         * The Wicket Application in which this target is used
         */
        private final Application application;
        /**
         * The id of the behavior to which this target corresponds
         */
        private final String id;
        /**
         * The duration to wait before considering that a page is not connected
         * any more This is usually set to the polling interval + a safety
         * margin
         */
        private final Duration timeout;

        public TimerPushTarget(final Application application, final String id, final Duration timeout) {
            super();
            this.application = application;
            this.id = id;
            this.timeout = timeout;
        }

        /**
         * @see IAjaxPushBehavior#addComponent(Component)
         */
        public void addComponent(final Component component) {
            synchronized (currentTrigger) {
                currentTrigger.addCall(ADD_COMPONENT_METHOD, new Object[] { component });
            }
        }

        /**
         * @see IAjaxPushBehavior#addComponent(Component, String)
         */
        public void addComponent(final Component component, final String markupId) {
            synchronized (currentTrigger) {
                currentTrigger.addCall(ADD_COMPONENT_WITH_MARKUP_ID_METHOD, new Object[] { component, markupId });
            }
        }

        /**
         * @see IAjaxPushBehavior#appendJavaScript(String)
         */
        public void appendJavaScript(final String javascript) {
            synchronized (currentTrigger) {
                currentTrigger.addCall(APPEND_JAVASCRIPT_METHOD, new Object[] { javascript });
            }
        }

        /**
         * @see IAjaxPushBehavior#focusComponent(Component)
         */
        public void focusComponent(final Component component) {
            synchronized (currentTrigger) {
                currentTrigger.addCall(FOCUS_COMPONENT_METHOD, new Object[] { component });
            }
        }

        /**
         * @see IAjaxPushBehavior#prependJavaScript(String)
         */
        public void prependJavaScript(final String javascript) {
            synchronized (currentTrigger) {
                currentTrigger.addCall(PREPEND_JAVASCRIPT_METHOD, new Object[] { javascript });
            }
        }

        /**
         * @see IAjaxPushBehavior#trigger()
         */
        public void trigger() {
            DelayedMethodCallList trigger = null;
            synchronized (currentTrigger) {
                if (currentTrigger.isEmpty()) {
                    return;
                }
                trigger = new DelayedMethodCallList(currentTrigger);
                currentTrigger.clear();
            }
            final List<DelayedMethodCallList> triggers = getTriggers();
            synchronized (triggers) {
                triggers.add(trigger);
            }
        }

        public boolean isConnected() {
            return TimerChannelBehavior.isConnected(application, id, timeout);
        }

        /**
         * Methods used to access the triggers queued for the the behavior to
         * which this target corresponds.
         * 
         * @return a List of triggers queued for the current component
         */
        private List<DelayedMethodCallList> getTriggers() {
            return TimerChannelBehavior.getTriggers(application, id);
        }
    }

    private final String id;
    private final Duration timeout;

    /**
     * Construct a TimerChannelBehavior which actually refreshes the clients by
     * polling the server for changes at the given duration.
     * 
     * @param updateInterval
     *            the interval at which the server should be polled for changes
     */
    public TimerChannelBehavior(final Duration updateInterval) {
        this(updateInterval, updateInterval.add(TIMEOUT_MARGIN));
    }

    /**
     * Construct a TimerChannelBehavior which actually refreshes the clients by
     * polling the server for changes at the given duration.
     * 
     * @param updateInterval
     *            the interval at which the server should be polled for changes
     */
    public TimerChannelBehavior(final Duration updateInterval, final Duration timeout) {
        super(updateInterval);
        id = String.valueOf(COUNTER.incrementAndGet());
        this.timeout = timeout;
    }

    @Override
    protected void onBind() {
        super.onBind();
        touch(getComponent().getApplication(), id);
    }

    /**
     * @see AbstractAjaxTimerBehavior#onTimer(AjaxRequestTarget)
     */
    @Override
    protected void onTimer(final AjaxRequestTarget target) {
        touch(getComponent().getApplication(), id);
        final List<DelayedMethodCallList> triggers = getTriggers(getComponent().getApplication(), id);
        List<DelayedMethodCallList> triggersCopy;
        synchronized (triggers) {
            if (triggers.isEmpty()) {
                return;
            }
            triggersCopy = new ArrayList<DelayedMethodCallList>(triggers);
            triggers.clear();
        }
        for (final DelayedMethodCallList dmcl : triggersCopy) {
            try {
                dmcl.invoke(target);
            } catch (final Exception e) {
                throw new WicketRuntimeException("a problem occured while adding events to AjaxRequestTarget", e);
            }
        }
    }

    /**
     * Creates a new push target to which triggers can be sent
     * 
     * @return an IPushTarget to which triggers can be sent in any thread.
     */
    public IPushTarget newPushTarget() {
        return new TimerPushTarget(Application.get(), id, timeout);
    }

    public void renderHead(Component component, IHeaderResponse response) {
        touch(getComponent().getApplication(), id);
        final String timerChannelPageId = getComponent().getPage().getId() + ":updateInterval:"
                + getUpdateInterval();
        if (!getPageId(getComponent().getApplication(), id).equals(id)) {
            // behavior has already been redirected, we can skip this rendering
            return;
        }
        if (!response.wasRendered(timerChannelPageId)) {
            super.renderHead(component, response);
            setRedirectId(getComponent().getApplication(), timerChannelPageId, id);
            response.markRendered(timerChannelPageId);
        } else {
            /*
             * A similar behavior has already been rendered, we have no need to
             * render ourself All we need is redirect our own behavior id to the
             * id of the behavior which has been rendered.
             */
            final String redirectedId = getPageId(getComponent().getApplication(), timerChannelPageId);
            setRedirectId(getComponent().getApplication(), id, redirectedId);
        }
    }

    /**
     * Meta data key for queued triggers, stored by page behavior id
     */
    static final MetaDataKey<ConcurrentMap<String, List<DelayedMethodCallList>>> TRIGGERS_KEY = new MetaDataKey<ConcurrentMap<String, List<DelayedMethodCallList>>>() {
        private static final long serialVersionUID = 1L;
    };

    /**
     * Meta data key for poll events time, stored by page behavior id
     */
    static final MetaDataKey<ConcurrentMap<String, Time>> EVENTS_KEY = new MetaDataKey<ConcurrentMap<String, Time>>() {
        private static final long serialVersionUID = 1L;
    };

    /**
     * Meta data key for page behavior ids, stored by behavior id
     */
    static final MetaDataKey<ConcurrentMap<String, String>> PAGE_ID_KEY = new MetaDataKey<ConcurrentMap<String, String>>() {
        private static final long serialVersionUID = 1L;
    };

    public static boolean isConnected(final Application application, final String id, final Duration timeout) {
        final Time time = TimerChannelBehavior.getLastPollEvent(application, id);
        boolean isConnected;
        if (time == null) {
            // the behavior has been cleaned
            return false;
        }
        isConnected = time.elapsedSince().compareTo(timeout) < 0;
        if (!isConnected) {
            // timeout expired, the page is probably not connected anymore

            // we clean the metadata to avoid memory leak
            TimerChannelBehavior.cleanMetadata(application, id);
        }
        return isConnected;
    }

    /**
     * Methods used to access the triggers queued for the behavior
     * 
     * The implementation uses a Map stored in the application, where the
     * behavior id is the key, because these triggers cannot be stored in
     * component instance or the behavior itself, since they may be serialized
     * and deserialized.
     * 
     * @param application
     *            the application in which the triggers are stored
     * @param id
     *            the id of the behavior
     * 
     * @return a List of triggers queued for the component
     */
    private static List<DelayedMethodCallList> getTriggers(final Application application, String id) {
        id = getPageId(application, id);
        ConcurrentMap<String, List<DelayedMethodCallList>> triggersById;
        synchronized (application) {
            triggersById = application.getMetaData(TRIGGERS_KEY);
            if (triggersById == null) {
                triggersById = new ConcurrentHashMap<String, List<DelayedMethodCallList>>();
                application.setMetaData(TRIGGERS_KEY, triggersById);
            }
        }
        List<DelayedMethodCallList> triggers = triggersById.get(id);
        if (triggers == null) {
            triggersById.putIfAbsent(id, new ArrayList<DelayedMethodCallList>());
            triggers = triggersById.get(id);
        }
        return triggers;
    }

    /**
     * Cleans the metadata (triggers, poll time) associated with a given
     * behavior id
     * 
     * @param application
     *            the application in which the metadata are stored
     * @param id
     *            the id of the behavior
     */
    private static void cleanMetadata(final Application application, String id) {
        id = getPageId(application, id);
        ConcurrentMap<String, List<DelayedMethodCallList>> triggersById = null;
        ConcurrentMap<String, Time> eventsTimeById = null;
        ConcurrentMap<String, String> pageIdsById = null;
        synchronized (application) {
            triggersById = application.getMetaData(TRIGGERS_KEY);
            eventsTimeById = application.getMetaData(EVENTS_KEY);
            pageIdsById = application.getMetaData(PAGE_ID_KEY);
        }
        if (triggersById != null) {
            final List<DelayedMethodCallList> triggers = triggersById.remove(id);
            if (triggers != null) {
                synchronized (triggers) {
                    triggers.clear();
                }
            }
        }
        if (eventsTimeById != null) {
            eventsTimeById.remove(id);
        }
        if (pageIdsById != null) {
            pageIdsById.remove(id);
        }
    }

    private static void touch(final Application application, String id) {
        id = getPageId(application, id);
        ConcurrentMap<String, Time> eventsTimeById;
        synchronized (application) {
            eventsTimeById = application.getMetaData(EVENTS_KEY);
            if (eventsTimeById == null) {
                eventsTimeById = new ConcurrentHashMap<String, Time>();
                application.setMetaData(EVENTS_KEY, eventsTimeById);
            }
        }
        eventsTimeById.put(id, Time.now());
    }

    private static Time getLastPollEvent(final Application application, String id) {
        id = getPageId(application, id);
        ConcurrentMap<String, Time> eventsTimeById;
        synchronized (application) {
            eventsTimeById = application.getMetaData(EVENTS_KEY);
            if (eventsTimeById == null) {
                return null;
            }
        }
        final Time time = eventsTimeById.get(id);
        return time;
    }

    /**
     * Returns the page behavior id corresponding the given behavior id. Only
     * one behavior is actually rendered on a page for the same updateInterval,
     * to optimize the number of requests. Therefore all timer channel behaviors
     * of the same page are redirected to the same id, using this method.
     * 
     * @param application
     *            the wicket application to which the behavior belong
     * @param id
     *            the id of the behavior for which the page behavior id should
     *            be found
     * @return the page behavior id corresponding the given behavior id.
     */
    private static String getPageId(final Application application, final String id) {
        ConcurrentMap<String, String> pageIdsById;
        synchronized (application) {
            pageIdsById = application.getMetaData(PAGE_ID_KEY);
            if (pageIdsById == null) {
                return id;
            }
        }
        final String pageId = pageIdsById.get(id);
        return pageId == null ? id : pageId;
    }

    private static void setRedirectId(final Application application, final String id, final String redirectedId) {
        ConcurrentMap<String, String> pageIdsById;
        synchronized (application) {
            pageIdsById = application.getMetaData(PAGE_ID_KEY);
            if (pageIdsById == null) {
                pageIdsById = new ConcurrentHashMap<String, String>();
                application.setMetaData(PAGE_ID_KEY, pageIdsById);
            }
        }
        final String oldRedirectedId = pageIdsById.put(id, redirectedId);
        if (!redirectedId.equals(oldRedirectedId)) {
            /*
             * The id was not already redirected to the redirectedId, we need to
             * merge the information before redirection with information after
             * redirection
             */
            final String idToRedirect = oldRedirectedId == null ? id : oldRedirectedId;
            redirect(application, idToRedirect, redirectedId);
        }
    }

    private static void redirect(final Application application, final String idToRedirect,
            final String redirectedId) {
        ConcurrentMap<String, List<DelayedMethodCallList>> triggersById = null;
        ConcurrentMap<String, Time> eventsTimeById = null;
        synchronized (application) {
            triggersById = application.getMetaData(TRIGGERS_KEY);
            eventsTimeById = application.getMetaData(EVENTS_KEY);
        }
        if (triggersById != null) {
            final List<DelayedMethodCallList> triggersToRedirect = triggersById.remove(idToRedirect);
            if (triggersToRedirect != null) {
                // we redirect triggers to the new list, in two steps, to avoid
                // acquiring
                // locks on two triggers simultaneously, which would be a source
                // of risk of
                // dead locks
                List<DelayedMethodCallList> triggersToRedirectCopy;
                synchronized (triggersToRedirect) {
                    triggersToRedirectCopy = new ArrayList<DelayedMethodCallList>(triggersToRedirect);
                    triggersToRedirect.clear();
                }
                if (!triggersToRedirectCopy.isEmpty()) {
                    final List<DelayedMethodCallList> triggers = getTriggers(application, redirectedId);
                    synchronized (triggers) {
                        triggers.addAll(triggersToRedirectCopy);
                    }
                }
            }
        }
        if (eventsTimeById != null) {
            eventsTimeById.remove(idToRedirect);
            /*
             * we don't need to merge touch information, since merged behaviors
             * always have the same touch rates
             */
        }
    }

    @Override
    public String toString() {
        return "TimerChannelBehavior::" + id;
    }

    public String getId() {
        return id;
    }
}