/*
* 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.exoplatform.services.jcr.impl.core.query.lucene;
import org.exoplatform.services.jcr.dataflow.ItemDataConsumer;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.datamodel.QPath;
import org.exoplatform.services.jcr.datamodel.QPathEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.RepositoryException;
/**
* Implements a ScoreNodeIterator that returns the score nodes in document order.
*/
class DocOrderScoreNodeIterator implements ScoreNodeIterator
{
/** Logger instance for this class */
private static final Logger LOG = LoggerFactory.getLogger("exo.jcr.component.core.DocOrderScoreNodeIterator");
/** A node iterator with ordered nodes */
private ScoreNodeIterator orderedNodes;
/** Unordered list of {@link ScoreNode}[]s. */
private final List<ScoreNode[]> scoreNodes;
/** ItemManager to turn UUIDs into Node instances */
protected final ItemDataConsumer itemMgr;
/**
* Apply document order on the score nodes with this selectorIndex.
*/
private final int selectorIndex;
/**
* Creates a <code>DocOrderScoreNodeIterator</code> that orders the nodes in
* <code>scoreNodes</code> in document order.
*
* @param itemMgr the item manager of the session executing the
* query.
* @param scoreNodes the ids of the matching nodes with their score
* value. <code>List<ScoreNode[]></code>
* @param selectorIndex apply document order on the score nodes with this
* selectorIndex.
*/
DocOrderScoreNodeIterator(ItemDataConsumer itemMgr, List<ScoreNode[]> scoreNodes, int selectorIndex)
{
this.itemMgr = itemMgr;
this.scoreNodes = scoreNodes;
this.selectorIndex = selectorIndex;
}
/**
* {@inheritDoc}
*/
public Object next()
{
return nextScoreNodes();
}
/**
* {@inheritDoc}
*/
public ScoreNode[] nextScoreNodes()
{
initOrderedIterator();
return orderedNodes.nextScoreNodes();
}
/**
* @throws UnsupportedOperationException always.
*/
public void remove()
{
throw new UnsupportedOperationException("remove");
}
/**
* {@inheritDoc}
*/
public void skip(long skipNum)
{
initOrderedIterator();
orderedNodes.skip(skipNum);
}
/**
* {@inheritDoc}
*/
public void skipBack(long skipNum)
{
initOrderedIterator();
orderedNodes.skipBack(skipNum);
}
/**
* Returns the number of nodes in this iterator.
* </p>
* Note: The number returned by this method may differ from the number
* of nodes actually returned by calls to hasNext() / getNextNode()! This
* is because this iterator works on a lazy instantiation basis and while
* iterating over the nodes some of them might have been deleted in the
* meantime. Those will not be returned by getNextNode(). As soon as an
* invalid node is detected, the size of this iterator is adjusted.
*
* @return the number of node in this iterator.
*/
public long getSize()
{
if (orderedNodes != null)
{
return orderedNodes.getSize();
}
else
{
return scoreNodes.size();
}
}
/**
* {@inheritDoc}
*/
public long getPosition()
{
initOrderedIterator();
return orderedNodes.getPosition();
}
/**
* {@inheritDoc}
*/
public boolean hasNext()
{
initOrderedIterator();
return orderedNodes.hasNext();
}
//------------------------< internal >--------------------------------------
/**
* Initializes the NodeIterator in document order
*/
private void initOrderedIterator()
{
if (orderedNodes != null)
{
return;
}
long time = 0;
if (LOG.isDebugEnabled())
{
time = System.currentTimeMillis();
}
ScoreNode[][] nodes = (ScoreNode[][])scoreNodes.toArray(new ScoreNode[scoreNodes.size()][]);
final Set<String> invalidIDs = new HashSet<String>(2);
/** Cache for Nodes obtainer during the order (comparator work) */
final Map<String, NodeData> lcache = new HashMap<String, NodeData>();
do
{
if (invalidIDs.size() > 0)
{
// previous sort run was not successful -> remove failed uuids
List<ScoreNode[]> tmp = new ArrayList<ScoreNode[]>();
for (int i = 0; i < nodes.length; i++)
{
if (!invalidIDs.contains(nodes[i][selectorIndex].getNodeId()))
{
tmp.add(nodes[i]);
}
}
nodes = (ScoreNode[][])tmp.toArray(new ScoreNode[tmp.size()][]);
invalidIDs.clear();
}
try
{
// sort the uuids
Arrays.sort(nodes, new ScoreNodeComparator(lcache, invalidIDs));
}
catch (SortFailedException e)
{
if (LOG.isTraceEnabled())
{
LOG.trace("An exception occurred: " + e.getMessage());
}
}
}
while (invalidIDs.size() > 0);
if (LOG.isDebugEnabled())
{
LOG.debug("" + nodes.length + " node(s) ordered in " + (System.currentTimeMillis() - time) + " ms");
}
orderedNodes = new ScoreNodeIteratorImpl(nodes);
}
private class ScoreNodeComparator implements Comparator<ScoreNode[]>
{
private final Map<String, NodeData> lcache;
private final Set<String> invalidIDs;
public ScoreNodeComparator(Map<String, NodeData> lcache, Set<String> invalidIDs)
{
super();
this.lcache = lcache;
this.invalidIDs = invalidIDs;
}
private NodeData getNode(String id) throws RepositoryException
{
NodeData node = lcache.get(id);
if (node == null)
{
node = (NodeData)itemMgr.getItemData(id);
if (node != null)
lcache.put(id, node);
}
return node;
}
public int compare(final ScoreNode[] nodes1, final ScoreNode[] nodes2)
{
ScoreNode n1 = nodes1[selectorIndex];
ScoreNode n2 = nodes2[selectorIndex];
// handle null values
// null is considered less than any value
if (n1.equals(n2))
{
return 0;
}
else if (n1 == null)
{
return -1;
}
else if (n2 == null)
{
return 1;
}
try
{
NodeData ndata1;
try
{
ndata1 = getNode(n1.getNodeId());
if (ndata1 == null)
throw new RepositoryException("Node not found for " + n1.getNodeId());
}
catch (RepositoryException e)
{
// log.warn("Node " + n1.identifier + " does not exist anymore:
// " + e);
// node does not exist anymore
invalidIDs.add(n1.getNodeId());
throw new SortFailedException();
}
NodeData ndata2;
try
{
ndata2 = getNode(n2.getNodeId());
if (ndata2 == null)
throw new RepositoryException("Node not found for " + n2.getNodeId());
}
catch (RepositoryException e)
{
// log.warn("Node " + n2.identifier + " does not exist anymore:
// " + e);
// node does not exist anymore
invalidIDs.add(n2.getNodeId());
throw new SortFailedException();
}
QPath path1 = ndata1.getQPath();
QPath path2 = ndata2.getQPath();
QPathEntry[] pentries1 = path1.getEntries();
QPathEntry[] pentries2 = path2.getEntries();
// find nearest common ancestor
int commonDepth = 0; // root
while (pentries1.length > commonDepth && pentries2.length > commonDepth)
{
if (pentries1[commonDepth].equals(pentries2[commonDepth]))
{
commonDepth++;
}
else
{
break;
}
}
// path elements at last depth were equal
commonDepth--;
// check if either path is an ancestor of the other
if (pentries1.length - 1 == commonDepth)
{
// path1 itself is ancestor of path2
return -1;
}
if (pentries2.length - 1 == commonDepth)
{
// path2 itself is ancestor of path1
return 1;
}
// Get the ancestor of node1 that is under the common
// ancestor with node2
while (pentries1.length - 1 > commonDepth + 1)
{
String id = ndata1.getParentIdentifier();
try
{
ndata1 = getNode(id);
if (ndata1 == null)
{
throw new RepositoryException("Node not found for " + id);
}
pentries1 = ndata1.getQPath().getEntries();
}
catch (RepositoryException e)
{
invalidIDs.add(id);
throw new SortFailedException();
}
}
// Get the ancestor of node2 that is under the common
// ancestor with node1
while (pentries2.length - 1 > commonDepth + 1)
{
String id = ndata2.getParentIdentifier();
try
{
ndata2 = getNode(id);
if (ndata2 == null)
{
throw new RepositoryException("Node not found for " + id);
}
pentries2 = ndata2.getQPath().getEntries();
}
catch (RepositoryException e)
{
invalidIDs.add(id);
throw new SortFailedException();
}
}
return ndata1.getOrderNumber() - ndata2.getOrderNumber();
}
catch (SortFailedException e)
{
throw e;
}
catch (Exception e)
{
LOG.error("Exception while sorting nodes in document order: " + e.toString(), e);
}
// if we get here something went wrong
// remove both identifiers from array
invalidIDs.add(n1.getNodeId());
invalidIDs.add(n2.getNodeId());
// terminate sorting
throw new SortFailedException();
}
}
/**
* Indicates that sorting failed.
*/
private static final class SortFailedException extends RuntimeException
{
private static final long serialVersionUID = 3079054269187311527L;
}
}