View Javadoc
1   /**
2    * This file Copyright (c) 2014-2015 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.dam.external.app.contentview;
35  
36  import info.magnolia.dam.api.AssetProvider;
37  import info.magnolia.dam.api.AssetQuery;
38  import info.magnolia.dam.api.ItemKey;
39  import info.magnolia.dam.api.metadata.MagnoliaAssetMetadata;
40  import info.magnolia.dam.external.app.contentconnector.AssetContentConnector;
41  import info.magnolia.dam.external.app.contentconnector.AssetContentConnectorDefinition;
42  import info.magnolia.dam.jcr.metadata.JcrDublinCore;
43  import info.magnolia.ui.workbench.container.AbstractContainer;
44  import info.magnolia.ui.workbench.container.Refreshable;
45  
46  import java.io.Serializable;
47  import java.util.ArrayList;
48  import java.util.Collection;
49  import java.util.Collections;
50  import java.util.EventObject;
51  import java.util.HashMap;
52  import java.util.Iterator;
53  import java.util.LinkedHashSet;
54  import java.util.List;
55  import java.util.Map;
56  import java.util.Set;
57  
58  import javax.inject.Inject;
59  
60  import org.apache.commons.lang3.StringUtils;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  import com.vaadin.data.Container;
65  import com.vaadin.data.ContainerHelpers;
66  import com.vaadin.data.Item;
67  import com.vaadin.data.Property;
68  import com.vaadin.data.util.BeanItem;
69  import com.vaadin.data.util.MethodProperty;
70  
71  /**
72   * Vaadin container that stores copies of items obtained via {@link AssetProvider} implementations. Implements a simple mechanism
73   * for lazy loading items and a cache for items and item ids.
74   */
75  public abstract class AbstractAssetContainer extends AbstractContainer implements Container.Sortable, Container.Indexed, Container.ItemSetChangeNotifier, Refreshable {
76  
77      public static final int DEFAULT_PAGE_LENGTH = 30;
78  
79      public static final int DEFAULT_CACHE_RATIO = 2;
80  
81      private static final Long LONG_ZERO = (long) 0;
82  
83      private static final Logger log = LoggerFactory.getLogger(AbstractAssetContainer.class);
84  
85      private Set<ItemSetChangeListener> itemSetChangeListeners;
86  
87      private final List<String> sortableProperties = new ArrayList<String>();
88  
89      private final List<AssetQuery.OrderBy> sorters = new ArrayList<AssetQuery.OrderBy>();
90  
91      private AssetProvider assetProvider;
92  
93      private AssetContentConnectorDefinition contentConnectorDefinition;
94  
95      /**
96       * Item and index caches.
97       */
98      private final Map<Long, ItemKey> itemIndexes = new HashMap<Long, ItemKey>();
99  
100     private int size = 0;
101 
102     /**
103      * Page length = number of items contained in one page.
104      */
105     private int pageLength = DEFAULT_PAGE_LENGTH;
106 
107     /**
108      * Number of items to cache = cacheRatio x pageLength.
109      */
110     private int cacheRatio = DEFAULT_CACHE_RATIO;
111 
112     /**
113      * Starting row number of the currently fetched page.
114      */
115     private int currentOffset;
116 
117 
118     @Inject
119     public AbstractAssetContainer(AssetContentConnector assetContentConnector) {
120         this.assetProvider = assetContentConnector.getAssetProvider();
121         this.contentConnectorDefinition = assetContentConnector.getContentConnectorDefinition();
122     }
123 
124     public void setAssetProvider(AssetProvider assetProvider) {
125         this.assetProvider = assetProvider;
126     }
127 
128     public AssetProvider getAssetProvider() {
129         return this.assetProvider;
130     }
131 
132     public int getPageLength() {
133         return pageLength;
134     }
135 
136     public void setPageLength(int pageLength) {
137         this.pageLength = pageLength;
138     }
139 
140     public int getCacheRatio() {
141         return cacheRatio;
142     }
143 
144     public void setCacheRatio(int cacheRatio) {
145         this.cacheRatio = cacheRatio;
146     }
147 
148     public void addSortableProperty(final String sortableProperty) {
149         sortableProperties.add(sortableProperty);
150     }
151 
152     /* {@link Container} interface */
153 
154     @Override
155     public int size() {
156         return size;
157     }
158 
159     @Override
160     public BeanItem<info.magnolia.dam.api.Item> getItem(Object itemId) {
161         if (itemId != null && ItemKey.isValid(((ItemKey) itemId).asString())) {
162             try {
163                 info.magnolia.dam.api.Item item = getAssetProvider().getItem((ItemKey) itemId);
164                 return new BeanItem<info.magnolia.dam.api.Item>(item);
165             } catch (AssetProvider.AssetNotFoundException e) {
166                 log.error("Failed to retrieve item with given id: " + e.getMessage(), e);
167             } catch (AssetProvider.IllegalItemKeyException e) {
168                 log.error("Failed to handle item id: " + e.getMessage(), e);
169             }
170         }
171         return null;
172     }
173 
174     @Override
175     public boolean containsId(Object itemId) {
176         return getItem(itemId) != null;
177     }
178 
179     @Override
180     public Property<?> getContainerProperty(Object itemId, Object propertyId) {
181         info.magnolia.dam.api.Item item = getAssetItem(itemId);
182         if (item == null) {
183             return null;
184         }
185         return new MethodProperty<Object>(item, (String) propertyId);
186     }
187 
188     @Override
189     public Collection<String> getItemIds() {
190         throw new UnsupportedOperationException(
191                 "Asset container does not support 'getItemIds()' operation");
192     }
193 
194     @Override
195     public Item addItem(Object itemId) throws UnsupportedOperationException {
196         throw new UnsupportedOperationException(
197                 "Asset container does not support 'addItem(Object itemId)' operation");
198     }
199 
200     @Override
201     public Object addItem() throws UnsupportedOperationException {
202         throw new UnsupportedOperationException(
203                 "Asset container does not support 'addItem()' operation");
204     }
205 
206     @Override
207     public boolean removeAllItems() throws UnsupportedOperationException {
208         throw new UnsupportedOperationException(
209                 "Asset container does not support 'removeAllItems()' operation");
210     }
211 
212     @Override
213     public boolean removeItem(Object itemId) throws UnsupportedOperationException {
214         throw new UnsupportedOperationException(
215                 "Asset container does not support 'removeItem(Object itemId)' operation");
216     }
217 
218     /* {@link Refreshable} interface */
219 
220     /**
221      * Refreshes the container - clears all caches and resets size and offset. Does NOT remove sorting or filtering
222      * rules!
223      */
224     @Override
225     public void refresh() {
226         resetOffset();
227         clearItemIndexes();
228         updateSize();
229         fireItemSetChange();
230     }
231 
232     /* {@link Container.ItemSetChangeNotifier} interface */
233 
234     @Override
235     public void addItemSetChangeListener(ItemSetChangeListener listener) {
236         if (itemSetChangeListeners == null) {
237             itemSetChangeListeners = new LinkedHashSet<ItemSetChangeListener>();
238         }
239         itemSetChangeListeners.add(listener);
240     }
241 
242     @Override
243     public void addListener(ItemSetChangeListener listener) {
244         addItemSetChangeListener(listener);
245     }
246 
247     @Override
248     public void removeItemSetChangeListener(ItemSetChangeListener listener) {
249         if (itemSetChangeListeners != null) {
250             itemSetChangeListeners.remove(listener);
251             if (itemSetChangeListeners.isEmpty()) {
252                 itemSetChangeListeners = null;
253             }
254         }
255     }
256 
257     @Override
258     public void removeListener(ItemSetChangeListener listener) {
259         removeItemSetChangeListener(listener);
260     }
261 
262     public void fireItemSetChange() {
263         if (itemSetChangeListeners != null && !itemSetChangeListeners.isEmpty()) {
264             final Object[] l = itemSetChangeListeners.toArray();
265             BaseItemSetChangeEvent event = new BaseItemSetChangeEvent(this);
266             for (Object listener : l) {
267                 ((ItemSetChangeListener) listener).containerItemSetChange(event);
268             }
269         }
270     }
271 
272     /**
273      * Basic itemSet change event.
274      */
275     protected static class BaseItemSetChangeEvent extends EventObject implements Container.ItemSetChangeEvent, Serializable {
276 
277         private static final long serialVersionUID = 1L;
278 
279         protected BaseItemSetChangeEvent(Container source) {
280             super(source);
281         }
282 
283         @Override
284         public Container getContainer() {
285             return (Container) getSource();
286         }
287     }
288 
289     /* {@link Container.Ordered} interface */
290 
291     @Override
292     public Object nextItemId(Object itemId) {
293         return getIdByIndex(indexOfId(itemId) + 1);
294     }
295 
296     @Override
297     public Object prevItemId(Object itemId) {
298         return getIdByIndex(indexOfId(itemId) - 1);
299     }
300 
301     @Override
302     public Object firstItemId() {
303         if (size == 0) {
304             return null;
305         }
306         if (!itemIndexes.containsKey(LONG_ZERO)) {
307             updateOffsetAndCache(0);
308         }
309         return itemIndexes.get(LONG_ZERO);
310     }
311 
312     @Override
313     public Object lastItemId() {
314         final Long lastIx = (long) (size() - 1);
315         if (!itemIndexes.containsKey(lastIx)) {
316             updateOffsetAndCache(size - 1);
317         }
318         return itemIndexes.get(lastIx);
319     }
320 
321     @Override
322     public boolean isFirstId(Object itemId) {
323         return firstItemId().equals(itemId);
324     }
325 
326     @Override
327     public boolean isLastId(Object itemId) {
328         return lastItemId().equals(itemId);
329     }
330 
331     @Override
332     public Item addItemAfter(Object previousItemId, Object newItemId) throws UnsupportedOperationException {
333         throw new UnsupportedOperationException(
334                 "Asset container does not support 'addItemAfter(Object previousItemId, Object newItemId)' operation");
335     }
336 
337     @Override
338     public Object addItemAfter(Object previousItemId) throws UnsupportedOperationException {
339         throw new UnsupportedOperationException(
340                 "Asset container does not support 'addItemAfter(Object previousItemId)' operation");
341     }
342 
343     /**
344      * Fetches a page from the data source based on the values of pageLength, cacheRatio and currentOffset.
345      */
346     private void getPage() {
347         final AssetQuery query = constructQuery(pageLength * cacheRatio, currentOffset, true);
348         Iterator<info.magnolia.dam.api.Item> queryResult = getAssetProvider().list(query);
349         updateItems(queryResult);
350     }
351 
352     /**
353      * @see #getPage() without pagination.
354      */
355     private void updateSize() {
356         long size = 0;
357         final AssetQuery query = constructQuery(0, 0, false);
358         Iterator<info.magnolia.dam.api.Item> queryResult = getAssetProvider().list(query);
359         while (queryResult.hasNext()) {
360             queryResult.next();
361             size += 1;
362         }
363         updateCount(size);
364     }
365 
366     /**
367      * Updates this container by storing the items found in the query result passed as argument.
368      */
369     private void updateItems(final Iterator<info.magnolia.dam.api.Item> queryResult) {
370         long rowCount = currentOffset;
371         while (queryResult.hasNext()) {
372             info.magnolia.dam.api.Item item = queryResult.next();
373             itemIndexes.put(rowCount++, item.getItemKey());
374         }
375     }
376 
377     /**
378      * @param considerSorting an optional <code>ORDER BY</code> is added if this parameter is <code>true</code>.
379      * @return a AssetQuery representing a statement to retrieve this container's items.
380      */
381     protected final AssetQuery constructQuery(long pageSize, long offset, final boolean considerSorting) {
382         AssetQuery.Builder builder = new AssetQuery.Builder();
383         if (pageSize > 0) {
384             builder.withMaxResults(pageSize);
385         }
386         if (offset > 0) {
387             builder.withOffset(offset);
388         }
389         if (considerSorting) {
390             if (sorters.isEmpty() && StringUtils.isNotBlank(contentConnectorDefinition.getDefaultOrder())) {
391                 // no sorters set - use defaultOrder (always ascending)
392                 String defaultOrder = contentConnectorDefinition.getDefaultOrder();
393                 String[] defaultOrders = defaultOrder.split(",");
394                 for (String current : defaultOrders) {
395                     sorters.add(getDefaultOrderBy(current));
396                 }
397             }
398             for (AssetQuery.OrderBy orderBy : sorters) {
399                 builder.orderBy(orderBy.getMetadataClass(), orderBy.getPropertyId(), orderBy.getOrder());
400             }
401         }
402         return builder.build();
403     }
404 
405     /**
406      * @return an {@link AssetQuery.OrderBy} object for the passed in property to be used for the default order by clause.
407      */
408     protected AssetQuery.OrderBy getDefaultOrderBy(final String property) {
409         return new AssetQuery.OrderBy(MagnoliaAssetMetadata.class, property, AssetQuery.Order.ASCENDING);
410     }
411 
412     /**
413      * Determines a new offset for updating the row cache. The offset is calculated from the given index, and will be
414      * fixed to match the start of a page, based on the value of pageLength.
415      *
416      * @param index Index of the item that was requested, but not found in cache
417      */
418     private void updateOffsetAndCache(int index) {
419         if (itemIndexes.containsKey((long) index)) {
420             return;
421         }
422         currentOffset = (index / (pageLength * cacheRatio)) * (pageLength * cacheRatio);
423         if (currentOffset < 0) {
424             resetOffset();
425         }
426         getPage();
427     }
428 
429     protected info.magnolia.dam.api.Item getAssetItem(Object itemKey) {
430         if (!ItemKey.isValid(((ItemKey) itemKey).asString())) {
431             return null;
432         }
433         try {
434             return getAssetProvider().getItem((ItemKey) itemKey);
435         } catch (AssetProvider.AssetNotFoundException e) {
436             log.debug("Failed to retrieve item with given id: " + e.getMessage(), e);
437         } catch (AssetProvider.IllegalItemKeyException e) {
438             log.debug("Failed to handle item id: " + e.getMessage(), e);
439         }
440         return null;
441     }
442 
443     /**
444      * Triggers a refresh if the current row count has changed.
445      */
446     private void updateCount(long newSize) {
447         if (newSize != size) {
448             setSize((int) newSize);
449         }
450     }
451 
452     protected void resetOffset() {
453         currentOffset = 0;
454     }
455 
456     protected void clearItemIndexes() {
457         itemIndexes.clear();
458     }
459 
460     protected int getCurrentOffset() {
461         return currentOffset;
462     }
463 
464     protected void setSize(int size) {
465         this.size = size;
466     }
467 
468     /* {@link Container.Indexed} interface */
469 
470     @Override
471     public int indexOfId(Object itemId) {
472         if (!containsId(itemId)) {
473             return -1;
474         }
475         int size = size();
476         boolean wrappedAround = false;
477         while (!wrappedAround) {
478             for (Long i : itemIndexes.keySet()) {
479                 if (itemIndexes.get(i).equals(itemId)) {
480                     return i.intValue();
481                 }
482             }
483             // load in the next page.
484             int nextIndex = (currentOffset / (pageLength * cacheRatio) + 1) * (pageLength * cacheRatio);
485             if (nextIndex >= size) {
486                 // Container wrapped around, start from index 0.
487                 wrappedAround = true;
488                 nextIndex = 0;
489             }
490             updateOffsetAndCache(nextIndex);
491         }
492         return -1;
493     }
494 
495     @Override
496     public ItemKey getIdByIndex(int index) {
497         if (index < 0 || index > size - 1) {
498             return null;
499         }
500         final Long idx = (long) index;
501         if (itemIndexes.containsKey(idx)) {
502             return itemIndexes.get(idx);
503         }
504         log.debug("item id {} not found in cache. Need to update offset, fetch new item ids from asset provider and put them in cache.", index);
505         updateOffsetAndCache(index);
506         return itemIndexes.get(idx);
507     }
508 
509     @Override
510     public List<?> getItemIds(int startIndex, int numberOfItems) {
511         return ContainerHelpers.getItemIdsUsingGetIdByIndex(startIndex, numberOfItems, this);
512     }
513 
514     @Override
515     public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException {
516         throw new UnsupportedOperationException(
517                 "Asset container does not support 'addItemAt(int index, newItemId)' operation");
518     }
519 
520     @Override
521     public Object addItemAt(int index) throws UnsupportedOperationException {
522         throw new UnsupportedOperationException(
523                 "Asset container does not support 'addItemAt(int index)' operation");
524     }
525 
526     /* {@link Container.Sortable} interface */
527 
528     @Override
529     public void sort(Object[] propertyId, boolean[] ascending) {
530         clearItemIndexes();
531         resetOffset();
532         sorters.clear();
533         for (int i = 0; i < propertyId.length; i++) {
534             if (sortableProperties.contains(String.valueOf(propertyId[i]))) {
535                 AssetQuery.OrderBy orderBy = new AssetQuery.OrderBy(JcrDublinCore.class, (String) propertyId[i], ascending[i] ? AssetQuery.Order.ASCENDING : AssetQuery.Order.DESCENDING);
536                 sorters.add(orderBy);
537             }
538         }
539         updateSize();
540         getPage();
541     }
542 
543     @Override
544     public List<String> getSortableContainerPropertyIds() {
545         return Collections.unmodifiableList(sortableProperties);
546     }
547 }