/* * Part of the CCNx Java Library. * * Copyright (C) 2008, 2009, 2011, 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.nameenum; import static org.ccnx.ccn.profiles.CommandMarker.COMMAND_MARKER_BASIC_ENUMERATION; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedList; import java.util.logging.Level; import org.ccnx.ccn.CCNContentHandler; import org.ccnx.ccn.CCNContentInterest; import org.ccnx.ccn.CCNHandle; import org.ccnx.ccn.CCNInterestHandler; import org.ccnx.ccn.impl.QueuedContentHandler; import org.ccnx.ccn.impl.support.Log; import org.ccnx.ccn.io.content.ContentDecodingException; import org.ccnx.ccn.io.content.Link; import org.ccnx.ccn.io.content.Collection.CollectionObject; import org.ccnx.ccn.profiles.CommandMarker; import org.ccnx.ccn.profiles.SegmentationProfile; import org.ccnx.ccn.profiles.VersioningProfile; import org.ccnx.ccn.profiles.nameenum.NameEnumerationResponse.NameEnumerationResponseMessage; import org.ccnx.ccn.profiles.nameenum.NameEnumerationResponse.NameEnumerationResponseMessage.NameEnumerationResponseMessageObject; import org.ccnx.ccn.profiles.security.KeyProfile; import org.ccnx.ccn.protocol.ContentName; import org.ccnx.ccn.protocol.ContentObject; import org.ccnx.ccn.protocol.Exclude; import org.ccnx.ccn.protocol.Interest; /** * Implements the base Name Enumerator. Applications register name prefixes. * Each prefix is explored until canceled by the application. This version * supports enumeration with multiple responders (repositories and applications). * * An application can have multiple enumerations active at the same time. * For each prefix, the name enumerator will generate an Interest. Responses * to the Interest will be in the form of Collections (by a * NameEnumeratorResponder and repository implementations). Returned Collections * will be parsed for the enumerated names and sent back to the application * using the callback with the applicable prefix and an array of names in * that namespace. The application is expected to handle duplicate names from * multiple responses and should be able to handle names that are returned, but * may not be available at this time (for example, /a.com/b/c.txt might have * been enumerated but a.com content may not be available). * * @see CCNInterestHandler * @see CCNContentHandler * @see BasicNameEnumeratorListener * @see NameEnumerationResponse * */ public class CCNNameEnumerator implements CCNInterestHandler, CCNContentHandler { protected CCNHandle _handle = null; //protected ArrayList<ContentName> _registeredPrefixes = new ArrayList<ContentName>(); protected BasicNameEnumeratorListener callback; protected ArrayList<ContentName> _registeredNames = new ArrayList<ContentName>(); protected NEHandler _neHandler; /** * A supporting class for CCNNameEnumerator. NERequest objects hold registered prefixes and * their corresponding active interests. * */ private class NERequest{ ContentName prefix = null; ArrayList<Interest> ongoingInterests = new ArrayList<Interest>(); public NERequest(ContentName n) { prefix = n; } Interest getInterest(ContentName in) { for (Interest i : ongoingInterests) if (i.name().equals(in)) return i; return null; } void removeInterest(Interest i) { ongoingInterests.remove(getInterest(i.name())); } void addInterest(Interest i) { if (getInterest(i.name()) == null) ongoingInterests.add(i); } ArrayList<Interest> getInterests() { return ongoingInterests; } public boolean containsInterest(Interest interest) { for (Interest i : ongoingInterests) { if(i.equals(interest)) return true; } return false; } } /** * A supporting class for CCNNameEnumerator. NEResponse objects hold ContentName responses * for incoming name enumeration requests. Each NEResponse flag additionally has a dirty * flag to determine if a new name enumeration response is needed. If there is not any new * information since the last request, a new response will not be sent. * */ private class NEResponse { ContentName prefix = null; boolean dirty = true; public NEResponse(ContentName n) { prefix = n; } boolean isDirty() { return dirty; } void clean() { dirty = false; } void dirty() { dirty = true; } } /** * Class to handle responses via a separate thread */ protected class NEHandler extends QueuedContentHandler<CCNContentInterest> { protected CCNHandle _handle; protected CCNContentHandler _handler; protected NEHandler(CCNHandle handle, CCNContentHandler handler) { _handle = handle; _handler = handler; } @Override public void process(CCNContentInterest ci) { ContentObject c = ci.getContent(); Interest interest = ci.getInterest(); ContentName prefix = interest.name().cut(CommandMarker.COMMAND_MARKER_BASIC_ENUMERATION.getBytes()); NERequest ner = getCurrentRequest(prefix); //need to make sure the prefix is still registered if (ner==null) return; ner.removeInterest(interest); NameEnumerationResponseMessageObject neResponse; ArrayList<ContentName> names = new ArrayList<ContentName>(); LinkedList<Link> links; Interest newInterest = interest; //update: now supports multiple responders! //note: if responseIDs are longer than 1 component, need to revisit interest generation for followups if (c != null) { if (Log.isLoggable(Log.FAC_SEARCH, Level.FINE)) { Log.fine(Log.FAC_SEARCH, "we have a match for: {0} [{1}]", interest.name(), interest.toString()); } ArrayList<Interest> newInterests = new ArrayList<Interest>(); //we want to get new versions of this object newInterest = VersioningProfile.firstBlockLatestVersionInterest(c.name(), null); newInterests.add(newInterest); //does this content object have a response id in it? ContentName responseName = getIdFromName(c.name()); if (responseName==null ) { //no response name... this is an error! Log.warning(Log.FAC_SEARCH, "CCNNameEnumerator received a response without a responseID: {0} matching interest {1}", c.name(), interest.name()); } else { //we have a response name. //supports single component response IDs //if response IDs are hierarchical, we need to avoid exploding the number of Interests we express //if the interest had a responseId in it, we don't need to make a new base interest with an exclude, we would have done this already. if (Log.isLoggable(Log.FAC_SEARCH, Level.FINE)) { Log.fine(Log.FAC_SEARCH, "response id from interest: {0}", getIdFromName(interest.name())); } if(getIdFromName(interest.name()) != null && getIdFromName(interest.name()).count() > 0) { //the interest has a response ID in it already... skip making new base interest } else { //also need to add this responder to the exclude list to find more responders ContentName prefixWithMarker = new ContentName(prefix, CommandMarker.COMMAND_MARKER_BASIC_ENUMERATION.getBytes()); Exclude excludes = interest.exclude(); if(excludes==null) excludes = new Exclude(); excludes.add(new byte[][]{responseName.component(0)}); newInterest = Interest.constructInterest(prefixWithMarker, excludes, null, null, 4, null); //check to make sure the interest isn't already expressed if(!ner.containsInterest(newInterest)) newInterests.add(newInterest); } } try { for(Interest i: newInterests) { _handle.expressInterest(i, _handler); ner.addInterest(i); if (Log.isLoggable(Log.FAC_SEARCH, Level.FINEST)) Log.finest(Log.FAC_SEARCH, "expressed: {0}", i); } } catch (IOException e1) { // error registering new interest Log.warning(Log.FAC_SEARCH, "error registering new interest in handleContent"); Log.warningStackTrace(Log.FAC_SEARCH, e1); } newInterests.clear(); try { //need to make sure that the content object we got back is the first segment of the underlying stream. if (SegmentationProfile.isFirstSegment(c.getContentName())) { neResponse = new NameEnumerationResponseMessageObject(c, _handle); } else { neResponse = new NameEnumerationResponseMessageObject(SegmentationProfile.segmentRoot(c.getContentName()), _handle); Log.fine(Log.FAC_SEARCH, "Discovery interest got a content object that wasn't the base segment, stripping off segment number and opening object with name"); } links = neResponse.contents(); for (Link l: links) { names.add(l.targetName()); } //strip off NEMarker before passing through callback //Note: we must not hold any locks here callback.handleNameEnumerator( interest.name().cut(CommandMarker.COMMAND_MARKER_BASIC_ENUMERATION.getBytes()), names); } catch(ContentDecodingException e) { Log.warning(Log.FAC_SEARCH, "Error parsing Collection from ContentObject in CCNNameEnumerator"); Log.warningStackTrace(Log.FAC_SEARCH, e); } catch(IOException e) { Log.warning(Log.FAC_SEARCH, "error getting CollectionObject from ContentObject in CCNNameEnumerator.handleContent"); Log.warningStackTrace(Log.FAC_SEARCH, e); } } } } protected ArrayList<NEResponse> _handledResponses = new ArrayList<NEResponse>(); protected ArrayList<NERequest> _currentRequests = new ArrayList<NERequest>(); /** * CCNNameEnumerator constructor. Creates a CCNNameEnumerator, sets the CCNHandle, * registers the callback and registers a prefix for enumeration. * * @param prefix ContentName to enumerate names under * @param handle CCNHandle for sending and receiving collection objects during enumeration. * @param c BasicNameEnumeratorListener callback to receive enumeration responses. */ public CCNNameEnumerator(ContentName prefix, CCNHandle handle, BasicNameEnumeratorListener c) throws IOException { _handle = handle; _neHandler = new NEHandler(handle, this); callback = c; registerPrefix(prefix); } /** * CCNNameEnumerator constructor. Creates a CCNNameEnumerator, sets the CCNHandle, and * registers the callback. * * @param handle CCNHandle for sending and receiving collection objects during enumeration. * @param c BasicNameEnumeratorListener callback to receive enumeration responses. */ public CCNNameEnumerator(CCNHandle handle, BasicNameEnumeratorListener c) { _handle = handle; _neHandler = new NEHandler(handle, this); callback = c; } public CCNHandle handle() { return _handle; } /** * Method to register a prefix for name enumeration. A NERequest and initial interest is created for new prefixes. * Prefixes that are already registered return and do not impact the already active registration. * * @param prefix ContentName to enumerate * @throws IOException */ public void registerPrefix(ContentName prefix) throws IOException { synchronized (_currentRequests) { NERequest r = getCurrentRequest(prefix); if (r != null) { // this prefix is already registered... if (Log.isLoggable(Log.FAC_SEARCH, Level.FINE)) Log.fine(Log.FAC_SEARCH, "prefix {0} is already registered... returning", prefix); return; } else { r = new NERequest(prefix); _currentRequests.add(r); } if (Log.isLoggable(Log.FAC_SEARCH, Level.INFO)) Log.info(Log.FAC_SEARCH, "Registered Prefix: {0}", prefix); ContentName prefixMarked = new ContentName(prefix, COMMAND_MARKER_BASIC_ENUMERATION); //we have minSuffixComponents to account for sig, version, seg and digest Interest pi = Interest.constructInterest(prefixMarked, null, null, null, 4, null); r.addInterest(pi); _handle.expressInterest(pi, this); } } /** * Method to cancel active enumerations. The active interests are retrieved from the corresponding * NERequest object for the prefix. Each interest is canceled and the NERequest object is removed * from the list of active enumerations. * * @param prefix ContentName to cancel enumeration * @return boolean Returns if the prefix is successfully canceled. */ public boolean cancelPrefix(ContentName prefix) { if (Log.isLoggable(Log.FAC_SEARCH, Level.INFO)) Log.info(Log.FAC_SEARCH, "cancel prefix: {0} ", prefix); synchronized(_currentRequests) { //cancel the behind the scenes interests and remove from the local ArrayList NERequest r = getCurrentRequest(prefix); if (r != null) { ArrayList<Interest> is = r.getInterests(); if (Log.isLoggable(Log.FAC_SEARCH, Level.FINE)) Log.fine(Log.FAC_SEARCH, "we have {0} interests to cancel", is.size()); Interest i; while (!r.getInterests().isEmpty()) { i=r.getInterests().remove(0); _handle.cancelInterest(i, this); } _currentRequests.remove(r); return (getCurrentRequest(prefix) == null); } return false; } } /** * Callback for name enumeration responses. The results contain CollectionObjects containing the * names under a prefix. The collection objects are matched to registered prefixes and returned * to the calling applications using their registered callback handlers. Each response can create * a new Interest that is used to further enumerate the namespace. The implementation * explicitly handles multiple name enumeration responders. The method may now create multiple * interests to further enumerate the prefix. Please note that the current implementation will * need to be updated if responseIDs are more than one component long. * * @param c ContentObject containing the ContentNames under a registered prefix * @param interest The interest matching or triggering a name enumeration response * * @return Interest Returns a new Interest to further enumerate or null to cancel the interest * that matched these objects. This implementation returns null since new interests are created and * expressed as the returned CollectionObjects are processed. * * @see CollectionObject * @see CCNInterestHandler */ public Interest handleContent(ContentObject c, Interest interest) { if (interest.name().contains(COMMAND_MARKER_BASIC_ENUMERATION)) { //the NEMarker is in the name... good! } else { //COMMAND_MARKER_BASIC_ENUMERATION missing... we have a problem Log.warning(Log.FAC_SEARCH, "the name enumeration marker is missing... shouldn't have gotten this callback"); return null; } if (Log.isLoggable(Log.FAC_SEARCH, Level.FINE)) { Log.fine(Log.FAC_SEARCH, "NE: received a response for interest {0}", interest); } _neHandler.add(new CCNContentInterest(c, interest)); return null; } /** * Method for receiving Interests matching the namespace for answering name enumeration requests. Incoming Interests are * verified to have the name enumeration marker. The NEResponse matching the interest is found (if it already exists) and if * new names have been registered under the prefix or if no matching NEResponse object is found, a name enumeration * response is created. * * @param interest Interest object matching the namespace filter. * * @return boolean */ public boolean handleInterest(Interest interest) { boolean result = false; ContentName responseName = null; Link match; NameEnumerationResponseMessage nem; ContentName name = null; NEResponse r = null; if (Log.isLoggable(Log.FAC_SEARCH, Level.FINER)) { Log.finer(Log.FAC_SEARCH, "got an interest: {0}",interest.name()); } name = interest.name(); nem = new NameEnumerationResponseMessage(); //Verify NameEnumeration Marker is in the name int cmbe = name.containsWhere(COMMAND_MARKER_BASIC_ENUMERATION); if (cmbe < 0) { //Skip... we don't handle these } else { name = name.cut(cmbe); responseName = new ContentName(name, COMMAND_MARKER_BASIC_ENUMERATION); boolean skip = false; synchronized (_handledResponses) { //have we handled this response already? r = getHandledResponse(name); if (r != null) { //we have handled this before! if (r.isDirty()) { //this has updates to send back!! } else { //nothing new to send back... go ahead and skip to next interest skip = true; } } else { //this is a new one... r = new NEResponse(name); _handledResponses.add(r); } if (!skip) { for (ContentName n: _registeredNames) { if (name.isPrefixOf(n) && name.count() < n.count()) { ContentName tempName = new ContentName(n.component(name.count())); match = new Link(tempName); if (!nem.contents().contains(match)) { nem.add(match); } } } } if (nem.size() > 0) { try { ContentName responseNameWithId = KeyProfile.keyName(responseName, _handle.keyManager().getDefaultKeyID()); NameEnumerationResponseMessageObject nemobj = new NameEnumerationResponseMessageObject(responseNameWithId, nem, _handle); nemobj.saveLaterWithClose(interest); result = true; if (Log.isLoggable(Log.FAC_SEARCH, Level.FINE)) { Log.fine(Log.FAC_SEARCH, "Saved collection object in name enumeration: " + nemobj.getVersionedName()); } r.clean(); } catch(IOException e) { Log.warning(Log.FAC_SEARCH, "error processing an incoming interest.. dropping and returning"); Log.warningStackTrace(Log.FAC_SEARCH, e); return false; } } if (Log.isLoggable(Log.FAC_SEARCH, Level.FINER)) Log.finer(Log.FAC_SEARCH, "this interest did not have any matching names... not returning anything."); if (r != null) r.clean(); } //end of synchronized } //end of name enumeration marker check return result; } /** * Method to check if a name is already registered to be included in name enumeration responses for incoming Interests. * * @param name ContentName to check for in registered names for responses * @return boolean Returns true if the name is registered and false if not */ public boolean containsRegisteredName(ContentName name) { if (name == null) { Log.warning(Log.FAC_SEARCH, "trying to check for null registered name"); return false; } synchronized(_handledResponses) { if (_registeredNames.contains(name)) return true; else return false; } } /** * Method to register a namespace for filtering incoming Interests * * @param name ContentName to register for filtering incoming Interests * @throws IOException * * @see CCNInterestHandler */ public void registerNameSpace(ContentName name) throws IOException { synchronized(_handledResponses) { if (!_registeredNames.contains(name)) { _registeredNames.add(name); _handle.registerFilter(name, this); } } } /** * Method to register a name to include in incoming name enumeration requests. * * @param name ContentName to register for name enumeration responses */ public void registerNameForResponses(ContentName name) { if (name == null) { Log.warning(Log.FAC_SEARCH, "The content name for registerNameForResponses was null, ignoring"); return; } //Do not need to register each name as a filter... the namespace should cover it synchronized(_handledResponses) { if (!_registeredNames.contains(name)) { // DKS - if we don't care about order, could use a Set instead of an ArrayList, // then just call add as duplicates suppressed _registeredNames.add(name); } //check prefixes that were handled... if so, mark them dirty updateHandledResponses(name); } } /** * Method to get the NEResponse object for a registered name. Returns null if no matching NEResponse is found. * * @param n ContentName identifying a NEResponse * @return NEResponse Returns the NEResponse matching the name. */ protected NEResponse getHandledResponse(ContentName n) { //Log.info("checking handled responses..."); synchronized (_handledResponses) { for (NEResponse t: _handledResponses) { if (t.prefix.equals(n)) return t; } return null; } } /** * Method to set the dirty flag for NEResponse objects that are updated as new names are registered for responses. * * @param n New ContentName to be included in name enumeration responses */ protected void updateHandledResponses(ContentName n) { synchronized (_handledResponses) { for (NEResponse t: _handledResponses) { if (t.prefix.isPrefixOf(n)) { t.dirty(); } } } } /** * Method to get the corresponding NERequest for a ContentName. Returns null * if no NERequest is found. * * @param n ContentName for the NERequest to be found. * * @return NERequest NERequest instance with the supplied ContentName. * Returns null if no NERequest exists. */ protected NERequest getCurrentRequest(ContentName n) { synchronized (_currentRequests) { for (NERequest r : _currentRequests) { if (r.prefix.equals(n)) return r; } return null; } } /** * Method to cancel more than one prefix at a time. This method will cancel all active Interests * matching the prefix supplied. The matching NERequest objects are removed from the set of active * registered prefixes and the corresponding Interests are canceled. * * @param prefixToCancel */ public void cancelEnumerationsWithPrefix(ContentName prefixToCancel) { if (Log.isLoggable(Log.FAC_SEARCH, Level.INFO)) Log.info(Log.FAC_SEARCH, "cancel prefix: {0}",prefixToCancel); synchronized(_currentRequests) { //cancel the behind the scenes interests and remove from the local ArrayList ArrayList<NERequest> toRemove = new ArrayList<NERequest>(); for(NERequest n: _currentRequests){ if(prefixToCancel.isPrefixOf(n.prefix)) toRemove.add(n); } while(!toRemove.isEmpty()){ if(cancelPrefix(toRemove.remove(0).prefix)) if (Log.isLoggable(Log.FAC_SEARCH, Level.INFO)) Log.info(Log.FAC_SEARCH, "cancelled prefix: {0}", prefixToCancel); else if (Log.isLoggable(Log.FAC_SEARCH, Level.INFO)) Log.info(Log.FAC_SEARCH, "could not cancel prefix: {0}", prefixToCancel); } } } private ContentName getIdFromName(ContentName name) { //get the response id, could be more than one component and have a version in it ContentName responseName = null; try { int index = name.containsWhere(COMMAND_MARKER_BASIC_ENUMERATION); ContentName prefix = name.subname(index+1, name.count()); if(VersioningProfile.hasTerminalVersion(prefix)) responseName = VersioningProfile.cutLastVersion(prefix); else responseName = prefix; if (Log.isLoggable(Log.FAC_SEARCH, Level.FINEST)) Log.finest(Log.FAC_SEARCH, "NameEnumeration response ID: {0}", responseName); } catch(Exception e) { return null; } return responseName; } }