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