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.vaadin.integration.contentconnector.JcrContentConnectorDefinition;
39 import info.magnolia.ui.vaadin.integration.contentconnector.NodeTypeDefinition;
40 import info.magnolia.ui.vaadin.integration.jcr.JcrItemId;
41 import info.magnolia.ui.vaadin.integration.jcr.JcrItemUtil;
42 import info.magnolia.ui.vaadin.integration.jcr.JcrNodeAdapter;
43 import info.magnolia.ui.vaadin.integration.jcr.JcrPropertyAdapter;
44 import info.magnolia.ui.vaadin.integration.jcr.ModelConstants;
45
46 import java.util.ArrayList;
47 import java.util.Collection;
48 import java.util.Collections;
49 import java.util.HashMap;
50 import java.util.HashSet;
51 import java.util.LinkedHashSet;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.Set;
55
56 import javax.jcr.LoginException;
57 import javax.jcr.Node;
58 import javax.jcr.PathNotFoundException;
59 import javax.jcr.RepositoryException;
60 import javax.jcr.Session;
61 import javax.jcr.nodetype.NodeType;
62 import javax.jcr.nodetype.NodeTypeIterator;
63 import javax.jcr.nodetype.NodeTypeManager;
64 import javax.jcr.query.Query;
65 import javax.jcr.query.QueryManager;
66 import javax.jcr.query.QueryResult;
67 import javax.jcr.query.RowIterator;
68
69 import org.apache.commons.lang3.StringUtils;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
72
73 import com.vaadin.data.Container;
74 import com.vaadin.data.ContainerHelpers;
75 import com.vaadin.data.Item;
76 import com.vaadin.data.Property;
77
78
79
80
81
82 public abstract class AbstractJcrContainer extends AbstractContainer implements Container.Sortable, Container.Indexed, Container.ItemSetChangeNotifier, Refreshable {
83
84 private static final Logger log = LoggerFactory.getLogger(AbstractJcrContainer.class);
85
86 public static final int DEFAULT_PAGE_LENGTH = 30;
87
88 public static final int DEFAULT_CACHE_RATIO = 2;
89
90
91
92
93 public static final String PROPERTY_NAME_AND_UUID_SEPARATOR = "@";
94
95 private static final Long LONG_ZERO = Long.valueOf(0);
96
97
98
99
100 public static final String DEFAULT_NODE_TYPE = NodeTypes.Content.NAME;
101
102 private static final String QUERY_LANGUAGE = Query.JCR_JQOM;
103
104 protected static final String SELECTOR_NAME = "t";
105
106 protected static final String SELECT_TEMPLATE = "select * from [nt:base] as " + SELECTOR_NAME;
107
108 protected static final String WHERE_TEMPLATE_FOR_PATH = " ISDESCENDANTNODE('%s')";
109
110 protected static final String ORDER_BY = " order by ";
111
112 protected static final String ASCENDING_KEYWORD = " asc";
113
114 protected static final String DESCENDING_KEYWORD = " desc";
115
116 protected static final String JCR_NAME_FUNCTION = "lower(name(" + SELECTOR_NAME + "))";
117
118
119
120
121 private final Map<Long, JcrItemId> itemIndexes = new HashMap<Long, JcrItemId>();
122
123 private final List<String> sortableProperties = new ArrayList<String>();
124
125 private final List<OrderBy> sorters = new ArrayList<OrderBy>();
126
127 private int size = Integer.MIN_VALUE;
128
129
130
131
132 private int pageLength = DEFAULT_PAGE_LENGTH;
133
134
135
136
137 private int cacheRatio = DEFAULT_CACHE_RATIO;
138
139 private Set<ItemSetChangeListener> itemSetChangeListeners;
140
141
142
143
144 private int currentOffset;
145
146 private Set<NodeType> searchableNodeTypes;
147
148 private JcrContentConnectorDefinition contentConnectorDefinition;
149
150 public AbstractJcrContainer(JcrContentConnectorDefinition jcrContentConnectorDefinition) {
151 this.contentConnectorDefinition = jcrContentConnectorDefinition;
152 this.searchableNodeTypes = findSearchableNodeTypes();
153 }
154
155 public void addSortableProperty(final String sortableProperty) {
156 sortableProperties.add(sortableProperty);
157 }
158
159 public JcrContentConnectorDefinition getConfiguration() {
160 return contentConnectorDefinition;
161 }
162
163 @Override
164 public void addItemSetChangeListener(ItemSetChangeListener listener) {
165 if (itemSetChangeListeners == null) {
166 itemSetChangeListeners = new LinkedHashSet<ItemSetChangeListener>();
167 }
168 itemSetChangeListeners.add(listener);
169 }
170
171 @Override
172 public void addListener(ItemSetChangeListener listener) {
173 addItemSetChangeListener(listener);
174 }
175
176 @Override
177 public void removeItemSetChangeListener(ItemSetChangeListener listener) {
178 if (itemSetChangeListeners != null) {
179 itemSetChangeListeners.remove(listener);
180 if (itemSetChangeListeners.isEmpty()) {
181 itemSetChangeListeners = null;
182 }
183 }
184 }
185
186 @Override
187 public void removeListener(ItemSetChangeListener listener) {
188 removeItemSetChangeListener(listener);
189 }
190
191 public void fireItemSetChange() {
192 log.debug("Firing item set changed");
193 if (itemSetChangeListeners != null && !itemSetChangeListeners.isEmpty()) {
194 final Container.ItemSetChangeEvent event = new AbstractContainer.ItemSetChangeEvent();
195 Object[] array = itemSetChangeListeners.toArray();
196 for (Object anArray : array) {
197 ItemSetChangeListener listener = (ItemSetChangeListener) anArray;
198 listener.containerItemSetChange(event);
199 }
200 }
201 }
202
203 protected Map<Long, JcrItemId> getItemIndexes() {
204 return itemIndexes;
205 }
206
207 public int getPageLength() {
208 return pageLength;
209 }
210
211 public void setPageLength(int pageLength) {
212 this.pageLength = pageLength;
213 }
214
215 public int getCacheRatio() {
216 return cacheRatio;
217 }
218
219 public void setCacheRatio(int cacheRatio) {
220 this.cacheRatio = cacheRatio;
221 }
222
223 public javax.jcr.Item getJcrItem(Object itemId) {
224 if (itemId == null || !(itemId instanceof JcrItemId)) {
225 return null;
226 }
227 try {
228 return JcrItemUtil.getJcrItem((JcrItemId) itemId);
229 } catch (PathNotFoundException p) {
230 log.debug("Could not access itemId {} in workspace {} - {}. Most likely it has been (re)moved in the meantime.", new Object[] { itemId, getWorkspace(), p.toString() });
231 } catch (RepositoryException e) {
232 handleRepositoryException(log, "Could not retrieve jcr item with id: " + itemId, e);
233 }
234 return null;
235 }
236
237
238
239
240 @Override
241 public Item getItem(Object itemId) {
242 javax.jcr.Item item = getJcrItem(itemId);
243 if (item == null) {
244 return null;
245 }
246 return item.isNode() ? new JcrNodeAdapter((Node) item) : new JcrPropertyAdapter((javax.jcr.Property) item);
247 }
248
249 @Override
250 public Collection<String> getItemIds() {
251 throw new UnsupportedOperationException(getClass().getName() + " does not support this method.");
252 }
253
254 @Override
255 public Property<?> getContainerProperty(Object itemId, Object propertyId) {
256 final Item item = getItem(itemId);
257 if (item != null) {
258 return item.getItemProperty(propertyId);
259 }
260
261 log.warn("Couldn't find item {} so property {} can't be retrieved!", itemId, propertyId);
262 return null;
263 }
264
265
266
267
268 @Override
269 public int size() {
270 return size;
271 }
272
273 @Override
274 public boolean containsId(Object itemId) {
275 return getItem(itemId) != null;
276 }
277
278 @Override
279 public Item addItem(Object itemId) throws UnsupportedOperationException {
280 fireItemSetChange();
281 return getItem(itemId);
282 }
283
284 @Override
285 public Object addItem() throws UnsupportedOperationException {
286 throw new UnsupportedOperationException();
287 }
288
289 @Override
290 public boolean removeAllItems() throws UnsupportedOperationException {
291 throw new UnsupportedOperationException();
292 }
293
294
295
296
297
298
299
300 @Override
301 public int indexOfId(Object itemId) {
302
303 if (!containsId(itemId)) {
304 return -1;
305 }
306 int size = size();
307 ensureItemIndices();
308 boolean wrappedAround = false;
309 while (!wrappedAround) {
310 for (Long i : itemIndexes.keySet()) {
311 if (itemIndexes.get(i).equals(itemId)) {
312 return i.intValue();
313 }
314 }
315
316 int nextIndex = (currentOffset / (pageLength * cacheRatio) + 1) * (pageLength * cacheRatio);
317 if (nextIndex >= size) {
318
319 wrappedAround = true;
320 nextIndex = 0;
321 }
322 updateOffsetAndCache(nextIndex);
323 }
324 return -1;
325 }
326
327 protected void ensureItemIndices() {
328 if (itemIndexes.isEmpty()) {
329 getPage();
330 }
331 }
332
333 @Override
334 public JcrItemId getIdByIndex(int index) {
335 if (index < 0 || index > size - 1) {
336 return null;
337 }
338 final Long idx = Long.valueOf(index);
339 if (itemIndexes.containsKey(idx)) {
340 return itemIndexes.get(idx);
341 }
342 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);
343 updateOffsetAndCache(index);
344 return itemIndexes.get(idx);
345 }
346
347
348
349
350
351
352
353 @Override
354 public JcrItemId nextItemId(Object itemId) {
355 return getIdByIndex(indexOfId(itemId) + 1);
356 }
357
358 @Override
359 public JcrItemId prevItemId(Object itemId) {
360 return getIdByIndex(indexOfId(itemId) - 1);
361 }
362
363 @Override
364 public JcrItemId firstItemId() {
365 if (size == 0) {
366 return null;
367 }
368 if (!itemIndexes.containsKey(LONG_ZERO)) {
369 updateOffsetAndCache(0);
370 }
371 return itemIndexes.get(LONG_ZERO);
372 }
373
374 @Override
375 public JcrItemId lastItemId() {
376 final Long lastIx = Long.valueOf(size() - 1);
377 if (!itemIndexes.containsKey(lastIx)) {
378 updateOffsetAndCache(size - 1);
379 }
380 return itemIndexes.get(lastIx);
381 }
382
383 @Override
384 public boolean isFirstId(Object itemId) {
385 return firstItemId().equals(itemId);
386 }
387
388 @Override
389 public boolean isLastId(Object itemId) {
390 return lastItemId().equals(itemId);
391 }
392
393
394
395
396
397
398 @Override
399 public void sort(Object[] propertyId, boolean[] ascending) {
400 clearItemIndexes();
401 resetOffset();
402 sorters.clear();
403 for (int i = 0; i < propertyId.length; i++) {
404 if (sortableProperties.contains(propertyId[i])) {
405 OrderBy orderBy = new OrderBy((String) propertyId[i], ascending[i]);
406 sorters.add(orderBy);
407 }
408 }
409 getPage();
410 }
411
412 @Override
413 public List<String> getSortableContainerPropertyIds() {
414 return Collections.unmodifiableList(sortableProperties);
415 }
416
417 @Override
418 public boolean removeItem(Object itemId) throws UnsupportedOperationException {
419 fireItemSetChange();
420 return true;
421 }
422
423
424
425
426
427
428
429 @Override
430 public Item addItemAfter(Object previousItemId, Object newItemId) throws UnsupportedOperationException {
431 throw new UnsupportedOperationException();
432 }
433
434 @Override
435 public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException {
436 throw new UnsupportedOperationException();
437 }
438
439 @Override
440 public Object addItemAt(int index) throws UnsupportedOperationException {
441 throw new UnsupportedOperationException();
442 }
443
444 @Override
445 public Object addItemAfter(Object previousItemId) throws UnsupportedOperationException {
446 throw new UnsupportedOperationException();
447 }
448
449
450
451
452
453
454
455 private void updateOffsetAndCache(int index) {
456 if (itemIndexes.containsKey(Long.valueOf(index))) {
457 return;
458 }
459 currentOffset = (index / (pageLength * cacheRatio)) * (pageLength * cacheRatio);
460 if (currentOffset < 0) {
461 resetOffset();
462 }
463 getPage();
464 }
465
466
467
468
469 private void updateCount(long newSize) {
470 if (newSize != size) {
471 setSize((int) newSize);
472 }
473 }
474
475
476
477
478 private final void getPage() {
479
480 final String stmt = constructJCRQuery(true);
481 if (StringUtils.isEmpty(stmt)) {
482 return;
483 }
484
485 try {
486 final QueryResult queryResult = executeQuery(stmt, QUERY_LANGUAGE, pageLength * cacheRatio, currentOffset);
487 updateItems(queryResult);
488 } catch (RepositoryException e) {
489 handleRepositoryException(log, "Cannot get Page with statement: " + stmt, e);
490 }
491 }
492
493
494
495
496
497
498 private void updateItems(final QueryResult queryResult) throws RepositoryException {
499 long start = System.currentTimeMillis();
500 log.debug("Starting iterating over QueryResult");
501 final RowIterator iterator = queryResult.getRows();
502 long rowCount = currentOffset;
503 while (iterator.hasNext()) {
504 final Node node = iterator.nextRow().getNode(SELECTOR_NAME);
505 final JcrItemId itemId = JcrItemUtil.getItemId(node);
506 log.trace("Adding node {} to cached items.", itemId.getUuid());
507 itemIndexes.put(rowCount++, itemId);
508 }
509
510 log.debug("Done in {} ms", System.currentTimeMillis() - start);
511 }
512
513
514
515
516
517
518
519
520
521
522 protected final String constructJCRQuery(final boolean considerSorting) {
523 final String select = getQuerySelectStatement();
524 final StringBuilder stmt = new StringBuilder(select);
525
526 stmt.append(getQueryWhereClause());
527
528 if (considerSorting) {
529 if (sorters.isEmpty()) {
530
531 String defaultOrder = contentConnectorDefinition.getDefaultOrder();
532 String[] defaultOrders = defaultOrder.split(",");
533 for (String current : defaultOrders) {
534 sorters.add(getDefaultOrderBy(current));
535 }
536 }
537 stmt.append(ORDER_BY);
538 String sortOrder;
539 for (OrderBy orderBy : sorters) {
540 String propertyName = orderBy.getProperty();
541 sortOrder = orderBy.isAscending() ? ASCENDING_KEYWORD : DESCENDING_KEYWORD;
542 if (ModelConstants.JCR_NAME.equals(propertyName)) {
543 stmt.append(getJcrNameOrderByFunction()).append(sortOrder).append(", ");
544 continue;
545 }
546 stmt.append(SELECTOR_NAME);
547 stmt.append(".[").append(propertyName).append("]").append(sortOrder).append(", ");
548 }
549 stmt.delete(stmt.lastIndexOf(","), stmt.length());
550 }
551 log.debug("Constructed JCR query is {}", stmt);
552 return stmt.toString();
553 }
554
555
556
557
558 protected OrderBy getDefaultOrderBy(final String property) {
559 return new OrderBy(property, true);
560 }
561
562
563
564
565 protected String getJcrNameOrderByFunction() {
566 return JCR_NAME_FUNCTION;
567 }
568
569
570
571
572
573
574
575 protected String getQueryWhereClause() {
576 String whereClause = "";
577 String clauseWorkspacePath = getQueryWhereClauseWorkspacePath();
578 String clauseNodeTypes = getQueryWhereClauseNodeTypes();
579 if (StringUtils.isNotBlank(clauseNodeTypes)) {
580 whereClause = " where ((" + clauseNodeTypes + ") ";
581 if (StringUtils.isNotBlank(clauseWorkspacePath)) {
582 whereClause += "and " + clauseWorkspacePath;
583 }
584 whereClause += ") ";
585 } else {
586 whereClause = " where ";
587 }
588
589 log.debug("JCR query WHERE clause is {}", whereClause);
590 return whereClause;
591 }
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615 protected String getQueryWhereClauseNodeTypes() {
616
617 List<String> defs = new ArrayList<String>();
618
619 for (NodeType nt : getSearchableNodeTypes()) {
620 if (nt.isNodeType(NodeTypes.Folder.NAME)) {
621 continue;
622 }
623 if (nt.isMixin()) {
624
625 defs.add("[jcr:mixinTypes] = '" + nt.getName() + "'");
626 } else {
627 defs.add("[jcr:primaryType] = '" + nt.getName() + "'");
628 }
629 }
630 return StringUtils.join(defs, " or ");
631 }
632
633
634
635
636 protected final String getQueryWhereClauseWorkspacePath() {
637
638 String whereClauseWorkspacePath = "";
639 String path = contentConnectorDefinition.getRootPath();
640 if (StringUtils.isNotBlank(path) && !"/".equals(path)) {
641 whereClauseWorkspacePath = String.format(WHERE_TEMPLATE_FOR_PATH, path);
642 }
643 log.debug("Workspace path where-clause is {}", whereClauseWorkspacePath);
644 return whereClauseWorkspacePath;
645 }
646
647
648
649
650 protected String getQuerySelectStatement() {
651 return SELECT_TEMPLATE;
652 }
653
654
655
656
657
658 @Deprecated
659 protected String getMainNodeType() {
660 final List<NodeTypeDefinition> nodeTypes = contentConnectorDefinition.getNodeTypes();
661 return nodeTypes.isEmpty() ? DEFAULT_NODE_TYPE : nodeTypes.get(0).getName();
662 }
663
664
665
666
667 public void updateSize() {
668 final String stmt = constructJCRQuery(false);
669 try {
670
671 final QueryResult queryResult = executeQuery(stmt, QUERY_LANGUAGE, 0, 0);
672
673 final long pageSize = queryResult.getRows().getSize();
674 log.debug("Query result set contains {} items", pageSize);
675
676 updateCount((int) pageSize);
677 } catch (RepositoryException e) {
678 updateCount(0);
679 handleRepositoryException(log, "Could not update size with statement: " + stmt, e);
680 }
681 }
682
683 @Override
684 public List<?> getItemIds(int startIndex, int numberOfItems) {
685 return ContainerHelpers.getItemIdsUsingGetIdByIndex(startIndex, numberOfItems, this);
686 }
687
688 public String getWorkspace() {
689 return contentConnectorDefinition.getWorkspace();
690 }
691
692 public Set<NodeType> getSearchableNodeTypes() {
693 return searchableNodeTypes;
694 }
695
696
697
698
699
700 @Override
701 public void refresh() {
702 resetOffset();
703 clearItemIndexes();
704 updateSize();
705 fireItemSetChange();
706 }
707
708 protected void resetOffset() {
709 currentOffset = 0;
710 }
711
712 protected void clearItemIndexes() {
713 itemIndexes.clear();
714 }
715
716 protected int getCurrentOffset() {
717 return currentOffset;
718 }
719
720 protected void setSize(int size) {
721 this.size = size;
722 }
723
724 protected QueryResult executeQuery(String statement, String language, long limit, long offset) throws RepositoryException {
725 final Session jcrSession = MgnlContext.getJCRSession(getWorkspace());
726 final QueryManager jcrQueryManager = jcrSession.getWorkspace().getQueryManager();
727 final Query query = jcrQueryManager.createQuery(statement, language);
728 if (limit > 0) {
729 query.setLimit(limit);
730 }
731 if (offset >= 0) {
732 query.setOffset(offset);
733 }
734 log.debug("Executing query against workspace [{}] with statement [{}] and limit {} and offset {}...", new Object[] { getWorkspace(), statement, limit, offset });
735 long start = System.currentTimeMillis();
736 final QueryResult result = query.execute();
737 log.debug("Query execution took {} ms", System.currentTimeMillis() - start);
738
739 return result;
740 }
741
742
743
744
745
746
747
748
749 protected void handleRepositoryException(final Logger logger, final String message, final RepositoryException repositoryException) {
750 logger.warn(message + ": " + repositoryException);
751 }
752
753
754
755
756
757
758
759
760
761
762 protected Set<NodeType> findSearchableNodeTypes() {
763 final List<String> hiddenInList = new ArrayList<String>();
764 final List<NodeTypeDefinition> nodeTypeDefinition = contentConnectorDefinition.getNodeTypes();
765 log.debug("Defined node types are {}", nodeTypeDefinition);
766
767 for (NodeTypeDefinition def : nodeTypeDefinition) {
768 if (def.isHideInList()) {
769 log.debug("{} is hidden in list/search therefore it won't be displayed there.", def.getName());
770 hiddenInList.add(def.getName());
771 }
772 }
773
774 final Set<NodeType> searchableNodeTypes = new HashSet<NodeType>();
775 if (getWorkspace() == null) {
776
777 return searchableNodeTypes;
778 }
779 try {
780 final NodeTypeManager nodeTypeManager = MgnlContext.getJCRSession(getWorkspace()).getWorkspace().getNodeTypeManager();
781
782 for (NodeTypeDefinition def : nodeTypeDefinition) {
783 final String nodeTypeName = def.getName();
784 if (hiddenInList.contains(nodeTypeName)) {
785 continue;
786 }
787
788 final NodeType nt = nodeTypeManager.getNodeType(nodeTypeName);
789 searchableNodeTypes.add(nt);
790
791 if (def.isStrict()) {
792 log.debug("{} is defined as strict, therefore its possible subtypes won't be taken into account.", nodeTypeName);
793 continue;
794 }
795
796 final NodeTypeIterator subTypesIterator = nt.getSubtypes();
797
798 while (subTypesIterator.hasNext()) {
799 final NodeType subType = subTypesIterator.nextNodeType();
800 final String subTypeName = subType.getName();
801 if (hiddenInList.contains(subTypeName) || subTypeName.startsWith("jcr:") || subTypeName.startsWith("mix:") || subTypeName.startsWith("rep:") || subTypeName.startsWith("nt:")) {
802 continue;
803 }
804 log.debug("Adding {} as subtype of {}", subTypeName, nodeTypeName);
805 searchableNodeTypes.add(subType);
806 }
807 }
808 } catch (LoginException e) {
809 handleRepositoryException(log, e.getMessage(), e);
810
811 } catch (RepositoryException e) {
812 handleRepositoryException(log, e.getMessage(), e);
813 }
814 if (log.isDebugEnabled()) {
815 StringBuilder sb = new StringBuilder("[");
816 for (NodeType nt : searchableNodeTypes) {
817 sb.append(String.format("[%s - isMixin? %s] ", nt.getName(), nt.isMixin()));
818 }
819 sb.append("]");
820 log.debug("Found searchable nodetypes (both primary types and mixins) and their subtypes {}", sb.toString());
821 }
822 return searchableNodeTypes;
823 }
824 }