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}