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.ui.workbench.container;
35
36 import info.magnolia.context.MgnlContext;
37 import info.magnolia.jcr.util.NodeTypes;
38 import info.magnolia.ui.api.ModelConstants;
39 import info.magnolia.ui.vaadin.integration.jcr.JcrItemUtil;
40 import info.magnolia.ui.vaadin.integration.jcr.JcrNodeAdapter;
41 import info.magnolia.ui.vaadin.integration.jcr.JcrPropertyAdapter;
42 import info.magnolia.ui.workbench.definition.NodeTypeDefinition;
43 import info.magnolia.ui.workbench.definition.WorkbenchDefinition;
44
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.Collections;
48 import java.util.HashMap;
49 import java.util.LinkedHashSet;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Set;
53
54 import javax.jcr.Node;
55 import javax.jcr.PathNotFoundException;
56 import javax.jcr.RepositoryException;
57 import javax.jcr.Session;
58 import javax.jcr.query.Query;
59 import javax.jcr.query.QueryManager;
60 import javax.jcr.query.QueryResult;
61 import javax.jcr.query.RowIterator;
62
63 import org.apache.commons.lang.StringUtils;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
66
67 import com.vaadin.data.Container;
68 import com.vaadin.data.ContainerHelpers;
69 import com.vaadin.data.Item;
70 import com.vaadin.data.Property;
71
72
73
74
75
76 public abstract class AbstractJcrContainer extends AbstractContainer implements Container.Sortable, Container.Indexed, Container.ItemSetChangeNotifier {
77
78 private static final Logger log = LoggerFactory.getLogger(AbstractJcrContainer.class);
79
80 public static final int DEFAULT_PAGE_LENGTH = 30;
81
82 public static final int DEFAULT_CACHE_RATIO = 2;
83
84
85
86
87 public static final String PROPERTY_NAME_AND_UUID_SEPARATOR = "@";
88 private static final Long LONG_ZERO = Long.valueOf(0);
89
90
91
92
93 public static final String DEFAULT_NODE_TYPE = NodeTypes.Content.NAME;
94
95 private static final String QUERY_LANGUAGE = Query.JCR_JQOM;
96
97 protected static final String SELECTOR_NAME = "t";
98
99 protected static final String SELECT_TEMPLATE = "select * from [%s] as " + SELECTOR_NAME;
100
101 protected static final String WHERE_TEMPLATE_FOR_PATH = " ISDESCENDANTNODE('%s')";
102
103 protected static final String ORDER_BY = " order by ";
104
105 protected static final String ASCENDING_KEYWORD = " asc";
106
107 protected static final String DESCENDING_KEYWORD = " desc";
108
109 protected static final String JCR_NAME_FUNCTION = "lower(name(" + SELECTOR_NAME + "))";
110
111
112
113
114 private final Map<Long, String> itemIndexes = new HashMap<Long, String>();
115
116 private final List<String> sortableProperties = new ArrayList<String>();
117
118 private final List<OrderBy> sorters = new ArrayList<OrderBy>();
119
120 private final WorkbenchDefinition workbenchDefinition;
121
122 private int size = Integer.MIN_VALUE;
123
124
125
126
127 private int pageLength = DEFAULT_PAGE_LENGTH;
128
129
130
131
132 private int cacheRatio = DEFAULT_CACHE_RATIO;
133
134 private Set<ItemSetChangeListener> itemSetChangeListeners;
135
136
137
138
139 private int currentOffset;
140
141
142 public AbstractJcrContainer(WorkbenchDefinition workbenchDefinition) {
143 this.workbenchDefinition = workbenchDefinition;
144 }
145
146 public void addSortableProperty(final String sortableProperty) {
147 sortableProperties.add(sortableProperty);
148 }
149
150 public WorkbenchDefinition getWorkbenchDefinition() {
151 return workbenchDefinition;
152 }
153
154 @Override
155 public void addItemSetChangeListener(ItemSetChangeListener listener) {
156 if (itemSetChangeListeners == null) {
157 itemSetChangeListeners = new LinkedHashSet<ItemSetChangeListener>();
158 }
159 itemSetChangeListeners.add(listener);
160 }
161
162 @Override
163 public void addListener(ItemSetChangeListener listener) {
164 addItemSetChangeListener(listener);
165 }
166
167 @Override
168 public void removeItemSetChangeListener(ItemSetChangeListener listener) {
169 if (itemSetChangeListeners != null) {
170 itemSetChangeListeners.remove(listener);
171 if (itemSetChangeListeners.isEmpty()) {
172 itemSetChangeListeners = null;
173 }
174 }
175 }
176
177 @Override
178 public void removeListener(ItemSetChangeListener listener) {
179 removeItemSetChangeListener(listener);
180 }
181
182 public void fireItemSetChange() {
183 log.debug("Firing item set changed");
184 if (itemSetChangeListeners != null && !itemSetChangeListeners.isEmpty()) {
185 final Container.ItemSetChangeEvent event = new AbstractContainer.ItemSetChangeEvent();
186 Object[] array = itemSetChangeListeners.toArray();
187 for (Object anArray : array) {
188 ItemSetChangeListener listener = (ItemSetChangeListener) anArray;
189 listener.containerItemSetChange(event);
190 }
191 }
192 }
193
194 protected Map<Long, String> getItemIndexes() {
195 return itemIndexes;
196 }
197
198 public int getPageLength() {
199 return pageLength;
200 }
201
202 public void setPageLength(int pageLength) {
203 this.pageLength = pageLength;
204 }
205
206 public int getCacheRatio() {
207 return cacheRatio;
208 }
209
210 public void setCacheRatio(int cacheRatio) {
211 this.cacheRatio = cacheRatio;
212 }
213
214 public javax.jcr.Item getJcrItem(Object itemId) {
215 if (itemId == null) {
216 return null;
217 }
218 try {
219 return JcrItemUtil.getJcrItem(getWorkspace(), (String) itemId);
220 } catch (PathNotFoundException p) {
221 log.debug("Could not access itemId {} in workspace {} - {}. Most likely it has been (re)moved in the meantime.", new Object[]{itemId, getWorkspace(), p.toString()});
222 } catch (RepositoryException e) {
223 handleRepositoryException(log, "Could not retrieve jcr item with id: " + itemId, e);
224 }
225 return null;
226 }
227
228
229
230
231 @Override
232 public Item getItem(Object itemId) {
233 javax.jcr.Item item = getJcrItem(itemId);
234 if (item == null) {
235 return null;
236 }
237 return item.isNode() ? new JcrNodeAdapter((Node) item) : new JcrPropertyAdapter((javax.jcr.Property) item);
238 }
239
240 @Override
241 public Collection<String> getItemIds() {
242 throw new UnsupportedOperationException(getClass().getName() + " does not support this method.");
243 }
244
245 @Override
246 public Property<?> getContainerProperty(Object itemId, Object propertyId) {
247 final Item item = getItem(itemId);
248 if (item != null) {
249 return item.getItemProperty(propertyId);
250 }
251
252 log.warn("Couldn't find item {} so property {} can't be retrieved!", itemId, propertyId);
253 return null;
254 }
255
256
257
258
259 @Override
260 public int size() {
261 return size;
262 }
263
264 @Override
265 public boolean containsId(Object itemId) {
266 return getItem(itemId) != null;
267 }
268
269 @Override
270 public Item addItem(Object itemId) throws UnsupportedOperationException {
271 fireItemSetChange();
272 return getItem(itemId);
273 }
274
275 @Override
276 public Object addItem() throws UnsupportedOperationException {
277 throw new UnsupportedOperationException();
278 }
279
280 @Override
281 public boolean removeAllItems() throws UnsupportedOperationException {
282 throw new UnsupportedOperationException();
283 }
284
285
286
287
288
289
290
291 @Override
292 public int indexOfId(Object itemId) {
293
294 if (!containsId(itemId)) {
295 return -1;
296 }
297 int size = size();
298 boolean wrappedAround = false;
299 while (!wrappedAround) {
300 for (Long i : itemIndexes.keySet()) {
301 if (itemIndexes.get(i).equals(itemId)) {
302 return i.intValue();
303 }
304 }
305
306 int nextIndex = (currentOffset / (pageLength * cacheRatio) + 1) * (pageLength * cacheRatio);
307 if (nextIndex >= size) {
308
309 wrappedAround = true;
310 nextIndex = 0;
311 }
312 updateOffsetAndCache(nextIndex);
313 }
314 return -1;
315 }
316
317 @Override
318 public Object getIdByIndex(int index) {
319 if (index < 0 || index > size - 1) {
320 return null;
321 }
322 final Long idx = Long.valueOf(index);
323 if (itemIndexes.containsKey(idx)) {
324 return itemIndexes.get(idx);
325 }
326 log.debug("item id {} not found in cache. Need to update offset, fetch new item ids from jcr repo and put them in cache.", index);
327 updateOffsetAndCache(index);
328 return itemIndexes.get(idx);
329 }
330
331
332
333
334
335
336
337 @Override
338 public Object nextItemId(Object itemId) {
339 return getIdByIndex(indexOfId(itemId) + 1);
340 }
341
342 @Override
343 public Object prevItemId(Object itemId) {
344 return getIdByIndex(indexOfId(itemId) - 1);
345 }
346
347 @Override
348 public Object firstItemId() {
349 if (size == 0) {
350 return null;
351 }
352 if (!itemIndexes.containsKey(LONG_ZERO)) {
353 updateOffsetAndCache(0);
354 }
355 return itemIndexes.get(LONG_ZERO);
356 }
357
358 @Override
359 public Object lastItemId() {
360 final Long lastIx = Long.valueOf(size() - 1);
361 if (!itemIndexes.containsKey(lastIx)) {
362 updateOffsetAndCache(size - 1);
363 }
364 return itemIndexes.get(lastIx);
365 }
366
367 @Override
368 public boolean isFirstId(Object itemId) {
369 return firstItemId().equals(itemId);
370 }
371
372 @Override
373 public boolean isLastId(Object itemId) {
374 return lastItemId().equals(itemId);
375 }
376
377
378
379
380
381
382 @Override
383 public void sort(Object[] propertyId, boolean[] ascending) {
384 resetOffset();
385 sorters.clear();
386 for (int i = 0; i < propertyId.length; i++) {
387 if (sortableProperties.contains(propertyId[i])) {
388 OrderBy orderBy = new OrderBy((String) propertyId[i], ascending[i]);
389 sorters.add(orderBy);
390 }
391 }
392 getPage();
393 }
394
395 @Override
396 public List<String> getSortableContainerPropertyIds() {
397 return Collections.unmodifiableList(sortableProperties);
398 }
399
400 @Override
401 public boolean removeItem(Object itemId) throws UnsupportedOperationException {
402 fireItemSetChange();
403 return true;
404 }
405
406
407
408
409
410
411
412 @Override
413 public Item addItemAfter(Object previousItemId, Object newItemId) throws UnsupportedOperationException {
414 throw new UnsupportedOperationException();
415 }
416
417 @Override
418 public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException {
419 throw new UnsupportedOperationException();
420 }
421
422 @Override
423 public Object addItemAt(int index) throws UnsupportedOperationException {
424 throw new UnsupportedOperationException();
425 }
426
427 @Override
428 public Object addItemAfter(Object previousItemId) throws UnsupportedOperationException {
429 throw new UnsupportedOperationException();
430 }
431
432
433
434
435
436
437
438 private void updateOffsetAndCache(int index) {
439 if (itemIndexes.containsKey(Long.valueOf(index))) {
440 return;
441 }
442 currentOffset = (index / (pageLength * cacheRatio)) * (pageLength * cacheRatio);
443 if (currentOffset < 0) {
444 resetOffset();
445 }
446 getPage();
447 }
448
449
450
451
452 private void updateCount(long newSize) {
453 if (newSize != size) {
454 setSize((int) newSize);
455 }
456 }
457
458
459
460
461 private final void getPage() {
462
463 final String stmt = constructJCRQuery(true);
464 if (StringUtils.isEmpty(stmt)) {
465 return;
466 }
467
468 try {
469 final QueryResult queryResult = executeQuery(stmt, QUERY_LANGUAGE, pageLength * cacheRatio, currentOffset);
470 updateItems(queryResult);
471 } catch (RepositoryException e) {
472 handleRepositoryException(log, "Cannot get Page with statement: " + stmt, e);
473 }
474 }
475
476
477
478
479
480
481 private void updateItems(final QueryResult queryResult) throws RepositoryException {
482 long start = System.currentTimeMillis();
483 log.debug("Starting iterating over QueryResult");
484 final RowIterator iterator = queryResult.getRows();
485 long rowCount = currentOffset;
486 while (iterator.hasNext()) {
487 final Node node = iterator.nextRow().getNode(SELECTOR_NAME);
488 final String id = node.getIdentifier();
489 log.debug("Adding node {} to cached items.", id);
490 itemIndexes.put(rowCount++, id);
491 }
492
493 log.debug("Done in {} ms", System.currentTimeMillis() - start);
494 }
495
496
497
498
499
500
501
502
503
504
505 protected final String constructJCRQuery(final boolean considerSorting) {
506 final String select = getQuerySelectStatement();
507 final StringBuilder stmt = new StringBuilder(select);
508
509 stmt.append(getQueryWhereClause());
510
511 if (considerSorting) {
512 if (sorters.isEmpty()) {
513
514 String defaultOrder = workbenchDefinition.getDefaultOrder();
515 String[] defaultOrders = defaultOrder.split(",");
516 for (String current : defaultOrders) {
517 sorters.add(new OrderBy(current, true));
518 }
519 }
520 stmt.append(ORDER_BY);
521 String sortOrder;
522 for (OrderBy orderBy : sorters) {
523 String propertyName = orderBy.getProperty();
524 sortOrder = orderBy.isAscending() ? ASCENDING_KEYWORD : DESCENDING_KEYWORD;
525 if (ModelConstants.JCR_NAME.equals(propertyName)) {
526 stmt.append(JCR_NAME_FUNCTION).append(sortOrder).append(", ");
527 continue;
528 }
529 stmt.append(SELECTOR_NAME);
530 stmt.append(".[").append(propertyName).append("]").append(sortOrder).append(", ");
531 }
532 stmt.delete(stmt.lastIndexOf(","), stmt.length());
533 }
534 log.debug("Constructed JCR query is {}", stmt);
535 return stmt.toString();
536 }
537
538
539
540
541
542
543 protected String getQueryWhereClause() {
544 String whereClause = "";
545 String clauseWorkspacePath = getQueryWhereClauseWorkspacePath();
546 if (!"".equals(clauseWorkspacePath)) {
547 whereClause = " where " + clauseWorkspacePath;
548 }
549 log.debug("JCR query WHERE clause is {}", whereClause);
550 return whereClause;
551 }
552
553
554
555
556
557 protected final String getQueryWhereClauseWorkspacePath() {
558
559 String whereClauseWorkspacePath = "";
560 if (StringUtils.isNotBlank(workbenchDefinition.getPath()) && !"/".equals(workbenchDefinition.getPath())) {
561 whereClauseWorkspacePath = String.format(WHERE_TEMPLATE_FOR_PATH, workbenchDefinition.getPath());
562 }
563 log.debug("Workspace path where-clause is {}", whereClauseWorkspacePath);
564 return whereClauseWorkspacePath;
565 }
566
567
568
569
570 protected String getQuerySelectStatement() {
571 return String.format(SELECT_TEMPLATE, getMainNodeType());
572 }
573
574
575
576
577 protected String getMainNodeType() {
578 final List<NodeTypeDefinition> nodeTypes = workbenchDefinition.getNodeTypes();
579 return nodeTypes.isEmpty() ? DEFAULT_NODE_TYPE : nodeTypes.get(0).getName();
580 }
581
582
583
584
585 public final void updateSize() {
586 final String stmt = constructJCRQuery(false);
587 try {
588
589 final QueryResult queryResult = executeQuery(stmt, QUERY_LANGUAGE, 0, 0);
590
591 final long pageSize = queryResult.getRows().getSize();
592 log.debug("Query result set contains {} items", pageSize);
593
594 updateCount((int) pageSize);
595 } catch (RepositoryException e) {
596 handleRepositoryException(log, "Could not update size with statement: " + stmt, e);
597 }
598 }
599
600 @Override
601 public List<?> getItemIds(int startIndex, int numberOfItems) {
602 return ContainerHelpers.getItemIdsUsingGetIdByIndex(startIndex, numberOfItems, this);
603 }
604
605 public String getWorkspace() {
606 return workbenchDefinition.getWorkspace();
607 }
608
609
610
611
612
613 public void refresh() {
614 resetOffset();
615 clearItemIndexes();
616 updateSize();
617 }
618
619 protected void resetOffset() {
620 currentOffset = 0;
621 }
622
623 protected void clearItemIndexes() {
624 itemIndexes.clear();
625 }
626
627 protected int getCurrentOffset() {
628 return currentOffset;
629 }
630
631 protected void setSize(int size) {
632 this.size = size;
633 }
634
635 protected QueryResult executeQuery(String statement, String language, long limit, long offset) throws RepositoryException {
636 final Session jcrSession = MgnlContext.getJCRSession(getWorkspace());
637 final QueryManager jcrQueryManager = jcrSession.getWorkspace().getQueryManager();
638 final Query query = jcrQueryManager.createQuery(statement, language);
639 if (limit > 0) {
640 query.setLimit(limit);
641 }
642 if (offset >= 0) {
643 query.setOffset(offset);
644 }
645 log.debug("Executing query against workspace [{}] with statement [{}] and limit {} and offset {}...", new Object[]{getWorkspace(), statement, limit, offset});
646 long start = System.currentTimeMillis();
647 final QueryResult result = query.execute();
648 log.debug("Query execution took {} ms", System.currentTimeMillis() - start);
649
650 return result;
651 }
652
653
654
655
656
657
658
659
660 protected void handleRepositoryException(final Logger logger, final String message, final RepositoryException repositoryException) {
661 logger.warn(message + ": " + repositoryException);
662 }
663 }