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.extensions.ajax.markup.html; 018 019import java.io.Serial; 020import java.time.Duration; 021import java.util.List; 022import java.util.Optional; 023import org.apache.wicket.Component; 024import org.apache.wicket.ajax.AbstractAjaxTimerBehavior; 025import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior; 026import org.apache.wicket.ajax.AjaxRequestTarget; 027import org.apache.wicket.markup.html.basic.Label; 028import org.apache.wicket.markup.html.panel.Panel; 029import org.apache.wicket.model.IModel; 030import org.apache.wicket.request.IRequestHandler; 031import org.apache.wicket.request.cycle.RequestCycle; 032import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler; 033import org.apache.wicket.util.lang.Comparators; 034import org.apache.wicket.util.visit.IVisit; 035import org.apache.wicket.util.visit.IVisitor; 036 037/** 038 * A panel which load lazily a single content component. This can be used if you have a 039 * component that is pretty heavy in creation and you first want to show the user the page and 040 * then replace the panel when it is ready. 041 * <p> 042 * This panel will wait with adding the content until {@link #isContentReady()} returns 043 * {@code true}. It will poll using an {@link AbstractAjaxTimerBehavior} that is installed on the page. When the 044 * component is replaced, the timer stops. When you have multiple {@code AjaxLazyLoadPanel}s on the 045 * same page, only one timer is used and all panels piggyback on this single timer. 046 * <p> 047 * This component will also replace the contents when a normal request comes through and the 048 * content is ready. 049 * 050 * @since 1.3 051 */ 052public abstract class AjaxLazyLoadPanel<T extends Component> extends Panel 053{ 054 private static final long serialVersionUID = 1L; 055 056 /** 057 * The component id which will be used to load the lazily loaded component. 058 */ 059 private static final String CONTENT_ID = "content"; 060 061 private boolean loaded; 062 063 /** 064 * Constructor 065 * 066 * @param id 067 */ 068 public AjaxLazyLoadPanel(final String id) 069 { 070 this(id, null); 071 } 072 073 /** 074 * Constructor 075 * 076 * @param id 077 * @param model 078 */ 079 public AjaxLazyLoadPanel(final String id, final IModel<?> model) 080 { 081 super(id, model); 082 083 setOutputMarkupId(true); 084 } 085 086 /** 087 * Determines that the content we're waiting for is ready, typically used in polling background 088 * threads for their result. Override this to implement your own check. 089 * <p> 090 * This default implementation returns {@code true}, i.e. assuming the content is ready immediately. 091 * 092 * @return whether the actual content is ready 093 */ 094 protected boolean isContentReady() 095 { 096 return true; 097 } 098 099 /** 100 * Create a loading component shown instead of the actual content until it is {@link #isContentReady()}. 101 * 102 * @param id 103 * The components id 104 * @return The component to show while the real content isn't ready yet 105 */ 106 public Component getLoadingComponent(final String id) 107 { 108 IRequestHandler handler = new ResourceReferenceRequestHandler( 109 AbstractDefaultAjaxBehavior.INDICATOR); 110 return new Label(id, 111 "<img alt=\"Loading...\" src=\"" + RequestCycle.get().urlFor(handler) + "\"/>") 112 .setEscapeModelStrings(false); 113 } 114 115 /** 116 * Factory method for creating the lazily loaded content that replaces the loading component after 117 * {@link #isContentReady()} returns {@code true}. You may call setRenderBodyOnly(true) 118 * on this component if you need the body only. 119 * 120 * @param markupId 121 * The components markupid. 122 * @return the content to show after {@link #isContentReady()} 123 */ 124 public abstract T getLazyLoadComponent(String markupId); 125 126 /** 127 * Called after the loading component was replaced with the lazy loaded content. 128 * <p> 129 * This default implementation does nothing. 130 * 131 * @param content 132 * The lazy loaded content 133 * @param target 134 * optional Ajax request handler 135 */ 136 protected void onContentLoaded(T content, Optional<AjaxRequestTarget> target) 137 { 138 } 139 140 /** 141 * Installs a page-global timer if not already present. 142 */ 143 @Override 144 protected void onBeforeRender() 145 { 146 super.onBeforeRender(); 147 148 if (loaded == false) { 149 initTimer(); 150 } 151 } 152 153 /** 154 * Initialize a timer - default implementation installs an {@link AbstractAjaxTimerBehavior} on the page, 155 * if it is not already present. 156 */ 157 protected void initTimer() 158 { 159 // when the timer is not yet installed add it 160 List<AjaxLazyLoadTimer> behaviors = getPage().getBehaviors(AjaxLazyLoadTimer.class); 161 if (behaviors.isEmpty()) { 162 AbstractAjaxTimerBehavior timer = new AjaxLazyLoadTimer(); 163 getPage().add(timer); 164 165 getRequestCycle().find(AjaxRequestTarget.class).ifPresent(target -> { 166 // the timer will not be rendered, so restart it immediately on the Ajax target 167 timer.restart(target); 168 }); 169 } 170 } 171 172 @Override 173 protected void onConfigure() 174 { 175 super.onConfigure(); 176 177 if (get(CONTENT_ID) == null) { 178 add(getLoadingComponent(CONTENT_ID)); 179 } 180 } 181 182 /** 183 * Get the preferred interval for updates. 184 * <p> 185 * Since all LazyLoadingPanels on a page share the same Ajax timer, its update interval 186 * is derived from the minimum of all panel's update intervals. 187 * 188 * @return update interval, must not be {@code null} 189 */ 190 public Duration getUpdateInterval() { 191 return Duration.ofSeconds(1); 192 } 193 194 /** 195 * Check whether the content is loaded. 196 * <p> 197 * If not loaded already and the content is ready, replaces the lazy loading component with 198 * the lazily loaded content. 199 * 200 * @return {@code true} if content is loaded 201 * 202 * @see #isContentReady() 203 */ 204 public final boolean isLoaded() { 205 if (loaded == false) 206 { 207 if (isContentReady()) 208 { 209 loaded = true; 210 211 // create the lazy load component 212 T content = getLazyLoadComponent(CONTENT_ID); 213 214 // replace the loading component with the new component 215 // note: use addOrReplace(), since onConfigure() might not have been called yet 216 AjaxLazyLoadPanel.this.addOrReplace(content); 217 218 Optional<AjaxRequestTarget> target = getRequestCycle().find(AjaxRequestTarget.class); 219 220 // notify our subclasses of the updated component 221 onContentLoaded(content, target); 222 223 // repaint our selves if there's an AJAX request in play, otherwise let the page 224 // redraw itself 225 target.ifPresent(t -> t.add(AjaxLazyLoadPanel.this)); 226 } 227 } 228 229 return loaded; 230 } 231 232 /** 233 * The AJAX timer for updating the AjaxLazyLoadPanel. Is designed to be a page-local singleton 234 * running as long as LazyLoadPanels are still loading. 235 * 236 * @see AjaxLazyLoadPanel#isLoaded() 237 */ 238 public static class AjaxLazyLoadTimer extends AbstractAjaxTimerBehavior 239 { 240 @Serial 241 private static final long serialVersionUID = 1L; 242 243 public AjaxLazyLoadTimer() 244 { 245 super(Duration.ofSeconds(1)); 246 } 247 248 @Override 249 protected void onTimer(AjaxRequestTarget target) 250 { 251 load(target); 252 } 253 254 public void load(AjaxRequestTarget target) 255 { 256 setUpdateInterval(Duration.ofMillis(Long.MAX_VALUE)); 257 258 getComponent().getPage().visitChildren(AjaxLazyLoadPanel.class, new IVisitor<AjaxLazyLoadPanel<?>, Void>() 259 { 260 @Override 261 public void component(AjaxLazyLoadPanel<?> panel, IVisit<Void> visit) 262 { 263 if (panel.isVisibleInHierarchy() && panel.isLoaded() == false) { 264 Duration updateInterval = panel.getUpdateInterval(); 265 if (getUpdateInterval() == null) { 266 throw new IllegalArgumentException("update interval must not ben null"); 267 } 268 269 setUpdateInterval(Comparators.min(getUpdateInterval(), updateInterval)); 270 } 271 } 272 }); 273 274 // all panels have completed their replacements, we can stop the timer 275 if (Duration.ofMillis(Long.MAX_VALUE).equals(getUpdateInterval())) 276 { 277 stop(target); 278 279 getComponent().remove(this); 280 } 281 } 282 } 283}