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