001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.ajax;
018
019import java.time.Duration;
020import org.apache.wicket.Component;
021import org.apache.wicket.Page;
022import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
023import org.apache.wicket.markup.head.IHeaderResponse;
024import org.apache.wicket.markup.head.OnLoadHeaderItem;
025import org.apache.wicket.util.lang.Args;
026import org.danekja.java.util.function.serializable.SerializableConsumer;
027
028/**
029 * A behavior that generates an AJAX update callback at a regular interval.
030 * 
031 * @since 1.2
032 * 
033 * @author Igor Vaynberg (ivaynberg)
034 * 
035 * @see #onTimer(AjaxRequestTarget)
036 * @see #restart(IPartialPageRequestHandler)
037 * @see #stop(IPartialPageRequestHandler)
038 */
039public abstract class AbstractAjaxTimerBehavior extends AbstractDefaultAjaxBehavior
040{
041        private static final long serialVersionUID = 1L;
042
043        /** The update interval */
044        private Duration updateInterval;
045
046        private boolean stopped = false;
047        
048        /**
049         * Id of timer in JavaScript.
050         */
051        private String timerId;
052
053        /**
054         * Construct.
055         *
056         * @param updateInterval
057         *            {@link Duration} between AJAX callbacks
058         */
059        public AbstractAjaxTimerBehavior(final Duration updateInterval)
060        {
061                setUpdateInterval(updateInterval);
062        }
063
064        /**
065         * Sets the update interval duration. This method should only be called within the
066         * {@link #onTimer(AjaxRequestTarget)} method.
067         * 
068         * @param updateInterval
069         */
070        protected final void setUpdateInterval(Duration updateInterval)
071        {
072                if (updateInterval == null || updateInterval.toMillis() <= 0)
073                {
074                        throw new IllegalArgumentException("Invalid update interval");
075                }
076                this.updateInterval = updateInterval;
077        }
078
079        /**
080         * Returns the update interval
081         * 
082         * @return The update interval
083         */
084        public final Duration getUpdateInterval()
085        {
086                return updateInterval;
087        }
088
089        @Override
090        public void renderHead(Component component, IHeaderResponse response)
091        {
092                super.renderHead(component, response);
093
094                if (isStopped() == false)
095                {
096                        setTimeout(response);
097                }
098        }
099
100        @Override
101        protected final void respond(final AjaxRequestTarget target)
102        {
103                // timerId is no longer valid after timer has triggered
104                timerId = null;
105                
106                if (shouldTrigger())
107                {
108                        onTimer(target);
109
110                        if (shouldTrigger())
111                        {
112                                setTimeout(target.getHeaderResponse());
113                        }
114                }
115        }
116
117        /**
118         * Decides whether the timer behavior should render its JavaScript to re-trigger it after the
119         * update interval.
120         *
121         * @return {@code true} if the behavior is not stopped, it is enabled and still attached to any
122         *         component in the page or to the page itself
123         */
124        protected boolean shouldTrigger()
125        {
126                return isStopped() == false &&
127                                isEnabled(getComponent()) &&
128                                (getComponent() instanceof Page || getComponent().findParent(Page.class) != null);
129        }
130
131        /**
132         * Listener method for the AJAX timer event.
133         * 
134         * @param target
135         *            The request target
136         */
137        protected abstract void onTimer(final AjaxRequestTarget target);
138
139        /**
140         * @return {@code true} if has been stopped via {@link #stop(IPartialPageRequestHandler)}
141         */
142        public final boolean isStopped()
143        {
144                return stopped;
145        }
146
147        /**
148         * Restart the timer.
149         * 
150         * @param target
151         *            may be null
152         */
153        public final void restart(final IPartialPageRequestHandler target)
154        {
155                stopped = false;
156
157                if (target != null)
158                {
159                        setTimeout(target.getHeaderResponse());
160                }
161        }
162
163        /**
164         * Create an identifier for the JavaScript timer.
165         * <p>
166         * Note: The identifier must not change as long as this behavior is attached to a component!
167         * 
168         * @return creates an id based on {@link Component#getMarkupId()} and
169         *         {@link Component#getBehaviorById(int)} by default
170         */
171        protected String getTimerId()
172        {
173                Component component = getComponent();
174
175                return component.getMarkupId() + "." + component.getBehaviorId(this);
176        }
177
178        /**
179         * Set the timeout on the given {@link IHeaderResponse}. Implementation note:
180         * <p>
181         * {@link #respond(AjaxRequestTarget)} might set the timer once and
182         * {@link #renderHead(Component, IHeaderResponse)} a second time successively, if the attached
183         * component is re-rendered on the same {@link AjaxRequestTarget}.
184         * <p>
185         * But rendering of the component might <em>not</em> actually happen on the same {@link AjaxRequestTarget},
186         * e.g. when a redirect to a full page-render is scheduled. Thus this method <em>always</em> sets the timeout 
187         * and in the former case {@link AjaxRequestTarget} will take care of executing one of the
188         * two {@link OnLoadHeaderItem}s only.
189         * 
190         * @param headerResponse
191         */
192        private void setTimeout(IHeaderResponse headerResponse)
193        {
194                CharSequence js = getCallbackScript();
195
196                // remember id to be able to clear it later
197                timerId = getTimerId();
198
199                headerResponse.render(
200                        OnLoadHeaderItem.forScript("Wicket.Timer.set('" + timerId + "', function(){" + js + "}, " + updateInterval.toMillis() + ");"));
201        }
202
203        private void clearTimeout(IHeaderResponse headerResponse)
204        {
205                if (timerId != null)
206                {
207                        headerResponse
208                                .render(OnLoadHeaderItem.forScript("Wicket.Timer.clear('" + timerId + "');"));
209
210                        timerId = null;
211                }
212        }
213
214        /**
215         * Stops the timer.
216         * 
217         * @param target
218         *            may be null
219         */
220        public final void stop(final IPartialPageRequestHandler target)
221        {
222                if (stopped == false)
223                {
224                        stopped = true;
225
226                        if (target != null)
227                        {
228                                clearTimeout(target.getHeaderResponse());
229                        }
230                }
231        }
232
233        @Override
234        public void onRemove(Component component)
235        {
236                component.getRequestCycle().find(IPartialPageRequestHandler.class).ifPresent(target -> clearTimeout(target.getHeaderResponse()));
237        }
238
239        @Override
240        protected void onUnbind()
241        {
242                Component component = getComponent();
243                
244                component.getRequestCycle().find(IPartialPageRequestHandler.class).ifPresent(target -> clearTimeout(target.getHeaderResponse()));
245        }
246
247        /**
248         * Creates an {@link AbstractAjaxTimerBehavior} based on lambda expressions
249         *
250         * @param interval
251         *            the interval the timer
252         * @param onTimer
253         *            the consumer which accepts the {@link AjaxRequestTarget}
254         * @return the {@link AbstractAjaxTimerBehavior}
255         */
256        public static AbstractAjaxTimerBehavior onTimer(Duration interval, SerializableConsumer<AjaxRequestTarget> onTimer)
257        {
258                Args.notNull(onTimer, "onTimer");
259
260                return new AbstractAjaxTimerBehavior(interval)
261                {
262                        private static final long serialVersionUID = 1L;
263
264                        @Override
265                        protected void onTimer(AjaxRequestTarget target)
266                        {
267                                onTimer.accept(target);
268                        }
269                };
270        }
271}