View Javadoc
1   /**
2    * This file Copyright (c) 2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.connector;
35  
36  import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.rpc.LazyLayoutClientRpc;
37  import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.rpc.LazyLayoutServerRpc;
38  import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.shared.ThumbnailData;
39  import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.widget.EscalatorPanel;
40  import info.magnolia.ui.vaadin.gwt.shared.Range;
41  import info.magnolia.ui.vaadin.layout.LazyLayout;
42  
43  import java.util.HashMap;
44  import java.util.LinkedList;
45  import java.util.List;
46  import java.util.Map;
47  import java.util.logging.Level;
48  import java.util.logging.Logger;
49  
50  import com.google.gwt.core.client.Scheduler;
51  import com.vaadin.client.communication.StateChangeEvent;
52  import com.vaadin.client.ui.AbstractComponentConnector;
53  
54  /**
55   * Connector for {@link LazyLayout}.
56   *
57   * TODO: should be merged with {@link LazyCardLayoutConnector} once 5.x UI is dropped.
58   */
59  public abstract class LazyLayoutConnector extends AbstractComponentConnector {
60  
61      protected static final int THUMBNAIL_QUERY_RPC_DELAY = 200;
62  
63      private static final Logger log = Logger.getLogger(LazyLayoutConnector.class.getSimpleName());
64  
65      protected Range cachedThumbnails = Range.between(0, 0);
66  
67      protected Map<Object, Integer> idToIndex = new HashMap<>();
68  
69      protected Map<Integer, ThumbnailData> indexToThumbnail = new HashMap<>();
70  
71      protected Map<String, String> idToUrl = new HashMap<>();
72  
73      private boolean widgetInitialized = false;
74  
75      protected boolean waitingData = false;
76  
77      @Override
78      protected void init() {
79          super.init();
80          registerRpc(LazyLayoutClientRpc.class, new LazyLayoutClientRpc() {
81              @Override
82              public void addElements(List<ThumbnailData> data, int startingFrom) {
83                  final Range received = Range.withLength(startingFrom, data.size());
84                  final Range maxCacheRange = getMaxCacheRange();
85                  final Range[] partition = received.partitionWith(maxCacheRange);
86                  final Range newUsefulData = partition[1];
87                  if (!newUsefulData.isEmpty()) {
88                      // Update the parts that are actually inside
89                      for (int i = newUsefulData.getStart(); i < newUsefulData.getEnd(); i++) {
90                          final ThumbnailData thumbnailData = data.get(i - startingFrom);
91                          if (thumbnailData.isRealResource()) {
92                              idToUrl.put(thumbnailData.getThumbnailId(), getResourceUrl(thumbnailData.getThumbnailId()));
93                          }
94  
95                          indexToThumbnail.put(i, thumbnailData);
96                          idToIndex.put(thumbnailData.getThumbnailId(), i);
97                      }
98  
99                      final Range toPushToWidget = newUsefulData.restrictTo(getWidget().getDisplayedRange());
100                     for (int i = toPushToWidget.getStart(); i < toPushToWidget.getEnd(); ++i) {
101                         updateThumbnailContentAtIndex(i);
102                     }
103 
104                     // Potentially extend the range
105                     if (cachedThumbnails.isEmpty()) {
106                         cachedThumbnails = newUsefulData;
107                     } else {
108                         purgeCache();
109                         if (!cachedThumbnails.isEmpty()) {
110                             cachedThumbnails = cachedThumbnails.combineWith(newUsefulData);
111                         } else {
112                             cachedThumbnails = newUsefulData;
113                         }
114                     }
115                 }
116 
117                 waitingData = false;
118 
119                 // Eventually check whether all needed rows are now available
120                 serveThumbnails();
121             }
122 
123             @Override
124             public void refresh() {
125                 refreshViewport();
126             }
127         });
128 
129         getLayoutManager().addElementResizeListener(getWidget().getElement(), e -> getWidget().resize());
130     }
131 
132     @Override
133     public void onStateChanged(StateChangeEvent stateChangeEvent) {
134         super.onStateChanged(stateChangeEvent);
135 
136         if (widgetInitialized && stateChangeEvent.hasPropertyChanged("size")) {
137             getWidget().initialize(getState().elementAmount, getState().offset, getState().size, getState().scaleRatio, getState().isFirstUpdate);
138         }
139 
140         if (widgetInitialized && stateChangeEvent.hasPropertyChanged("selection")) {
141             updateSelection();
142         }
143 
144         if (!widgetInitialized) {
145             Scheduler.get().scheduleDeferred(() -> {
146                 widgetInitialized = true;
147                 getWidget().initialize(getState().elementAmount, getState().offset, getState().size, getState().scaleRatio, getState().isFirstUpdate);
148                 updateSelection();
149             });
150         }
151     }
152 
153     protected void refreshViewport() {
154         // Clear caches and maps
155         this.cachedThumbnails = Range.between(0, 0);
156         this.idToUrl.clear();
157         this.idToIndex.clear();
158         this.indexToThumbnail.clear();
159 
160         // Reset thumbnail amount
161         getWidget().setThumbnailAmount(getState().elementAmount);
162 
163         // Previous call would trigger lazy update of the viewport, but we don't need it because we anyway
164         // are going to query the whole visible range => cancel the timer
165         cancelTimer();
166         queryThumbnails(getWidget().getDisplayedRange());
167     }
168 
169     abstract void cancelTimer();
170 
171     public EscalatorPanel getWidget() {
172         return (EscalatorPanel) super.getWidget();
173     }
174 
175     @Override
176     public ThumbnailLayoutState getState() {
177         return (ThumbnailLayoutState) super.getState();
178     }
179 
180     public void serveThumbnails() {
181         this.getLazyLayoutServerRpc().updateOffset(getWidget().getCurrentThumbnailOffset());
182 
183         if (waitingData) {
184             return;
185         }
186 
187         final Range newMinimumCachedRange = getMinCacheRange();
188         if (!newMinimumCachedRange.intersects(cachedThumbnails) || cachedThumbnails.isEmpty()) {
189             indexToThumbnail.clear();
190             idToIndex.clear();
191             idToUrl.clear();
192 
193             cachedThumbnails = Range.between(0, 0);
194 
195             queryThumbnails(getMaxCacheRange());
196             log.log(Level.FINEST, "Querying: " + getMaxCacheRange());
197         } else {
198             final Range intersection = newMinimumCachedRange.restrictTo(cachedThumbnails).restrictTo(getWidget().getDisplayedRange());
199             for (int i = intersection.getStart(); i < intersection.getEnd(); ++i) {
200                 updateThumbnailContentAtIndex(i);
201             }
202 
203             purgeCache();
204 
205             if (!newMinimumCachedRange.isSubsetOf(cachedThumbnails)) {
206                 final Range[] missingCachePartition = getMaxCacheRange().partitionWith(cachedThumbnails);
207                 queryThumbnails(missingCachePartition[0]);
208                 queryThumbnails(missingCachePartition[2]);
209                 log.log(Level.FINEST, "Querying: " + missingCachePartition[0] + " and " + missingCachePartition[2]);
210             }
211         }
212     }
213 
214     protected void purgeCache() {
215         final Range[] cachePartition = cachedThumbnails.partitionWith(getMaxCacheRange());
216         dropFromCache(cachePartition[0]);
217         cachedThumbnails = cachePartition[1];
218         dropFromCache(cachePartition[2]);
219     }
220 
221     private void dropFromCache(Range range) {
222         for (int i = range.getStart(); i < range.getEnd(); i++) {
223             final ThumbnailData removed = indexToThumbnail.remove(i);
224 
225             String thumbnailId = removed.getThumbnailId();
226             idToIndex.remove(thumbnailId);
227             idToUrl.remove(thumbnailId);
228         }
229     }
230 
231     private void queryThumbnails(Range range) {
232         if (!range.isEmpty()) {
233             this.getLazyLayoutServerRpc().loadElements(range.getStart(), range.length(), cachedThumbnails.getStart(), cachedThumbnails.getEnd());
234             waitingData = true;
235         }
236     }
237 
238     /**
239      * @return how many thumbnails should be cached from both sides before we will have to ask server to
240      * load more (half of the page).
241      */
242     private Range getMinCacheRange() {
243         final Range displayedRange = getWidget().getDisplayedRange();
244         int cachePageSize = displayedRange.length();
245         return displayedRange.expand(cachePageSize / 2, cachePageSize / 2).restrictTo(getAvailableRange());
246     }
247 
248     /**
249      * @return how many thumbnails can be cached on either of sides before we start evicting them from the
250      * cache (full page by). Also this is the amount of thumbnails to be queried from teh server.
251      */
252     protected Range getMaxCacheRange() {
253         final Range displayedRange = getWidget().getDisplayedRange();
254         int cachePageSize = displayedRange.length();
255         return displayedRange.expand(cachePageSize, cachePageSize).restrictTo(getAvailableRange());
256     }
257 
258     /**
259      * @return
260      */
261     private Range getAvailableRange() {
262         return Range.between(0, getState().elementAmount);
263     }
264 
265     @Override
266     abstract protected EscalatorPanel createWidget();
267 
268     public void updateSelection() {
269         final ThumbnailLayoutState.SelectionModel selection = getState().selection;
270 
271         final Range displayedRange = getWidget().getDisplayedRange();
272         final Range selectionBoundaries = selection.getSelectionBoundaries();
273         final List<Integer> indices = new LinkedList<>();
274 
275         if (selectionBoundaries.intersects(displayedRange)) {
276             for (int selectedIndex : selection.selectedIndices) {
277                 if (displayedRange.contains(selectedIndex)) {
278                     indices.add(selectedIndex);
279                 }
280             }
281         }
282         getWidget().setSelectedThumbnailsViaIndices(indices);
283     }
284 
285     abstract void updateThumbnailContentAtIndex(int thumbnailAbsoluteIndex);
286 
287     abstract LazyLayoutServerRpc getLazyLayoutServerRpc();
288 }