View Javadoc
1   /**
2    * This file Copyright (c) 2012-2018 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.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   * Vaadin container that reads its items from a JCR repository. Implements a simple mechanism for lazy loading items
80   * from a JCR repository and a cache for items and item ids.
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       * String separating a properties name and the uuid of its node.
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       * Node type to use if none is configured.
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      * Item and index caches.
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      * Page length = number of items contained in one page.
131      */
132     private int pageLength = DEFAULT_PAGE_LENGTH;
133 
134     /**
135      * Number of items to cache = cacheRatio x pageLength.
136      */
137     private int cacheRatio = DEFAULT_CACHE_RATIO;
138 
139     private Set<ItemSetChangeListener> itemSetChangeListeners;
140 
141     /**
142      * Starting row number of the currently fetched page.
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      * Sends an Item set change event to all registered interested listeners.
197      *
198      * @param event
199      *            the item set change event to send, optionally with additional
200      *            information
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     /** Methods from interface Container **/
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      * Gets the number of visible Items in the Container.
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     /** Methods from interface Container.Indexed **/
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             // load in the next page.
326             int nextIndex = (currentOffset / (pageLength * cacheRatio) + 1) * (pageLength * cacheRatio);
327             if (nextIndex >= size) {
328                 // Container wrapped around, start from index 0.
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     /** Methods from interface Container.Ordered **/
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     /** Methods from interface Container.Sortable **/
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     /** UNSUPPORTED CONTAINER FEATURES **/
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      * Determines a new offset for updating the row cache. The offset is calculated from the given index, and will be
461      * fixed to match the start of a page, based on the value of pageLength.
462      *
463      * @param index Index of the item that was requested, but not found in cache
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      * Triggers a refresh if the current row count has changed.
478      */
479     private void updateCount(long newSize) {
480         if (newSize != size) {
481             setSize((int) newSize);
482         }
483     }
484 
485     /**
486      * Fetches a page from the data source based on the values of pageLength, cacheRatio and currentOffset.
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      * Updates this container by storing the items found in the query result passed as argument.
505      *
506      * @see #getPage()
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      * @param considerSorting an optional <code>ORDER BY</code> is added if this parameter is <code>true</code>.
525      * @return a string representing a JCR statement to retrieve this container's items.
526      * It creates a JCR query in the form {@code select * from [nt:base] as selectorName [WHERE] [ORDER BY]"}.
527      * <p>
528      * Subclasses can customize the optional <code>WHERE</code> clause by overriding {@link #getQueryWhereClause()} or, at a more fine-grained level, {@link #getQueryWhereClauseNodeTypes()} and {@link #getQueryWhereClauseWorkspacePath()}.
529      * <p>
530      * A restriction on the node types to be searched for can be applied via {@link #getQueryWhereClauseNodeTypes()}.
531      */
532     protected String constructJCRQuery(final boolean considerSorting) {
533         final String select = getQuerySelectStatement();
534         final StringBuilder stmt = new StringBuilder(select);
535         // Return results only within the node configured in workbench/path
536         stmt.append(getQueryWhereClause());
537 
538         if (considerSorting) {
539             if (sorters.isEmpty()) {
540                 // no sorters set - use defaultOrder (always ascending)
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      * @return an {@link OrderBy} object for the passed in property to be used for the default order by clause.
567      */
568     protected OrderBy getDefaultOrderBy(final String property) {
569         return new OrderBy(property, true);
570     }
571 
572     /**
573      * @return the jcr function used to sort the node name (or jcr name property) column. By default it's {@link #JCR_NAME_FUNCTION}.
574      */
575     protected String getJcrNameOrderByFunction() {
576         return JCR_NAME_FUNCTION;
577     }
578 
579     /**
580      * @return the JCR query where clause to select only node types which are not hidden in list and nodes under the path configured in the workspace as String - if the latter
581      * is not configured return a blank string so that all nodes are considered.
582      * @see AbstractJcrContainer#getQueryWhereClauseNodeTypes()
583      * @see #getQueryWhereClauseWorkspacePath()
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      * @return a String containing the node types to be displayed in a list view and searched for in a query. All node types declared in a workbench definition are returned
605      * unless their <code>hideInList</code> property is true or the node is of type <code>mgnl:folder</code> (custom implementations of this method may still decide to display folders). Assuming a node types declaration like the following
606      *
607      * <pre>
608      * ...
609      * + workbench
610      *  + nodeTypes
611      *   + foo
612      *    * name = nt:foo
613      *   + bar
614      *    * name = nt:bar
615      *    * hideInList = true
616      *   + baz (a mixin type)
617      *    * name = nt:baz
618      * ...
619      * </pre>
620      *
621      * this method will return the following string <code>[jcr:primaryType] = 'nt:foo' or [jcr:mixinTypes] = 'baz'</code>. This will eventually be used to restrict the node types to be displayed in list views and searched for
622      * in search views, i.e. <code>select * from [nt:base] where ([jcr:primaryType] = 'nt:foo' or [jcr:mixinTypes] = 'baz')</code>.
623      * @see #findSearchableNodeTypes()
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                 // Mixin type information is found in jcr:mixinTypes property see http://www.day.com/specs/jcr/2.0/10_Writing.html#10.10.3%20Assigning%20Mixin%20Node%20Types
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      * @return if ((JcrContentConnectorDefinition)workbenchDefinition.getContentConnector()).getPath() is not null or root ("/"), an ISDESCENDATNODE constraint narrowing the scope of search under the configured path, else an empty string.
645      */
646     protected final String getQueryWhereClauseWorkspacePath() {
647         // By default, search the root and therefore do not need a query clause.
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      * @return a <code>SELECT</code> statement whose node type is <code>nt:base</code>. Can be customized by subclasses to utilize other item types, i.e. {@code select * from [my:fancytype]}. Used internally by {@link #constructJCRQuery(boolean)}.
659      */
660     protected String getQuerySelectStatement() {
661         return SELECT_TEMPLATE;
662     }
663 
664     /**
665      * @deprecated since 5.1. All node types declared in the workbench definition are equal, meaning that their position doesn't matter when it comes to which ones are used in a query.
666      * The discriminating factor is the <code>hideInList</code> boolean property. If that property is <code>true</code>, then the node will be excluded from the query.
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      * @see #getPage().
676      */
677     public void updateSize() {
678         final String stmt = constructJCRQuery(false);
679         try {
680             // query for all items in order to get the size
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      * Refreshes the container - clears all caches and resets size and offset. Does NOT remove sorting or filtering
708      * rules!
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      * Central method for uniform treatment of RepositoryExceptions in JcrContainers.
754      *
755      * @param logger logger to be used - passed in so subclasses can still user their proper logger
756      * @param message message to be used in the handling
757      * @param repositoryException exception to be handled
758      */
759     protected void handleRepositoryException(final Logger logger, final String message, final RepositoryException repositoryException) {
760         logger.warn(message + ": " + repositoryException);
761     }
762 
763     /**
764      * @return a Set of searchable {@link NodeType}s. A searchable node type is defined as follows
765      * <ul>
766      * <li>It is a <a href="http://jackrabbit.apache.org/node-types.html">primary or mixin</a> node type configured under <code>/modules/mymodule/apps/myapp/subApps/browser/workbench/nodeTypes</code>
767      * <li>It is not hidden in list and search views (property <code>hideInList=true</code>). By default nodes are not hidden.
768      * <li>If not strict (property <code>strict=false</code>), then its subtypes (if any) are searchable too. By default nodes are not defined as strict.
769      * <li>Subtypes beginning with <code>jcr:, nt:, mix:, rep:</code> are not taken into account.
770      * </ul>
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             // no workspace, no searchable types
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 }