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 org.apache.wicket.util.time.Duration} between AJAX callbacks
058         *
059         * @deprecated Since Wicket 9 this constructor is deprecated. It will be removed in Wicket 10. Use {@link AbstractAjaxTimerBehavior#AbstractAjaxTimerBehavior(Duration)} instead
060         */
061        @Deprecated
062        public AbstractAjaxTimerBehavior(final org.apache.wicket.util.time.Duration updateInterval)
063        {
064                setUpdateInterval(updateInterval.toJavaDuration());
065        }
066
067        /**
068         * Construct.
069         *
070         * @param updateInterval
071         *            {@link Duration} between AJAX callbacks
072         */
073        public AbstractAjaxTimerBehavior(final Duration updateInterval)
074        {
075                setUpdateInterval(updateInterval);
076        }
077
078        /**
079         * Sets the update interval duration. This method should only be called within the
080         * {@link #onTimer(AjaxRequestTarget)} method.
081         * 
082         * @param updateInterval
083         */
084        protected final void setUpdateInterval(Duration updateInterval)
085        {
086                if (updateInterval == null || updateInterval.toMillis() <= 0)
087                {
088                        throw new IllegalArgumentException("Invalid update interval");
089                }
090                this.updateInterval = updateInterval;
091        }
092
093        /**
094         * Returns the update interval
095         * 
096         * @return The update interval
097         */
098        public final Duration getUpdateInterval()
099        {
100                return updateInterval;
101        }
102
103        @Override
104        public void renderHead(Component component, IHeaderResponse response)
105        {
106                super.renderHead(component, response);
107
108                if (isStopped() == false)
109                {
110                        setTimeout(response);
111                }
112        }
113
114        @Override
115        protected final void respond(final AjaxRequestTarget target)
116        {
117                // timerId is no longer valid after timer has triggered
118                timerId = null;
119                
120                if (shouldTrigger())
121                {
122                        onTimer(target);
123
124                        if (shouldTrigger())
125                        {
126                                setTimeout(target.getHeaderResponse());
127                        }
128                }
129        }
130
131        /**
132         * Decides whether the timer behavior should render its JavaScript to re-trigger it after the
133         * update interval.
134         *
135         * @return {@code true} if the behavior is not stopped, it is enabled and still attached to any
136         *         component in the page or to the page itself
137         */
138        protected boolean shouldTrigger()
139        {
140                return isStopped() == false &&
141                                isEnabled(getComponent()) &&
142                                (getComponent() instanceof Page || getComponent().findParent(Page.class) != null);
143        }
144
145        /**
146         * Listener method for the AJAX timer event.
147         * 
148         * @param target
149         *            The request target
150         */
151        protected abstract void onTimer(final AjaxRequestTarget target);
152
153        /**
154         * @return {@code true} if has been stopped via {@link #stop(IPartialPageRequestHandler)}
155         */
156        public final boolean isStopped()
157        {
158                return stopped;
159        }
160
161        /**
162         * Restart the timer.
163         * 
164         * @param target
165         *            may be null
166         */
167        public final void restart(final IPartialPageRequestHandler target)
168        {
169                stopped = false;
170
171                if (target != null)
172                {
173                        setTimeout(target.getHeaderResponse());
174                }
175        }
176
177        /**
178         * Create an identifier for the JavaScript timer.
179         * <p>
180         * Note: The identifier must not change as long as this behavior is attached to a component!
181         * 
182         * @return creates an id based on {@link Component#getMarkupId()} and
183         *         {@link Component#getBehaviorById(int)} by default
184         */
185        protected String getTimerId()
186        {
187                Component component = getComponent();
188
189                return component.getMarkupId() + "." + component.getBehaviorId(this);
190        }
191
192        /**
193         * Set the timeout on the given {@link IHeaderResponse}. Implementation note:
194         * <p>
195         * {@link #respond(AjaxRequestTarget)} might set the timer once and
196         * {@link #renderHead(Component, IHeaderResponse)} a second time successively, if the attached
197         * component is re-rendered on the same {@link AjaxRequestTarget}.
198         * <p>
199         * But rendering of the component might <em>not</em> actually happen on the same {@link AjaxRequestTarget},
200         * e.g. when a redirect to a full page-render is scheduled. Thus this method <em>always</em> sets the timeout 
201         * and in the former case {@link AjaxRequestTarget} will take care of executing one of the
202         * two {@link OnLoadHeaderItem}s only.
203         * 
204         * @param headerResponse
205         */
206        private void setTimeout(IHeaderResponse headerResponse)
207        {
208                CharSequence js = getCallbackScript();
209
210                // remember id to be able to clear it later
211                timerId = getTimerId();
212
213                headerResponse.render(
214                        OnLoadHeaderItem.forScript("Wicket.Timer.set('" + timerId + "', function(){" + js + "}, " + updateInterval.toMillis() + ");"));
215        }
216
217        private void clearTimeout(IHeaderResponse headerResponse)
218        {
219                if (timerId != null)
220                {
221                        headerResponse
222                                .render(OnLoadHeaderItem.forScript("Wicket.Timer.clear('" + timerId + "');"));
223
224                        timerId = null;
225                }
226        }
227
228        /**
229         * Stops the timer.
230         * 
231         * @param target
232         *            may be null
233         */
234        public final void stop(final IPartialPageRequestHandler target)
235        {
236                if (stopped == false)
237                {
238                        stopped = true;
239
240                        if (target != null)
241                        {
242                                clearTimeout(target.getHeaderResponse());
243                        }
244                }
245        }
246
247        @Override
248        public void onRemove(Component component)
249        {
250                component.getRequestCycle().find(IPartialPageRequestHandler.class).ifPresent(target -> clearTimeout(target.getHeaderResponse()));
251        }
252
253        @Override
254        protected void onUnbind()
255        {
256                Component component = getComponent();
257                
258                component.getRequestCycle().find(IPartialPageRequestHandler.class).ifPresent(target -> clearTimeout(target.getHeaderResponse()));
259        }
260
261        /**
262         * Creates an {@link AbstractAjaxTimerBehavior} based on lambda expressions
263         *
264         * @param interval
265         *            the interval the timer
266         * @param onTimer
267         *            the consumer which accepts the {@link AjaxRequestTarget}
268         * @return the {@link AbstractAjaxTimerBehavior}
269         */
270        public static AbstractAjaxTimerBehavior onTimer(Duration interval, SerializableConsumer<AjaxRequestTarget> onTimer)
271        {
272                Args.notNull(onTimer, "onTimer");
273
274                return new AbstractAjaxTimerBehavior(interval)
275                {
276                        private static final long serialVersionUID = 1L;
277
278                        @Override
279                        protected void onTimer(AjaxRequestTarget target)
280                        {
281                                onTimer.accept(target);
282                        }
283                };
284        }
285}