/* * Part of the CCNx Java Library. * * Copyright (C) 2008-2012 Palo Alto Research Center, Inc. * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 2.1 * as published by the Free Software Foundation. * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. You should have received * a copy of the GNU Lesser General Public License along with this library; * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, * Fifth Floor, Boston, MA 02110-1301 USA. */ package org.ccnx.ccn.profiles.search; import java.io.IOException; import java.util.HashSet; import java.util.LinkedList; import java.util.Set; import org.ccnx.ccn.CCNContentHandler; import org.ccnx.ccn.CCNHandle; import org.ccnx.ccn.impl.support.Log; import org.ccnx.ccn.profiles.VersioningProfile; import org.ccnx.ccn.protocol.ContentName; import org.ccnx.ccn.protocol.ContentObject; import org.ccnx.ccn.protocol.Interest; /** * Implementation of some very specific, first pass search functionality. * Assume we have a starting point node N, e.g. * * N.name() = /a/b/c/d/e * * and we want to find a piece of content at some node along that path with the * final name components <postfix>, e.g. valid data would be one of: * /a/b/c/d/e/<postfix> * /a/b/c/d/<postfix> * /a/b/c/<postfix> * /a/b/<postfix> * /a/<postfix> * /<postfix> * * Depending on the requested search, we might want to find either the node with * the postfix <postfix> closest to the starting node (along the path to the root); * or the one farthest from the starting node (closest to the root). * * Eventually allow interests to be more sophisticated, at least via subclassing. **/ public class Pathfinder implements CCNContentHandler { public static class SearchResults { private ContentObject result; private ContentName interestName; private Set<ContentName> excluded; public SearchResults(ContentObject result, ContentName interestName) { this(result, interestName, null); } public SearchResults(ContentObject result, ContentName interestName, Set<ContentName> excluded) { this.result = result; this.interestName = interestName; this.excluded = excluded; } public ContentObject getResult() { return result; } public ContentName getInterestName() { return interestName; } public Set<ContentName> getExcluded() { return excluded; } public void setResult(ContentObject result) { this.result = result; } public void setExcluded(Set<ContentName> excluded) { this.excluded = excluded; } } protected ContentName _startingPoint; protected ContentName _stoppingPoint; // where to stop, inclusive; ROOT if non-null protected ContentName _postfix; protected boolean _closestOnPath; protected boolean _goneOK; protected long _timeout; protected CCNHandle _handle; protected long _startingTime; protected boolean _timedOut = false; protected Set<ContentName> _searchedPathCache; protected SearchResults _searchResult; // In order from startingPoint to root. protected LinkedList<Interest> _outstandingInterests = new LinkedList<Interest>(); /** * Search from startingPoint to stoppingPoint *inclusive* (i.e. stoppingPoint * will be searched). * @param startingPoint * @param stoppingPoint * @param desiredPostfix * @param closestOnPath * @param goneOK * @param timeout * @param searchedPathCache * @param handle * @throws IOException */ public Pathfinder(ContentName startingPoint, ContentName stoppingPoint, ContentName desiredPostfix, boolean closestOnPath, boolean goneOK, int timeout, Set<ContentName> searchedPathCache, CCNHandle handle) throws IOException { _startingPoint = startingPoint; _stoppingPoint = (null == stoppingPoint) ? ContentName.ROOT : stoppingPoint; _postfix = desiredPostfix; _closestOnPath = closestOnPath; _goneOK = goneOK; _timeout = timeout; _searchedPathCache = searchedPathCache; _handle = handle; startSearch(); } protected synchronized void startSearch() throws IOException { // Fire off interests, one per search point. ContentName searchPoint = _startingPoint; Interest theInterest = null; while (searchPoint != null) { if ((null != _searchedPathCache) && (_searchedPathCache.contains(searchPoint))) { Log.finer("Skipping search of point {0}, cached negative result.", searchPoint); } else { Log.finer("Pathfinder searching node {0}", searchPoint); theInterest = constructInterest(searchPoint); _handle.expressInterest(theInterest, this); _outstandingInterests.add(theInterest); } if (searchPoint.equals(_stoppingPoint)) { searchPoint = null; } else { searchPoint = searchPoint.parent(); } } _startingTime = System.currentTimeMillis(); } /** * Separate out so subclasses can override. */ protected Interest constructInterest(ContentName searchPoint) { ContentName targetName = searchPoint.append(_postfix); return new Interest(targetName); } /** * We want to hand back a list of paths we have checked and ruled out to our * caller, who can opt to keep them and not ask about them again (or to cache * them for some time before asking). These would basically be all the prefixes * we timed out on, not the prefixes we removed because we found something at a * closer point and were looking for the closest entry, or at a farther point and * were looking for the farthest entry. */ public synchronized Set<ContentName> stopSearch() { HashSet<ContentName> outstandingPrefixes = new HashSet<ContentName>(); int cutCount = _postfix.count(); ContentName prefixName; for (Interest interest : _outstandingInterests) { if (null != interest) { _handle.cancelInterest(interest, this); prefixName = interest.name().cut(interest.name().count() - cutCount); if (prefixName.isPrefixOf(_startingPoint)) { outstandingPrefixes.add(prefixName); Log.finer("Pathfinder: caching negative result for {0}", prefixName); } else { // we found a gone object, and were trying to find a non-gone child of it // TODO fix this when we change the GONE handling // TODO should this be prefixName or prefixName.parent() ContentName thisName = prefixName; while (!thisName.isPrefixOf(_startingPoint)) { if (thisName.equals(ContentName.ROOT)) { thisName = null; break; } thisName = thisName.parent(); } if (null != thisName) { outstandingPrefixes.add(thisName); Log.finer("Pathfinder: caching negative result for {0}", thisName); } } } } _outstandingInterests.clear(); return outstandingPrefixes; } public boolean goneOK() { return _goneOK; } public boolean seekingClosestMatchOnPath() { return _closestOnPath; } public synchronized SearchResults waitForResults() { // Wait, if woken up see if we're done, we've timed out, or we woke up early. long timeRemaining = _timeout - (System.currentTimeMillis() - _startingTime); while (timeRemaining > 0) { try { Log.finest("Pathfinder: waiting {0} more milliseconds.", timeRemaining); this.wait(timeRemaining); } catch (InterruptedException e) { } timeRemaining = _timeout - (System.currentTimeMillis() - _startingTime); if (done()) { break; } } if (done()) { Log.finer("Pathfinder: found answer, {0}", (null == _searchResult) ? "null" : _searchResult.getResult().name()); if (null == _searchResult) _searchResult = new SearchResults(null, null); return _searchResult; } else { Set<ContentName> excluded = stopSearch(); // Do we return null, as we ran out of time, or the best option // we found? _timedOut = true; Log.finer("Pathfinder: timed out, best answer so far: {0}", (null == _searchResult) ? "null" : _searchResult.getResult().name()); if (null == _searchResult) _searchResult = new SearchResults(null, null); _searchResult.setExcluded(excluded); return _searchResult; } } public boolean timedOut() { return _timedOut; } public Interest handleContent(ContentObject result, Interest interest) { // When we get data back, we can cancel all the outstanding interests in the // direction other than the one we want. // May want to extend this to allow caller to check this content and // go around again if it isn't acceptable. // For right now, we try a simple tack; caller can specify whether GONE // content is OK (only relevant for postfixes that will pull specific // information). If it isn't, and we get GONE content, we put out an // interest looking for a later (non-GONE) version at that point. Log.finer("Pathfinder: Got result matching interest {0}, first name is {1}", interest, result.name()); Interest returnInterest = null; synchronized(this) { int index = _outstandingInterests.indexOf(interest); // if the interest has already been removed from _outstandingInterests, do nothing. if (index == -1) return null; if (result.isGone() && !goneOK()) { Log.finer("Pathfinder found a GONE object when it wasn't looking for one. Replacing interest with one looking for latest version after (0}", result.name()); // TODO this isn't entirely correct -- will look for later versions of the GONE object, but // won't look for other objects that aren't in this one's version chain that aren't GONE; // need to maybe exclude the gone one and ask for that as well, but the interest-splitting // code won't handle more than one outstanding interest per prefix correctly. if (!VersioningProfile.hasTerminalVersion(result.name())) { Log.finer("Pathfinder: GONE object not versioned. Ignoring it and hoping something better comes along."); returnInterest = interest; } else { returnInterest = VersioningProfile.latestVersionInterest(result.name(), null, null); } } else { // we got some content that counts, gone or not. Cancel and remove all the // interests in the wrong direction from this one. // Longer interests (closer to start) are earlier in the list than index; shorter // interests (closer to the root) are later in the list. Interest thisInterest = null; if (_closestOnPath) { // Want the closest match to us on the path. So we want to remove anything // farther away (higher index) than this one. As we're removing items, // we actually never change our index, we just wait for the end of the array // to come down and meet us. Log.finer("Pathfinder: finding {0} closest to {1}, found {2}, removing more distant interests.", _postfix, _startingPoint, result.name()); for (int i=index + 1; i < _outstandingInterests.size(); ) { thisInterest = _outstandingInterests.get(i); _handle.cancelInterest(thisInterest, this); _outstandingInterests.remove(i); } // Still need to remove the interest we are responding to. Do that at the end. } else { // Remove any interests closer to us than the match on the path. Want the farthest // away match. Log.finer("Pathfinder: finding {0} farthest from {1}, found {2}, removing closer interests.", _postfix, _startingPoint, result.name()); for (int i=0; i < index; ++i) { thisInterest = _outstandingInterests.removeFirst(); _handle.cancelInterest(thisInterest, this); _outstandingInterests.remove(i); } } _searchResult = new SearchResults(result, interest.name()); // what if there is more than one } // Order may have changed index = _outstandingInterests.indexOf(interest); _outstandingInterests.remove(index); if (null != returnInterest) { _outstandingInterests.add(index, returnInterest); } if (done()) { // We're done -- this means that the answer was either at this point on the path, // or at the root, depending on whether we're searching for closest or farthest. // Anything else we need to time out. this.notifyAll(); } } return returnInterest; } public boolean done() { return (0 == _outstandingInterests.size()); } }