1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
73
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
97
98 private final Map<Long, ItemKey> itemIndexes = new HashMap<Long, ItemKey>();
99
100 private int size = 0;
101
102
103
104
105 private int pageLength = DEFAULT_PAGE_LENGTH;
106
107
108
109
110 private int cacheRatio = DEFAULT_CACHE_RATIO;
111
112
113
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
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
219
220
221
222
223
224 @Override
225 public void refresh() {
226 resetOffset();
227 clearItemIndexes();
228 updateSize();
229 fireItemSetChange();
230 }
231
232
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
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
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
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
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
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
379
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
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
407
408 protected AssetQuery.OrderBy getDefaultOrderBy(final String property) {
409 return new AssetQuery.OrderBy(MagnoliaAssetMetadata.class, property, AssetQuery.Order.ASCENDING);
410 }
411
412
413
414
415
416
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
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
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
484 int nextIndex = (currentOffset / (pageLength * cacheRatio) + 1) * (pageLength * cacheRatio);
485 if (nextIndex >= size) {
486
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
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 }