View Javadoc
1   /**
2    * This file Copyright (c) 2012-2015 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.data.Container;
74  import com.vaadin.data.ContainerHelpers;
75  import com.vaadin.data.Item;
76  import com.vaadin.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     private 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         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     /** Methods from interface Container **/
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      * Gets the number of visible Items in the Container.
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     /** Methods from interface Container.Indexed **/
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             // load in the next page.
316             int nextIndex = (currentOffset / (pageLength * cacheRatio) + 1) * (pageLength * cacheRatio);
317             if (nextIndex >= size) {
318                 // Container wrapped around, start from index 0.
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     /** Methods from interface Container.Ordered **/
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     /** Methods from interface Container.Sortable **/
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     /** UNSUPPORTED CONTAINER FEATURES **/
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      * Determines a new offset for updating the row cache. The offset is calculated from the given index, and will be
451      * fixed to match the start of a page, based on the value of pageLength.
452      *
453      * @param index Index of the item that was requested, but not found in cache
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      * Triggers a refresh if the current row count has changed.
468      */
469     private void updateCount(long newSize) {
470         if (newSize != size) {
471             setSize((int) newSize);
472         }
473     }
474 
475     /**
476      * Fetches a page from the data source based on the values of pageLength, cacheRatio and currentOffset.
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      * Updates this container by storing the items found in the query result passed as argument.
495      *
496      * @see #getPage()
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      * @param considerSorting an optional <code>ORDER BY</code> is added if this parameter is <code>true</code>.
515      * @return a string representing a JCR statement to retrieve this container's items.
516      * It creates a JCR query in the form {@code select * from [nt:base] as selectorName [WHERE] [ORDER BY]"}.
517      * <p>
518      * 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()}.
519      * <p>
520      * A restriction on the node types to be searched for can be applied via {@link #getQueryWhereClauseNodeTypes()}.
521      */
522     protected final String constructJCRQuery(final boolean considerSorting) {
523         final String select = getQuerySelectStatement();
524         final StringBuilder stmt = new StringBuilder(select);
525         // Return results only within the node configured in workbench/path
526         stmt.append(getQueryWhereClause());
527 
528         if (considerSorting) {
529             if (sorters.isEmpty()) {
530                 // no sorters set - use defaultOrder (always ascending)
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      * @return an {@link OrderBy} object for the passed in property to be used for the default order by clause.
557      */
558     protected OrderBy getDefaultOrderBy(final String property) {
559         return new OrderBy(property, true);
560     }
561 
562     /**
563      * @return the jcr function used to sort the node name (or jcr name property) column. By default it's {@link #JCR_NAME_FUNCTION}.
564      */
565     protected String getJcrNameOrderByFunction() {
566         return JCR_NAME_FUNCTION;
567     }
568 
569     /**
570      * @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
571      * is not configured return a blank string so that all nodes are considered.
572      * @see AbstractJcrContainer#getQueryWhereClauseNodeTypes()
573      * @see #getQueryWhereClauseWorkspacePath()
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      * @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
595      * 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
596      *
597      * <pre>
598      * ...
599      * + workbench
600      *  + nodeTypes
601      *   + foo
602      *    * name = nt:foo
603      *   + bar
604      *    * name = nt:bar
605      *    * hideInList = true
606      *   + baz (a mixin type)
607      *    * name = nt:baz
608      * ...
609      * </pre>
610      *
611      * 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
612      * in search views, i.e. <code>select * from [nt:base] where ([jcr:primaryType] = 'nt:foo' or [jcr:mixinTypes] = 'baz')</code>.
613      * @see #findSearchableNodeTypes()
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                 // 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
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      * @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.
635      */
636     protected final String getQueryWhereClauseWorkspacePath() {
637         // By default, search the root and therefore do not need a query clause.
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      * @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)}.
649      */
650     protected String getQuerySelectStatement() {
651         return SELECT_TEMPLATE;
652     }
653 
654     /**
655      * @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.
656      * 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.
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      * @see #getPage().
666      */
667     public void updateSize() {
668         final String stmt = constructJCRQuery(false);
669         try {
670             // query for all items in order to get the size
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      * Refreshes the container - clears all caches and resets size and offset. Does NOT remove sorting or filtering
698      * rules!
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      * Central method for uniform treatment of RepositoryExceptions in JcrContainers.
744      *
745      * @param logger logger to be used - passed in so subclasses can still user their proper logger
746      * @param message message to be used in the handling
747      * @param repositoryException exception to be handled
748      */
749     protected void handleRepositoryException(final Logger logger, final String message, final RepositoryException repositoryException) {
750         logger.warn(message + ": " + repositoryException);
751     }
752 
753     /**
754      * @return a Set of searchable {@link NodeType}s. A searchable node type is defined as follows
755      * <ul>
756      * <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>
757      * <li>It is not hidden in list and search views (property <code>hideInList=true</code>). By default nodes are not hidden.
758      * <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.
759      * <li>Subtypes beginning with <code>jcr:, nt:, mix:, rep:</code> are not taken into account.
760      * </ul>
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             // no workspace, no searchable types
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 }