Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/ChangeLogBasedHierarchyMgr.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/ChangeLogBasedHierarchyMgr.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/ChangeLogBasedHierarchyMgr.java (working copy) @@ -29,6 +29,7 @@ import org.apache.jackrabbit.core.state.NodeReferences; import org.apache.jackrabbit.core.state.NodeReferencesId; import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.Name; import javax.jcr.ItemNotFoundException; import javax.jcr.RepositoryException; @@ -68,10 +69,10 @@ * Same as {@link #getPath(ItemId)}} except that the old path is * returned in case of a moved/removed item. * - * @param id - * @return - * @throws ItemNotFoundException - * @throws RepositoryException + * @param id the id of the node for which to retrieve the path. + * @return the path of the item. + * @throws ItemNotFoundException if an item state cannot be found. + * @throws RepositoryException if another error occurs. */ public Path getZombiePath(ItemId id) throws ItemNotFoundException, RepositoryException { @@ -79,6 +80,21 @@ } /** + * Same as {@link #getName(NodeId, NodeId)} except that the old path + * is returned in case of moved/removed item. + * + * @param id the id of the node for which to retrieve the name. + * @param parentId the id of the parent node. + * @return the name of the node. + * @throws ItemNotFoundException if an item state cannot be found. + * @throws RepositoryException if another error occurs. + */ + public Name getZombieName(NodeId id, NodeId parentId) + throws ItemNotFoundException, RepositoryException { + return zombieHierMgr.getName(id, parentId); + } + + /** * Implements an ItemStateManager that is overlayed by a ChangeLog. */ private static class ChangeLogItemStateManager implements ItemStateManager { Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventImpl.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventImpl.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventImpl.java (working copy) @@ -165,6 +165,17 @@ } /** + * Returns a flag indicating whether the child node of this event is a + * shareable node. Only applies to node added/removed events. + * + * @return true for a shareable child node, false + * otherwise. + */ + public boolean isShareableChildNode() { + return eventState.isShareableNode(); + } + + /** * Return a flag indicating whether this is an externally generated event. * * @return true if this is an external event; Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventState.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventState.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventState.java (working copy) @@ -113,6 +113,12 @@ private final boolean external; /** + * If set to true, indicates that the child node of a node + * added or removed event is a shareable node. + */ + private boolean shareableNode = false; + + /** * Creates a new EventState instance. * * @param type the type of this event. @@ -563,6 +569,27 @@ } /** + * Returns a flag indicating whether the child node of this event is a + * shareable node. Only applies to node added/removed events. + * + * @return true for a shareable child node, false + * otherwise. + */ + boolean isShareableNode() { + return shareableNode; + } + + /** + * Sets a new value for the {@link #shareableNode} flag. + * + * @param shareableNode whether the child node is shareable. + * @see #isShareableNode() + */ + void setShareableNode(boolean shareableNode) { + this.shareableNode = shareableNode; + } + + /** * Returns a String representation of this EventState. * * @return a String representation of this EventState. Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventStateCollection.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventStateCollection.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventStateCollection.java (working copy) @@ -173,6 +173,8 @@ // 4) child node removed // 5) node moved // 6) node reordered + // 7) shareable node added + // 8) shareable node removed // cases 1) and 2) are detected with added and deleted states // on the PropertyState itself. // cases 3) and 4) are detected with added and deleted states @@ -183,6 +185,10 @@ // the node that actually got moved. // in case 6) only one node state changes. the state of the // parent node. + // in case 7) parent of added shareable node has new child node + // entry. + // in case 8) parent of removed shareable node has removed child + // node entry. NodeState n = (NodeState) state; if (n.hasOverlayedState()) { @@ -326,6 +332,9 @@ session)); } } + + // create events if n is shareable + createShareableNodeEvents(n, changes, hmgr, stateMgr); } else { // property changed Path path = getPath(state.getId(), hmgr); @@ -359,6 +368,9 @@ nodeType.getQName(), mixins, session)); + + // create events if n is shareable + createShareableNodeEvents(n, changes, hmgr, stateMgr); } else { // property removed // only create an event if node still exists @@ -400,6 +412,9 @@ nodeType.getQName(), mixins, session)); + + // create events if n is shareable + createShareableNodeEvents(n, changes, hmgr, stateMgr); } else { // property created / set NodeState n = (NodeState) changes.get(state.getParentId()); @@ -518,6 +533,67 @@ //----------------------------< internal >---------------------------------- + private void createShareableNodeEvents(NodeState n, + ChangeLog changes, + ChangeLogBasedHierarchyMgr hmgr, + ItemStateManager stateMgr) + throws ItemStateException { + if (n.isShareable()) { + // check if a share was added or removed + for (Iterator added = n.getAddedShares().iterator(); added.hasNext(); ) { + NodeId parentId = (NodeId) added.next(); + // ignore primary parent id + if (n.getParentId().equals(parentId)) { + continue; + } + NodeState parent = (NodeState) changes.get(parentId); + if (parent == null) { + // happens when mix:shareable is added to an existing node + // usually the parent node state is in the change log + // when a node is added to a shared set -> new child node + // entry on parent node state. + parent = (NodeState) stateMgr.getItemState(parentId); + } + Name ntName = getNodeType(parent, session).getQName(); + EventState es = EventState.childNodeAdded(parentId, + getPath(parentId, hmgr), + n.getNodeId(), + getNameElement(n.getNodeId(), parentId, hmgr), + ntName, + parent.getMixinTypeNames(), + session); + es.setShareableNode(true); + events.add(es); + } + for (Iterator removed = n.getRemovedShares().iterator(); removed.hasNext(); ) { + NodeId parentId = (NodeId) removed.next(); + // if this shareable node is removed, only create events for + // parent ids that are not primary + if (n.getParentId().equals(parentId)) { + continue; + } + NodeState parent = (NodeState) changes.get(parentId); + if (parent == null) { + // happens when mix:shareable is removed from an existing + // node. Usually the parent node state is in the change log + // when a node is removed to a shared set -> removed child + // node entry on parent node state. + parent = (NodeState) stateMgr.getItemState(parentId); + } + Name ntName = getNodeType(parent, session).getQName(); + EventState es = EventState.childNodeRemoved(parentId, + getZombiePath(parentId, hmgr), + n.getNodeId(), + getZombieNameElement(n.getNodeId(), parentId, hmgr), + ntName, + parent.getMixinTypeNames(), + session); + es.setShareableNode(true); + events.add(es); + } + } + } + /** * Resolves the node type name in node into a {@link javax.jcr.nodetype.NodeType} * object using the {@link javax.jcr.nodetype.NodeTypeManager} of session. @@ -579,6 +655,61 @@ } /** + * Returns the name element for the node with the given nodeId + * and its parent with parentId. This method is only useful + * if nodeId denotes a shareable node. + * + * @param nodeId the node id of a shareable node. + * @param parentId the id of the parent node. + * @param hmgr the hierarchy manager. + * @return the name element for the node. + * @throws ItemStateException if an error occurs while resolving the name. + */ + private Path.Element getNameElement(NodeId nodeId, + NodeId parentId, + HierarchyManager hmgr) + throws ItemStateException { + try { + Name name = hmgr.getName(nodeId, parentId); + PathBuilder builder = new PathBuilder(); + builder.addFirst(name); + return builder.getPath().getNameElement(); + } catch (RepositoryException e) { + String msg = "Unable to get name for node with id: " + nodeId; + throw new ItemStateException(msg, e); + } + } + + /** + * Returns the zombie (i.e. the old) name element for the node with + * the given nodeId and its parent with parentId. + * This method is only useful if nodeId denotes a shareable + * node. + * + * @param nodeId the node id of a shareable node. + * @param parentId the id of the parent node. + * @param hmgr the hierarchy manager. + * @return the name element for the node. + * @throws ItemStateException if an error occurs while resolving the name. + */ + private Path.Element getZombieNameElement(NodeId nodeId, + NodeId parentId, + ChangeLogBasedHierarchyMgr hmgr) + throws ItemStateException { + try { + Name name = hmgr.getZombieName(nodeId, parentId); + PathBuilder builder = new PathBuilder(); + builder.addFirst(name); + return builder.getPath().getNameElement(); + } catch (RepositoryException e) { + // should never happen actually + String msg = "Unable to resolve zombie name for item: " + nodeId; + log.error(msg); + throw new ItemStateException(msg, e); + } + } + + /** * Resolves the zombie (i.e. the old) path of the Item with id * itemId. * Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CachingIndexReader.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CachingIndexReader.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CachingIndexReader.java (working copy) @@ -56,6 +56,12 @@ private static long currentTick; /** + * BitSet where bits that correspond to document numbers are set for + * shareable nodes. + */ + private final BitSet shareableNodes; + + /** * Cache of nodes parent relation. If an entry in the array is not null, * that means the node with the document number = array-index has the node * with DocId as parent. @@ -105,6 +111,16 @@ super(delegatee); this.cache = cache; this.parents = new DocId[delegatee.maxDoc()]; + this.shareableNodes = new BitSet(); + TermDocs tDocs = delegatee.termDocs( + new Term(FieldNames.SHAREABLE_NODE, "")); + try { + while (tDocs.next()) { + shareableNodes.set(tDocs.doc()); + } + } finally { + tDocs.close(); + } this.cacheInitializer = new CacheInitializer(delegatee); if (initCache) { cacheInitializer.run(); @@ -144,32 +160,34 @@ if (parent == null) { Document doc = document(n, FieldSelectors.UUID_AND_PARENT); - String parentUUID = doc.get(FieldNames.PARENT); - if (parentUUID == null || parentUUID.length() == 0) { + String[] parentUUIDs = doc.getValues(FieldNames.PARENT); + if (parentUUIDs.length == 0 || parentUUIDs[0].length() == 0) { + // root node parent = DocId.NULL; } else { - // only create a DocId from document number if there is no - // existing DocId - if (!existing) { - Term id = new Term(FieldNames.UUID, parentUUID); - TermDocs docs = termDocs(id); - try { - while (docs.next()) { - if (!deleted.get(docs.doc())) { - parent = DocId.create(docs.doc()); - break; + if (shareableNodes.get(n)) { + parent = DocId.create(parentUUIDs); + } else { + if (!existing) { + Term id = new Term(FieldNames.UUID, parentUUIDs[0]); + TermDocs docs = termDocs(id); + try { + while (docs.next()) { + if (!deleted.get(docs.doc())) { + parent = DocId.create(docs.doc()); + break; + } } + } finally { + docs.close(); } - } finally { - docs.close(); } + // if still null, then parent is not in this index, or existing + // DocId was invalid. thus, only allowed to create DocId from uuid + if (parent == null) { + parent = DocId.create(parentUUIDs[0]); + } } - - // if still null, then parent is not in this index, or existing - // DocId was invalid. thus, only allowed to create DocId from uuid - if (parent == null) { - parent = DocId.create(parentUUID); - } } // finally put to cache @@ -379,8 +397,12 @@ public void collect(Term term, TermDocs tDocs) throws IOException { UUID uuid = UUID.fromString(term.text()); while (tDocs.next()) { - NodeInfo info = new NodeInfo(tDocs.doc(), uuid); - docs.put(new Integer(info.docId), info); + int doc = tDocs.doc(); + // skip shareable nodes + if (!shareableNodes.get(doc)) { + NodeInfo info = new NodeInfo(doc, uuid); + docs.put(new Integer(doc), info); + } } } }); @@ -392,9 +414,13 @@ while (tDocs.next()) { Integer docId = new Integer(tDocs.doc()); NodeInfo info = (NodeInfo) docs.get(docId); - info.parent = uuid; - docs.remove(docId); - docs.put(info.uuid, info); + if (info == null) { + // shareable node, see above + } else { + info.parent = uuid; + docs.remove(docId); + docs.put(info.uuid, info); + } } } }); @@ -413,6 +439,9 @@ } else if (info.parent != null) { foreignParents++; parents[info.docId] = DocId.create(info.parent); + } else if (shareableNodes.get(info.docId)) { + Document doc = reader.document(info.docId, FieldSelectors.UUID_AND_PARENT); + parents[info.docId] = DocId.create(doc.getValues(FieldNames.PARENT)); } else { // no parent -> root node parents[info.docId] = DocId.NULL; @@ -491,7 +520,7 @@ void collect(Term term, TermDocs tDocs) throws IOException; } - private static class NodeInfo { + private final static class NodeInfo { final int docId; Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CachingMultiIndexReader.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CachingMultiIndexReader.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CachingMultiIndexReader.java (working copy) @@ -83,17 +83,11 @@ } /** - * Returns the document number of the parent of n or - * -1 if n does not have a parent (n - * is the root node). - * - * @param n the document number. - * @return the document number of n's parent. - * @throws IOException if an error occurs while reading from the index. + * {@inheritDoc} */ - public int getParent(int n) throws IOException { + public int[] getParents(int n, int[] docNumbers) throws IOException { DocId id = getParentDocId(n); - return id.getDocumentNumber(this); + return id.getDocumentNumbers(this, docNumbers); } /** Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ChildAxisQuery.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ChildAxisQuery.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ChildAxisQuery.java (working copy) @@ -701,9 +701,20 @@ long time = System.currentTimeMillis(); Hits childrenHits = new AdaptingHits(); Hits nameHits = new ScorerHits(nameTestScorer); + int[] docs = new int[1]; for (int h = nameHits.next(); h > -1; h = nameHits.next()) { - if (docIds.contains(new Integer(hResolver.getParent(h)))) { - childrenHits.set(h); + docs = hResolver.getParents(h, docs); + if (docs.length == 1) { + // optimize single value + if (docIds.contains(new Integer(docs[0]))) { + childrenHits.set(h); + } + } else { + for (int i = 0; i < docs.length; i++) { + if (docIds.contains(new Integer(docs[i]))) { + childrenHits.set(h); + } + } } } time = System.currentTimeMillis() - time; Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DescendantSelfAxisQuery.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DescendantSelfAxisQuery.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DescendantSelfAxisQuery.java (working copy) @@ -26,6 +26,7 @@ import org.apache.lucene.search.Weight; import org.apache.lucene.search.Sort; import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.ItemManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -269,6 +270,8 @@ private NodeTraversingQueryHits currentTraversal; + private ItemManager itemMgr = session.getItemManager(); + { fetchNextTraversal(); } @@ -419,6 +422,16 @@ private int[] ancestorDocs = new int[2]; /** + * Reusable array that holds document numbers of parents. + */ + private int[] pDocs = new int[1]; + + /** + * Reusable array that holds a single document number. + */ + private final int[] singleDoc = new int[1]; + + /** * Creates a new DescendantSelfAxisScorer. * * @param similarity the Similarity instance to use. @@ -476,12 +489,7 @@ boolean match = subScorer.skipTo(target); if (match) { collectContextHits(); - if (isValid(subScorer.doc())) { - return true; - } else { - // find next valid - return next(); - } + return isValid(subScorer.doc()) || next(); } else { return false; } @@ -532,25 +540,46 @@ } // check if doc is a descendant of one of the context nodes - int parentDoc = hResolver.getParent(doc); + pDocs = hResolver.getParents(doc, pDocs); + if (pDocs.length == 0) { + return false; + } + int ancestorCount = 0; - ancestorDocs[ancestorCount++] = parentDoc; + // can only remember one parent doc per level + ancestorDocs[ancestorCount++] = pDocs[0]; // traverse - while (parentDoc != -1 && (!contextHits.get(parentDoc) || ancestorCount < minLevels)) { - parentDoc = hResolver.getParent(parentDoc); - // resize array if needed - if (ancestorCount == ancestorDocs.length) { - // double the size of the new array - int[] copy = new int[ancestorDocs.length * 2]; - System.arraycopy(ancestorDocs, 0, copy, 0, ancestorDocs.length); - ancestorDocs = copy; + while (pDocs.length != 0) { + boolean valid = false; + for (int i = 0; i < pDocs.length; i++) { + if (ancestorCount >= minLevels && contextHits.get(pDocs[i])) { + valid = true; + break; + } } - ancestorDocs[ancestorCount++] = parentDoc; + if (valid) { + break; + } else { + // load next level + pDocs = getParents(pDocs, singleDoc); + // resize array if needed + if (ancestorCount == ancestorDocs.length) { + // double the size of the new array + int[] copy = new int[ancestorDocs.length * 2]; + System.arraycopy(ancestorDocs, 0, copy, 0, ancestorDocs.length); + ancestorDocs = copy; + } + if (pDocs.length != 0) { + // can only remember one parent doc per level + ancestorDocs[ancestorCount++] = pDocs[0]; + } + } } - if (parentDoc != -1) { - // since current parentDoc is a descendant of one of the context + + if (pDocs.length > 0) { + // since current parentDocs are descendants of one of the context // docs we can promote all ancestorDocs to the context hits for (int i = 0; i < ancestorCount; i++) { contextHits.set(ancestorDocs[i]); @@ -559,5 +588,31 @@ } return false; } + + /** + * Returns the parent document numbers for the given docs. + * + * @param docs the current document numbers, for which to get the + * parents. + * @param pDocs an array of document numbers for reuse as return value. + * @return the parent document number for the given docs. + * @throws IOException if an error occurs while reading from the index. + */ + private int[] getParents(int[] docs, int[] pDocs) throws IOException { + // optimize single doc + if (docs.length == 1) { + return hResolver.getParents(docs[0], pDocs); + } else { + pDocs = new int[0]; + for (int i = 0; i < docs.length; i++) { + int[] p = hResolver.getParents(docs[i], new int[0]); + int[] tmp = new int[p.length + pDocs.length]; + System.arraycopy(pDocs, 0, tmp, 0, pDocs.length); + System.arraycopy(p, 0, tmp, pDocs.length, p.length); + pDocs = tmp; + } + return pDocs; + } + } } } Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DocId.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DocId.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DocId.java (working copy) @@ -27,6 +27,8 @@ */ abstract class DocId { + static final int[] EMPTY = new int[0]; + /** * Indicates a null DocId. Will be returned if the root node is asked for * its parent. @@ -34,12 +36,14 @@ static final DocId NULL = new DocId() { /** - * Always returns -1. + * Always returns an empty array. * @param reader the index reader. - * @return always -1. + * @param docNumbers a int array for reuse as return value. + * @return always an empty array. */ - final int getDocumentNumber(MultiIndexReader reader) { - return -1; + final int[] getDocumentNumbers(MultiIndexReader reader, + int[] docNumbers) { + return EMPTY; } /** @@ -62,15 +66,22 @@ }; /** - * Returns the document number of this DocId. If this id is - * invalid -1 is returned. + * Returns the document numbers of this DocId. An empty array + * is returned if this id is invalid. * - * @param reader the IndexReader to resolve this DocId. - * @return the document number of this DocId or -1 - * if it is invalid (e.g. does not exist). + * @param reader the IndexReader to resolve this DocId. + * @param docNumbers an array for reuse. An implementation should use the + * passed array as a container for the return value, + * unless the length of the returned array is different + * from docNumbers. In which case an + * implementation will create a new array with an + * appropriate size. + * @return the document numbers of this DocId or + * empty if it is invalid (e.g. does not exist). * @throws IOException if an error occurs while reading from the index. */ - abstract int getDocumentNumber(MultiIndexReader reader) throws IOException; + abstract int[] getDocumentNumbers(MultiIndexReader reader, int[] docNumbers) + throws IOException; /** * Applies an offset to this DocId. The returned DocId @@ -123,6 +134,16 @@ return new UUIDDocId(uuid); } + /** + * Creates a DocId that references multiple UUIDs. + * + * @param uuids the UUIDs of the referenced nodes. + * @return a DocId based on multiple node UUIDs. + */ + static DocId create(String[] uuids) { + return new MultiUUIDDocId(uuids); + } + //--------------------------< internal >------------------------------------ /** @@ -147,8 +168,13 @@ /** * @inheritDoc */ - int getDocumentNumber(MultiIndexReader reader) { - return docNumber; + int[] getDocumentNumbers(MultiIndexReader reader, int[] docNumbers) { + if (docNumbers.length == 1) { + docNumbers[0] = docNumber; + return docNumbers; + } else { + return new int[]{docNumber}; + } } /** @@ -208,7 +234,8 @@ /** * @inheritDoc */ - int getDocumentNumber(MultiIndexReader reader) throws IOException { + int[] getDocumentNumbers(MultiIndexReader reader, int[] docNumbers) + throws IOException { int realDoc = -1; ForeignSegmentDocId segDocId = doc; if (segDocId != null) { @@ -222,12 +249,18 @@ doc = segDocId; } } - return realDoc; + + if (docNumbers.length == 1) { + docNumbers[0] = realDoc; + return docNumbers; + } else { + return new int[]{realDoc}; + } } /** * This implementation will return this. Document number is - * not known until resolved in {@link #getDocumentNumber(MultiIndexReader)}. + * not known until resolved in {@link #getDocumentNumbers(MultiIndexReader,int[])}. * * @inheritDoc */ @@ -254,4 +287,76 @@ return "UUIDDocId(" + new UUID(msb, lsb) + ")"; } } + + /** + * A DocId based on multiple UUIDDocIds. + */ + private static final class MultiUUIDDocId extends DocId { + + /** + * The internal uuid based doc ids. + */ + private final UUIDDocId[] docIds; + + /** + * @param uuids the uuids of the referenced nodes. + * @throws IllegalArgumentException if one of the uuids is malformed. + */ + MultiUUIDDocId(String[] uuids) { + this.docIds = new UUIDDocId[uuids.length]; + for (int i = 0; i < uuids.length; i++) { + docIds[i] = new UUIDDocId(UUID.fromString(uuids[i])); + } + } + + /** + * @inheritDoc + */ + int[] getDocumentNumbers(MultiIndexReader reader, int[] docNumbers) + throws IOException { + int[] tmp = new int[1]; + docNumbers = new int[docIds.length]; + for (int i = 0; i < docNumbers.length; i++) { + docNumbers[i] = docIds[i].getDocumentNumbers(reader, tmp)[0]; + } + return docNumbers; + } + + /** + * This implementation will return this. Document number is + * not known until resolved in {@link #getDocumentNumbers(MultiIndexReader,int[])}. + * + * @inheritDoc + */ + DocId applyOffset(int offset) { + return this; + } + + /** + * Always returns true. + * + * @param deleted the deleted documents. + * @return always true. + */ + boolean isValid(BitSet deleted) { + return true; + } + + /** + * Returns a String representation for this DocId. + * + * @return a String representation for this DocId. + */ + public String toString() { + StringBuffer sb = new StringBuffer("MultiUUIDDocId("); + String separator = ""; + for (int i = 0; i < docIds.length; i++) { + sb.append(separator); + separator = ", "; + sb.append(new UUID(docIds[i].msb, docIds[i].lsb)); + } + sb.append(")"); + return sb.toString(); + } + } } Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldNames.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldNames.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldNames.java (working copy) @@ -107,6 +107,11 @@ public static final String REINDEXING_REQUIRED = "_:REINDEXING_REQUIRED".intern(); /** + * Name of the field that marks shareable nodes. + */ + public static final String SHAREABLE_NODE = "_:SHAREABLE_NODE".intern(); + + /** * Returns a named length for use as a term in the index. The named length * is of the form: propertyName + '[' + * {@link LongField#longToString(long)}. Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ForeignSegmentDocId.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ForeignSegmentDocId.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ForeignSegmentDocId.java (working copy) @@ -26,6 +26,11 @@ final class ForeignSegmentDocId extends DocId { /** + * Empty array of {@link ForeignSegmentDocId}s. + */ + static final ForeignSegmentDocId[] EMPTY_ARRAY = new ForeignSegmentDocId[0]; + + /** * The document number. */ private final int docNumber; @@ -64,13 +69,23 @@ /** * @inheritDoc */ - int getDocumentNumber(MultiIndexReader reader) throws IOException { - return reader.getDocumentNumber(this); + int[] getDocumentNumbers(MultiIndexReader reader, int[] docNumbers) throws IOException { + int doc = reader.getDocumentNumber(this); + if (doc == -1) { + return EMPTY; + } else { + if (docNumbers.length == 1) { + docNumbers[0] = doc; + return docNumbers; + } else { + return new int[]{doc}; + } + } } /** * This implementation will return this. Document number is - * not known until resolved in {@link #getDocumentNumber(MultiIndexReader)}. + * not known until resolved in {@link DocId#getDocumentNumbers(MultiIndexReader,int[])}. * * {@inheritDoc} */ @@ -82,7 +97,7 @@ * Always returns true because this calls is in context of the * index segment where this DocId lives. Within this segment this DocId is * always valid. Whether the target of this DocId is valid can only be - * checked in the method {@link #getDocumentNumber(MultiIndexReader)}. + * checked in the method {@link DocId#getDocumentNumbers(MultiIndexReader,int[])}. * * @param deleted the deleted documents in the segment where this DocId * lives. Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/HierarchyResolver.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/HierarchyResolver.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/HierarchyResolver.java (working copy) @@ -25,14 +25,20 @@ public interface HierarchyResolver { /** - * Returns the document number of the parent of n or - * -1 if n does not have a parent (n - * is the root node). + * Returns the document number of the parent of n or an empty + * array if n does not have a parent (n is the + * root node). * - * @param n the document number. + * @param n the document number. + * @param docNumbers an array for reuse. An implementation should use the + * passed array as a container for the return value, + * unless the length of the returned array is different + * from docNumbers. In which case an + * implementation will create a new array with an + * appropriate size. * @return the document number of n's parent. - * @throws java.io.IOException if an error occurs while reading from the index. + * @throws java.io.IOException if an error occurs while reading from the + * index. */ - int getParent(int n) throws IOException; - + int[] getParents(int n, int[] docNumbers) throws IOException; } Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexMigration.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexMigration.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexMigration.java (working copy) @@ -85,15 +85,12 @@ } } finally { reader.release(); + index.releaseWriterAndReaders(); } // if we get here then the index must be migrated log.debug("Index requires migration {}", indexDir); - // make sure readers are closed, otherwise the directory - // cannot be deleted - index.releaseWriterAndReaders(); - String migrationName = index.getName() + "_v2.3"; if (directoryManager.hasDirectory(migrationName)) { directoryManager.delete(migrationName); Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitIndexReader.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitIndexReader.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitIndexReader.java (working copy) @@ -82,8 +82,8 @@ /** * {@inheritDoc} */ - public int getParent(int n) throws IOException { - return resolver.getParent(n); + public int[] getParents(int n, int[] docNumbers) throws IOException { + return resolver.getParents(n, docNumbers); } //-------------------------< MultiIndexReader >----------------------------- Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NodeIndexer.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NodeIndexer.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NodeIndexer.java (working copy) @@ -186,28 +186,24 @@ // UUID doc.add(new Field( FieldNames.UUID, node.getNodeId().getUUID().toString(), - Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO)); + Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS)); try { // parent UUID if (node.getParentId() == null) { // root node - doc.add(new Field(FieldNames.PARENT, "", Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO)); + doc.add(new Field(FieldNames.PARENT, "", Field.Store.YES, + Field.Index.NOT_ANALYZED_NO_NORMS)); addNodeName(doc, "", ""); + } else if (node.getSharedSet().isEmpty()) { + addParentChildRelation(doc, node.getParentId()); } else { - doc.add(new Field( - FieldNames.PARENT, node.getParentId().toString(), - Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO)); - NodeState parent = (NodeState) stateProvider.getItemState(node.getParentId()); - ChildNodeEntry child = parent.getChildNodeEntry(node.getNodeId()); - if (child == null) { - // this can only happen when jackrabbit - // is running in a cluster. - throw new RepositoryException( - "Missing child node entry for node with id: " - + node.getNodeId()); + // shareable node + for (Iterator it = node.getSharedSet().iterator(); it.hasNext(); ) { + addParentChildRelation(doc, (NodeId) it.next()); } - Name name = child.getName(); - addNodeName(doc, name.getNamespaceURI(), name.getLocalName()); + // mark shareable nodes + doc.add(new Field(FieldNames.SHAREABLE_NODE, "", + Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS)); } } catch (NoSuchItemStateException e) { throwRepositoryException(e); @@ -890,4 +886,32 @@ doc.add(new Field(FieldNames.LOCAL_NAME, localName, Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS)); } } + + /** + * Adds a parent child relation to the given doc. + * + * @param doc the document. + * @param parentId the id of the parent node. + * @throws ItemStateException if the parent node cannot be read. + * @throws RepositoryException if the parent node does not have a child node + * entry for the current node. + */ + protected void addParentChildRelation(Document doc, + NodeId parentId) + throws ItemStateException, RepositoryException { + doc.add(new Field( + FieldNames.PARENT, parentId.toString(), + Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO)); + NodeState parent = (NodeState) stateProvider.getItemState(parentId); + ChildNodeEntry child = parent.getChildNodeEntry(node.getNodeId()); + if (child == null) { + // this can only happen when jackrabbit + // is running in a cluster. + throw new RepositoryException( + "Missing child node entry for node with id: " + + node.getNodeId()); + } + Name name = child.getName(); + addNodeName(doc, name.getNamespaceURI(), name.getLocalName()); + } } Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ParentAxisQuery.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ParentAxisQuery.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ParentAxisQuery.java (working copy) @@ -312,14 +312,25 @@ final IOException[] ex = new IOException[1]; contextScorer.score(new HitCollector() { + + private int[] docs = new int[1]; + public void collect(int doc, float score) { try { - doc = hResolver.getParent(doc); - if (doc != -1) { - hits.set(doc); + docs = hResolver.getParents(doc, docs); + if (docs.length == 1) { + // optimize single value + hits.set(docs[0]); if (score != DEFAULT_SCORE.floatValue()) { - scores.put(new Integer(doc), new Float(score)); + scores.put(new Integer(docs[0]), new Float(score)); } + } else { + for (int i = 0; i < docs.length; i++) { + hits.set(docs[i]); + if (score != DEFAULT_SCORE.floatValue()) { + scores.put(new Integer(docs[i]), new Float(score)); + } + } } } catch (IOException e) { ex[0] = e; Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SearchIndex.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SearchIndex.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SearchIndex.java (working copy) @@ -1410,11 +1410,11 @@ /** * @inheritDoc */ - public int getParent(int n) throws IOException { + public int[] getParents(int n, int[] docNumbers) throws IOException { int i = readerIndex(n); DocId id = subReaders[i].getParentDocId(n - starts[i]); id = id.applyOffset(starts[i]); - return id.getDocumentNumber(this); + return id.getDocumentNumbers(this, docNumbers); } //-------------------------< MultiIndexReader >------------------------- Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SearchManager.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SearchManager.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SearchManager.java (working copy) @@ -387,8 +387,18 @@ if (e.isExternal()) { removedNodes.add(e.getChildId()); } + if (e.isShareableChildNode()) { + // simply re-index shareable nodes + removedNodes.add(e.getChildId()); + } } else if (type == Event.NODE_REMOVED) { removedNodes.add(e.getChildId()); + if (e.isShareableChildNode()) { + // check if there is a node remaining in the shared set + if (itemMgr.hasItemState(e.getChildId())) { + addedNodes.put(e.getChildId(), e); + } + } } else { propEvents.add(e); } Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeState.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeState.java (revision 761214) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeState.java (working copy) @@ -797,6 +797,36 @@ } } + /** + * Returns a set of shares that were added. + * + * @return the set of shares that were added. Set of {@link NodeId}s. + */ + public synchronized Set getAddedShares() { + if (!hasOverlayedState() || !isShareable()) { + return Collections.EMPTY_SET; + } + NodeState other = (NodeState) getOverlayedState(); + HashSet set = new HashSet(sharedSet); + set.removeAll(other.sharedSet); + return set; + } + + /** + * Returns a set of shares that were removed. + * + * @return the set of shares that were removed. Set of {@link NodeId}s. + */ + public synchronized Set getRemovedShares() { + if (!hasOverlayedState() || !isShareable()) { + return Collections.EMPTY_SET; + } + NodeState other = (NodeState) getOverlayedState(); + HashSet set = new HashSet(other.sharedSet); + set.removeAll(sharedSet); + return set; + } + //--------------------------------------------------< ItemState overrides > /** Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/ShareableNodesTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/ShareableNodesTest.java (revision 0) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/ShareableNodesTest.java (revision 0) @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.observation; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Workspace; +import javax.jcr.observation.Event; + +import org.apache.jackrabbit.test.api.observation.EventResult; +import org.apache.jackrabbit.core.NodeImpl; + +/** + * ShareableNodesTest... + */ +public class ShareableNodesTest extends AbstractObservationTest { + + public void testAddShareableMixin() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + testRootNode.save(); + + EventResult result = new EventResult(log); + addEventListener(result); + + n1.addMixin(mixShareable); + testRootNode.save(); + + Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + for (int i = 0; i < events.length; i++) { + assertFalse("must not contain node added event", events[i].getType() == Event.NODE_ADDED); + assertFalse("must not contain node removed event", events[i].getType() == Event.NODE_REMOVED); + } + } + + public void testAddShare() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = testRootNode.addNode(nodeName2); + Node s = n1.addNode(nodeName3); + s.addMixin(mixShareable); + testRootNode.save(); + + EventResult result = new EventResult(log); + addEventListener(result); + + Workspace wsp = superuser.getWorkspace(); + wsp.clone(wsp.getName(), s.getPath(), n2.getPath() + "/" + s.getName(), false); + + checkNodeAdded(result.getEvents(DEFAULT_WAIT_TIMEOUT), + new String[]{nodeName2 + "/" + nodeName3}, + new String[0]); + } + + public void testRemoveShare() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = testRootNode.addNode(nodeName2); + Node s = n1.addNode(nodeName3); + s.addMixin(mixShareable); + testRootNode.save(); + + Workspace wsp = superuser.getWorkspace(); + wsp.clone(wsp.getName(), s.getPath(), n2.getPath() + "/" + s.getName(), false); + + EventResult result = new EventResult(log); + addEventListener(result); + + removeFromSharedSet(n2.getNode(nodeName3)); + testRootNode.save(); + + checkNodeRemoved(result.getEvents(DEFAULT_WAIT_TIMEOUT), + new String[]{nodeName2 + "/" + nodeName3}, + new String[0]); + } + + protected void removeFromSharedSet(Node node) throws RepositoryException { + ((NodeImpl) node).removeShare(); + } +} Property changes on: jackrabbit-core\src\test\java\org\apache\jackrabbit\core\observation\ShareableNodesTest.java ___________________________________________________________________ Added: svn:eol-style + native Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/TestAll.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/TestAll.java (revision 761214) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/TestAll.java (working copy) @@ -42,6 +42,7 @@ suite.addTestSuite(MoveInPlaceTest.class); suite.addTestSuite(EventJournalTest.class); suite.addTestSuite(UserDataTest.class); + suite.addTestSuite(ShareableNodesTest.class); return suite; } Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ShareableNodeTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ShareableNodeTest.java (revision 0) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ShareableNodeTest.java (revision 0) @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Workspace; +import javax.jcr.NodeIterator; + +import org.apache.jackrabbit.core.NodeImpl; + +/** + * ShareableNodeTest performs query tests with shareable nodes. + */ +public class ShareableNodeTest extends AbstractQueryTest { + + public void testPathConstraint() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = testRootNode.addNode(nodeName2); + Node s = n1.addNode(nodeName3); + s.setProperty(propertyName1, "value"); + s.addMixin(mixShareable); + Node n4 = s.addNode(nodeName4); + n4.setProperty(propertyName2, "value"); + testRootNode.save(); + + Workspace wsp = superuser.getWorkspace(); + wsp.clone(wsp.getName(), s.getPath(), n2.getPath() + "/" + s.getName(), false); + + String stmt = testPath + "/" + nodeName1 + "/*[@" + propertyName1 + "='value']"; + NodeIterator nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + stmt = testPath + "/" + nodeName2 + "/*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + stmt = testPath + "//*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + stmt = testPath + "//*[@" + propertyName2 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", n4.isSame(nodes.nextNode())); + + // remove a node from the shared set + ((NodeImpl) s).removeShare(); + testRootNode.save(); + + s = n2.getNode(nodeName3); + + stmt = testPath + "/" + nodeName1 + "/*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + + stmt = testPath + "/" + nodeName2 + "/*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + stmt = testPath + "//*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + // remove remaining node from the shared set + ((NodeImpl) s).removeShare(); + testRootNode.save(); + + stmt = testPath + "/" + nodeName1 + "/*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + + stmt = testPath + "/" + nodeName2 + "/*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + + stmt = testPath + "//*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + } + + public void testName() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = testRootNode.addNode(nodeName2); + Node s = n1.addNode(nodeName3); + s.addMixin(mixShareable); + testRootNode.save(); + + Workspace wsp = superuser.getWorkspace(); + wsp.clone(wsp.getName(), s.getPath(), n2.getPath() + "/" + nodeName4, false); + + String stmt = testPath + "//" + nodeName3; + NodeIterator nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + stmt = testPath + "//" + nodeName4; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + // remove a node from the shared set + ((NodeImpl) s).removeShare(); + testRootNode.save(); + + s = n2.getNode(nodeName4); + + stmt = testPath + "//" + nodeName3; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + + stmt = testPath + "//" + nodeName4; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + // remove remaining node from the shared set + ((NodeImpl) s).removeShare(); + testRootNode.save(); + + stmt = testPath + "//" + nodeName3; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + + stmt = testPath + "//" + nodeName4; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + } +} Property changes on: jackrabbit-core\src\test\java\org\apache\jackrabbit\core\query\ShareableNodeTest.java ___________________________________________________________________ Added: svn:eol-style + native Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/TestAll.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/TestAll.java (revision 761214) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/TestAll.java (working copy) @@ -59,6 +59,7 @@ suite.addTestSuite(IndexingAggregateTest.class); suite.addTestSuite(IndexFormatVersionTest.class); suite.addTestSuite(IndexingRuleTest.class); + suite.addTestSuite(ShareableNodeTest.class); return suite; } Index: jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/AbstractJCRTest.java =================================================================== --- jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/AbstractJCRTest.java (revision 761214) +++ jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/AbstractJCRTest.java (working copy) @@ -185,6 +185,11 @@ protected String mixLockable; /** + * JCR Name mix:shareable using the namespace resolver of the current session. + */ + protected String mixShareable; + + /** * JCR Name nt:query using the namespace resolver of the current session. */ protected String ntQuery; @@ -327,6 +332,7 @@ mixReferenceable = superuser.getNamespacePrefix(NS_MIX_URI) + ":referenceable"; mixVersionable = superuser.getNamespacePrefix(NS_MIX_URI) + ":versionable"; mixLockable = superuser.getNamespacePrefix(NS_MIX_URI) + ":lockable"; + mixShareable = superuser.getNamespacePrefix(NS_MIX_URI) + ":shareable"; ntQuery = superuser.getNamespacePrefix(NS_NT_URI) + ":query"; // setup custom namespaces