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}