All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.wicketstuff.chat.channel.TimerChannelBehavior Maven / Gradle / Ivy

The newest version!
/*
 * 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.util.time.Duration;
import org.apache.wicket.util.time.Time;
import org.wicketstuff.chat.channel.api.IPushTarget;

/**
 * Behavior used to enqueue triggers and send them to the client using timer based polling.
 * 

* 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. *

* 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 * */ public class TimerChannelBehavior extends AbstractAjaxTimerBehavior implements Serializable { /** * 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 { /** * 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); } } } private static final long serialVersionUID = 1L; private final Application _application; /** * stores the list of {@link DelayedMethodCall} to invoke */ private final List calls; /** * Construct. */ public DelayedMethodCallList() { _application = Application.get(); calls = new ArrayList(); } /** * Construct a copy of the given {@link DelayedMethodCallList}. * * @param dmcl */ public DelayedMethodCallList(final DelayedMethodCallList dmcl) { _application = Application.get(); calls = new ArrayList(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)); } /** * Used to remove all the delayed methods from this list */ public void clear() { calls.clear(); } /** * 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(); } } /** * 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; } /** * Adds the component. * * @param component * the component */ @Override public void addComponent(final Component component) { synchronized (currentTrigger) { currentTrigger.addCall(ADD_COMPONENT_METHOD, new Object[] { component }); } } /** * Adds the component. * * @param component * the component * @param markupId * the markup id */ @Override public void addComponent(final Component component, final String markupId) { synchronized (currentTrigger) { currentTrigger.addCall(ADD_COMPONENT_WITH_MARKUP_ID_METHOD, new Object[] { component, markupId }); } } /** * Append java script. * * @param javascript * the javascript */ @Override public void appendJavaScript(final String javascript) { synchronized (currentTrigger) { currentTrigger.addCall(APPEND_JAVASCRIPT_METHOD, new Object[] { javascript }); } } /** * Focus component. * * @param component * the component */ @Override public void focusComponent(final Component component) { synchronized (currentTrigger) { currentTrigger.addCall(FOCUS_COMPONENT_METHOD, new Object[] { component }); } } /** * 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 getTriggers() { return TimerChannelBehavior.getTriggers(application, id); } @Override public boolean isConnected() { return TimerChannelBehavior.isConnected(application, id, timeout); } /** * Prepend java script. * * @param javascript * the javascript */ @Override public void prependJavaScript(final String javascript) { synchronized (currentTrigger) { currentTrigger.addCall(PREPEND_JAVASCRIPT_METHOD, new Object[] { javascript }); } } /** * Trigger. */ @Override public void trigger() { DelayedMethodCallList trigger = null; synchronized (currentTrigger) { if (currentTrigger.isEmpty()) { return; } trigger = new DelayedMethodCallList(currentTrigger); currentTrigger.clear(); } final List triggers = getTriggers(); synchronized (triggers) { triggers.add(trigger); } } } 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); } } /** * Meta data key for queued triggers, stored by page behavior id */ static final MetaDataKey>> TRIGGERS_KEY = new MetaDataKey>>() { private static final long serialVersionUID = 1L; }; /** * Meta data key for poll events time, stored by page behavior id */ static final MetaDataKey> EVENTS_KEY = new MetaDataKey>() { private static final long serialVersionUID = 1L; }; /** * Meta data key for page behavior ids, stored by behavior id */ static final MetaDataKey> PAGE_ID_KEY = new MetaDataKey>() { private static final long serialVersionUID = 1L; }; /** * 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> triggersById = null; ConcurrentMap eventsTimeById = null; ConcurrentMap 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 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 Time getLastPollEvent(final Application application, String id) { id = getPageId(application, id); ConcurrentMap 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 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; } /** * 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 getTriggers(final Application application, String id) { id = getPageId(application, id); ConcurrentMap> triggersById; synchronized (application) { triggersById = application.getMetaData(TRIGGERS_KEY); if (triggersById == null) { triggersById = new ConcurrentHashMap>(); application.setMetaData(TRIGGERS_KEY, triggersById); } } List triggers = triggersById.get(id); if (triggers == null) { triggersById.putIfAbsent(id, new ArrayList()); triggers = triggersById.get(id); } return triggers; } 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; } private static void redirect(final Application application, final String idToRedirect, final String redirectedId) { ConcurrentMap> triggersById = null; ConcurrentMap eventsTimeById = null; synchronized (application) { triggersById = application.getMetaData(TRIGGERS_KEY); eventsTimeById = application.getMetaData(EVENTS_KEY); } if (triggersById != null) { final List 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 triggersToRedirectCopy; synchronized (triggersToRedirect) { triggersToRedirectCopy = new ArrayList( triggersToRedirect); triggersToRedirect.clear(); } if (!triggersToRedirectCopy.isEmpty()) { final List 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 */ } } private static void setRedirectId(final Application application, final String id, final String redirectedId) { ConcurrentMap pageIdsById; synchronized (application) { pageIdsById = application.getMetaData(PAGE_ID_KEY); if (pageIdsById == null) { pageIdsById = new ConcurrentHashMap(); 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 touch(final Application application, String id) { id = getPageId(application, id); ConcurrentMap eventsTimeById; synchronized (application) { eventsTimeById = application.getMetaData(EVENTS_KEY); if (eventsTimeById == null) { eventsTimeById = new ConcurrentHashMap(); application.setMetaData(EVENTS_KEY, eventsTimeById); } } eventsTimeById.put(id, Time.now()); } 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 * @param timeout * The timeout to set */ public TimerChannelBehavior(final Duration updateInterval, final Duration timeout) { super(updateInterval); id = String.valueOf(COUNTER.incrementAndGet()); this.timeout = timeout; } public String getId() { return id; } /** * 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); } @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 triggers = getTriggers(getComponent().getApplication(), id); List triggersCopy; synchronized (triggers) { if (triggers.isEmpty()) { return; } triggersCopy = new ArrayList(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); } } } @Override public void renderHead(final Component component, final 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); } } @Override public String toString() { return "TimerChannelBehavior::" + id; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy