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}