/* * 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; import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.logging.Level; import org.ccnx.ccn.CCNHandle; import org.ccnx.ccn.ContentVerifier; import org.ccnx.ccn.config.SystemConfiguration; import org.ccnx.ccn.impl.support.DataUtils; import org.ccnx.ccn.impl.support.Log; import org.ccnx.ccn.impl.support.Tuple; import org.ccnx.ccn.protocol.CCNTime; import org.ccnx.ccn.protocol.Component; import org.ccnx.ccn.protocol.ContentName; import org.ccnx.ccn.protocol.ContentObject; import org.ccnx.ccn.protocol.Exclude; import org.ccnx.ccn.protocol.ExcludeAny; import org.ccnx.ccn.protocol.ExcludeComponent; import org.ccnx.ccn.protocol.Interest; import org.ccnx.ccn.protocol.PublisherID; import org.ccnx.ccn.protocol.PublisherPublicKeyDigest; /** * Versions, when present, usually occupy the penultimate component of the CCN name, * not counting the digest component. A name may actually incorporate multiple * versions, where the rightmost version is the version of "this" object, if it * has one, and previous (parent) versions are the versions of the objects of * which this object is a part. The most common location of a version, if present, * is in the next to last component of the name, where the last component is a * segment number (which is generally always present; versions themselves are * optional). More complicated segmentation profiles occur, where a versioned * object has components that are structured and named in ways other than segments -- * and may themselves have individual versions (e.g. if the components of such * a composite object are written as CCNNetworkObjects and automatically pick * up an (unnecessary) version in their own right). Versioning operations therefore * take context from their caller about where to expect to find a version, * and attempt to ignore other versions in the name. * * Versions may be chosen based on time. * The first byte of the version component is 0xFD. The remaining bytes are a * big-endian binary number. If based on time they are expressed in units of * 2**(-12) seconds since the start of Unix time, using the minimum number of * bytes. The time portion will thus take 48 bits until quite a few centuries * from now (Sun, 20 Aug 4147 07:32:16 GMT). With 12 bits of precision, it allows * for sub-millisecond resolution. The client generating the version stamp * should try to avoid using a stamp earlier than (or the same as) any * version of the file, to the extent that it knows about it. It should * also avoid generating stamps that are unreasonably far in the future. * * Get latest version is going to exclude [B, 0xFD00FFFFFFFFFF, 0xFE000000000000,B], * so you need to be sure to use version numbers in those bounds. */ public class VersioningProfile implements CCNProfile { public static final byte VERSION_MARKER = (byte)0xFD; public static final byte FF = (byte) 0xFF; public static final byte O1 = (byte) 0x01; public static final byte OO = (byte) 0x00; public static final byte [] FIRST_VERSION_MARKER = new byte []{VERSION_MARKER}; public static final byte [] LAST_VERSION_MARKER = new byte [] {VERSION_MARKER+1, OO, OO, OO, OO, OO, OO }; // Due to shortlex comparison, need to have something 7 bytes long public static final byte [] MIN_VERSION_MARKER = new byte [] {VERSION_MARKER, O1, OO, OO, OO, OO, OO}; public static final byte [] MAX_VERSION_MARKER = new byte [] {VERSION_MARKER, FF, FF, FF, FF, FF, FF }; public static final byte [] BOTTOM_EXCLUDE_VERSION_MARKER = MIN_VERSION_MARKER; public static final byte [] TOP_EXCLUDE_VERSION_MARKER = LAST_VERSION_MARKER; /** * Add a version field to a ContentName. * @version should be a CCNTime toBinaryTimeAsLong() not getTime() * @return ContentName with a version appended. Does not affect previous versions. */ public static ContentName addVersion(ContentName name, long version) { // Need a minimum-bytes big-endian representation of version. byte [] vcomp = null; if (0 == version) { vcomp = FIRST_VERSION_MARKER; } else { vcomp = DataUtils.unsignedLongToByteArray(version, VERSION_MARKER); } return new ContentName(name, vcomp); } /** * Converts a timestamp into a fixed point representation, with 12 bits in the fractional * component, and adds this to the ContentName as a version field. The timestamp is rounded * to the nearest value in the fixed point representation. * <p> * This allows versions to be recorded as a timestamp with a 1/4096 second accuracy. * <p> * Get latest version is going to exclude [B, 0xFD00FFFFFFFFFF, 0xFE000000000000,B], * so you need to be sure to use version numbers in those bounds. * <p> * @see #addVersion(ContentName, long) * @deprecated Use new ContentName(name, version) instead. */ @Deprecated public static ContentName addVersion(ContentName name, CCNTime version) { if (null == version) throw new IllegalArgumentException("Version cannot be null!"); return new ContentName(name, version); } /** * Add a version field based on the current time, accurate to 1/4096 second. * <p> * Get latest version is going to exclude [B, 0xFD00FFFFFFFFFF, 0xFE000000000000,B], * so you need to be sure to use version numbers in those bounds. * <p> * @see #addVersion(ContentName, long) */ public static ContentName addVersion(ContentName name) { return new ContentName(name, CCNTime.now()); } public static byte [] timeToVersionComponent(CCNTime version) { byte [] varr = version.toBinaryTime(); byte [] vcomp = new byte[varr.length + 1]; vcomp[0] = VERSION_MARKER; System.arraycopy(varr, 0, vcomp, 1, varr.length); return vcomp; } public static String printAsVersionComponent(CCNTime version) { byte [] vcomp = timeToVersionComponent(version); return Component.printURI(vcomp); } /** * Adds a version to a ContentName; if there is a terminal version there already, * first removes it. */ public static ContentName updateVersion(ContentName name, long version) { return addVersion(cutTerminalVersion(name).first(), version); } /** * Adds a version to a ContentName; if there is a terminal version there already, * first removes it. */ public static ContentName updateVersion(ContentName name, CCNTime version) { return new ContentName(cutTerminalVersion(name).first(), version); } /** * Add updates the version field based on the current time, accurate to 1/4096 second. * @see #updateVersion(ContentName, Timestamp) */ public static ContentName updateVersion(ContentName name) { return updateVersion(name, CCNTime.now()); } /** * Finds the last component that looks like a version in name. * @param name * @return the index of the last version component in the name, or -1 if there is no version * component in the name */ public static int findLastVersionComponent(ContentName name) { int i = name.count(); for (;i >= 0; i--) if (isVersionComponent(name.component(i))) return i; return -1; } /** * Checks to see if this name has a validly formatted version field anywhere in it. */ public static boolean containsVersion(ContentName name) { return findLastVersionComponent(name) != -1; } /** * Checks to see if this name has a validly formatted version field either in final * component or in next to last component with final component being a segment marker. */ public static boolean hasTerminalVersion(ContentName name) { if ((name.count() > 0) && ((isVersionComponent(name.lastComponent()) || ((name.count() > 1) && SegmentationProfile.isSegment(name) && isVersionComponent(name.component(name.count()-2)))))) { return true; } return false; } /** * Check a name component to see if it is a valid version field */ public static boolean isVersionComponent(byte [] nameComponent) { return (null != nameComponent) && (0 != nameComponent.length) && (VERSION_MARKER == nameComponent[0]) && ((nameComponent.length == 1) || (nameComponent[1] != 0)) && (nameComponent.length <= 11); } public static boolean isBaseVersionComponent(byte [] nameComponent) { return (isVersionComponent(nameComponent) && (1 == nameComponent.length)); } /** * Remove a terminal version marker (one that is either the last component of name, or * the next to last component of name followed by a segment marker) if one exists, otherwise * return name as it was passed in. * @param name * @return */ public static Tuple<ContentName, byte[]> cutTerminalVersion(ContentName name) { if (name.count() > 0) { if (isVersionComponent(name.lastComponent())) { return new Tuple<ContentName, byte []>(name.parent(), name.lastComponent()); } else if ((name.count() > 2) && SegmentationProfile.isSegment(name) && isVersionComponent(name.component(name.count()-2))) { return new Tuple<ContentName, byte []>(name.cut(name.count()-2), name.component(name.count()-2)); } } return new Tuple<ContentName, byte []>(name, null); } /** * Take a name which may have one or more version components in it, * and strips the last one and all following components. If no version components * present, returns the name as handed in. */ public static ContentName cutLastVersion(ContentName name) { int offset = findLastVersionComponent(name); return (offset == -1) ? name : name.cut(offset); } /** * Function to get the version field as a long. Starts from the end and checks each name component for the version marker. * @param name * @return long * @throws VersionMissingException */ public static long getLastVersionAsLong(ContentName name) throws VersionMissingException { int i = findLastVersionComponent(name); if (i == -1) throw new VersionMissingException(); return getVersionComponentAsLong(name.component(i)); } public static byte [] getLastVersionComponent(ContentName name) throws VersionMissingException { int i = findLastVersionComponent(name); if (i == -1) throw new VersionMissingException(); return name.component(i); } public static long getVersionComponentAsLong(final byte [] versionComponent) { return DataUtils.byteArrayToUnsignedLong(versionComponent, 1); } public static CCNTime getVersionComponentAsTimestamp(byte [] versionComponent) { if (null == versionComponent) return null; return versionLongToTimestamp(getVersionComponentAsLong(versionComponent)); } /** * Extract the version from this name as a Timestamp. * @throws VersionMissingException */ public static CCNTime getLastVersionAsTimestamp(ContentName name) throws VersionMissingException { long time = getLastVersionAsLong(name); return CCNTime.fromBinaryTimeAsLong(time); } /** * Returns null if no version, otherwise returns the last version in the name. * @param name * @return */ public static CCNTime getLastVersionAsTimestampIfVersioned(ContentName name) { int versionComponent = findLastVersionComponent(name); if (versionComponent < 0) return null; return getVersionComponentAsTimestamp(name.component(versionComponent)); } public static CCNTime getTerminalVersionAsTimestampIfVersioned(ContentName name) { if (!hasTerminalVersion(name)) return null; int versionComponent = findLastVersionComponent(name); if (versionComponent < 0) return null; return getVersionComponentAsTimestamp(name.component(versionComponent)); } public static CCNTime versionLongToTimestamp(long version) { return CCNTime.fromBinaryTimeAsLong(version); } /** * Control whether versions start at 0 or 1. * @return */ public static final int baseVersion() { return 0; } /** * Compares terminal version (versions at the end of, or followed by only a segment * marker) of a name to a given timestamp. * @param left * @param right * @return */ public static int compareVersions( CCNTime left, ContentName right) { if (!hasTerminalVersion(right)) { throw new IllegalArgumentException("Both names to compare must be versioned!"); } try { return left.compareTo(getLastVersionAsTimestamp(right)); } catch (VersionMissingException e) { throw new IllegalArgumentException("Name that isVersioned returns true for throws VersionMissingException!: " + right); } } public static int compareVersionComponents( byte [] left, byte [] right) throws VersionMissingException { // Propagate correct exception to callers. if ((null == left) || (null == right)) throw new VersionMissingException("Must compare two versions!"); // DKS TODO -- should be able to just compare byte arrays, but would have to check version return getVersionComponentAsTimestamp(left).compareTo(getVersionComponentAsTimestamp(right)); } /** * See if version is a version of parent (not commutative). * @return */ public static boolean isVersionOf(ContentName version, ContentName parent) { Tuple<ContentName, byte []>versionParts = cutTerminalVersion(version); if (!parent.equals(versionParts.first())) { return false; // not versions of the same thing } if (null == versionParts.second()) return false; // version isn't a version return true; } /** * This compares two names, with terminal versions, and determines whether one is later than the other. * @param laterVersion * @param earlierVersion * @return * @throws VersionMissingException */ public static boolean isLaterVersionOf(ContentName laterVersion, ContentName earlierVersion) throws VersionMissingException { Tuple<ContentName, byte []>earlierVersionParts = cutTerminalVersion(earlierVersion); Tuple<ContentName, byte []>laterVersionParts = cutTerminalVersion(laterVersion); if (!laterVersionParts.first().equals(earlierVersionParts.first())) { return false; // not versions of the same thing } return (compareVersionComponents(laterVersionParts.second(), earlierVersionParts.second()) > 0); } /** * Finds out if you have a versioned name, and a ContentObject that might have a versioned name which is * a later version of the given name, even if that CO name might not refer to a segment of the original name. * For example, given a name /parc/foo.txt/<version1> or /parc/foo.txt/<version1>/<segment> * and /parc/foo.txt/<version2>/<stuff>, return true, whether <stuff> is a segment marker, a whole * bunch of repo write information, or whatever. * @param newName Will check to see if this name begins with something which is a later version of previousVersion. * @param previousVersion The name to compare to, must have a terminal version or be unversioned. * @return */ public static boolean startsWithLaterVersionOf(ContentName newName, ContentName previousVersion) { // If no version, treat whole name as prefix and any version as a later version. Tuple<ContentName, byte []>previousVersionParts = cutTerminalVersion(previousVersion); if (!previousVersionParts.first().isPrefixOf(newName)) return false; if (null == previousVersionParts.second()) { return ((newName.count() > previousVersionParts.first().count()) && VersioningProfile.isVersionComponent(newName.component(previousVersionParts.first().count()))); } try { return (compareVersionComponents(newName.component(previousVersionParts.first().count()), previousVersionParts.second()) > 0); } catch (VersionMissingException e) { return false; // newName doesn't have to have a version there... } } public static int compareTerminalVersions(ContentName laterVersion, ContentName earlierVersion) throws VersionMissingException { Tuple<ContentName, byte []>earlierVersionParts = cutTerminalVersion(earlierVersion); Tuple<ContentName, byte []>laterVersionParts = cutTerminalVersion(laterVersion); if (!laterVersionParts.first().equals(earlierVersionParts.first())) { throw new IllegalArgumentException("Names not versions of the same name!"); } return (compareVersionComponents(laterVersionParts.second(), earlierVersionParts.second())); } /** * Builds an Exclude filter that excludes components before or @ start, and components after * the last valid version. * @param startingVersionComponent The latest version component we know about. Can be null or * VersioningProfile.isBaseVersionComponent() == true to indicate that we want to start * from 0 (we don't have a known version we're trying to update). This exclude filter will * find versions *after* the version represented in startingVersionComponent. * @return An exclude filter. */ public static Exclude acceptVersions(byte [] startingVersionComponent) { byte [] start = null; // initially exclude name components just before the first version, whether that is the // 0th version or the version passed in if ((null == startingVersionComponent) || VersioningProfile.isBaseVersionComponent(startingVersionComponent)) { start = new byte [] { VersioningProfile.VERSION_MARKER, VersioningProfile.OO, VersioningProfile.FF, VersioningProfile.FF, VersioningProfile.FF, VersioningProfile.FF, VersioningProfile.FF }; } else { start = startingVersionComponent; } ArrayList<Exclude.Element> ees = new ArrayList<Exclude.Element>(); ees.add(new ExcludeAny()); ees.add(new ExcludeComponent(start)); ees.add(new ExcludeComponent(LAST_VERSION_MARKER)); ees.add(new ExcludeAny()); Log.fine(Log.FAC_IO, "acceptVersions: creating excludes {0} {1}", VersioningProfile.getVersionComponentAsLong(start), VersioningProfile.getVersionComponentAsLong(LAST_VERSION_MARKER)); return new Exclude(ees); } /** * Active methods. Want to provide profile-specific methods that: * - find the latest version without regard to what is below it * - if no version given, gets the latest version * - if a starting version given, gets the latest version available *after* that version; * will time out if no such newer version exists * Returns a content object, which may or may not be a segment of the latest version, but the * latest version information is available from its name. * * - find the first segment of the latest version of a name * - if no version given, gets the first segment of the latest version * - if a starting version given, gets the latest version available *after* that version or times out * Will ensure that what it returns is a segment of a version of that object. * * - generate an interest designed to find the first segment of the latest version * of a name, in the above form; caller is responsible for checking and re-issuing */ /** * Generate an interest that will find the leftmost child of the latest version. It * will ensure that the next to last segment is a version, and the last segment (excluding * digest) is the leftmost child available. But it can't guarantee that the latter is * a segment. Because most data is segmented, length constraints will make it very * likely, however. * @param startingVersion * @return */ public static Interest firstBlockLatestVersionInterest(ContentName startingVersion, PublisherPublicKeyDigest publisher) { // by the time we look for extra components we will have a version on our name if it // doesn't have one already, so look for names with 2 extra components -- segment and digest. return latestVersionInterest(startingVersion, 3, publisher); } /** * Generate an interest that will find a descendant of the latest version of startingVersion, * after any existing version component. If additionalNameComponents is non-null, it will * find a descendant with exactly that many name components after the version (including * the digest). The latest version is the rightmost child of the desired prefix, however, * this interest will find leftmost descendants of that rightmost child. With appropriate * length limitations, can be used to find segments of the latest version (though that * will work more effectively with appropriate segment numbering). */ public static Interest latestVersionInterest(ContentName startingVersion, Integer additionalNameComponents, PublisherPublicKeyDigest publisher) { if (hasTerminalVersion(startingVersion)) { // Has a version. Make sure it doesn't have a segment; find a version after this one. startingVersion = SegmentationProfile.segmentRoot(startingVersion); } else { // Doesn't have a version. Add the "0" version, so we are finding any version after that. ContentName firstVersionName = addVersion(startingVersion, baseVersion()); startingVersion = firstVersionName; } byte [] versionComponent = startingVersion.lastComponent(); Log.fine(Log.FAC_IO, "latestVersionInterest: creating interest - startingVersion = {0}", startingVersion); Interest constructedInterest = Interest.last(startingVersion, acceptVersions(versionComponent), startingVersion.count() - 1, additionalNameComponents, additionalNameComponents, null); if (null != publisher) { constructedInterest.publisherID(new PublisherID(publisher)); } Log.fine(Log.FAC_IO, "latestVersionInterest: created interest = {0}", constructedInterest); return constructedInterest; } /** * Function to (best effort) get the latest version. There may be newer versions available * if you ask again passing in the version found (i.e. each response will be the latest version * a given responder knows about. Further queries will move past that responder to other responders, * who may have newer information.) * * @param name If the name ends in a version then this method explicitly looks for a newer version * than that, and will time out if no such later version exists. If the name does not end in a * version then this call just looks for the latest version. * @param publisher Currently unused, will limit query to a specific publisher. * @param timeout This is the time to wait until you get any response. If nothing is returned, this method will return null. * @param verifier Used to verify the returned content objects * @param handle CCNHandle used to get the latest version * @return A ContentObject with the latest version, or null if the query timed out. * @result Returns a matching ContentObject, verified. * @throws IOException */ public static ContentObject getLatestVersion(ContentName startingVersion, PublisherPublicKeyDigest publisher, long timeout, ContentVerifier verifier, CCNHandle handle) throws IOException { return getLatestVersion(startingVersion, publisher, timeout, verifier, handle, null, false); } /** * Function to (best effort) get the latest version. There may be newer versions available * if you ask again passing in the version found (i.e. each response will be the latest version * a given responder knows about. Further queries will move past that responder to other responders, * who may have newer information.) * * @param name If the name ends in a version then this method explicitly looks for a newer version * than that, and will time out if no such later version exists. If the name does not end in a * version then this call just looks for the latest version. * @param publisher Currently unused, will limit query to a specific publisher. * @param timeout This is the time to wait to retrieve any version. If nothing is returned, this method will return null. * @param verifier Used to verify the returned content objects. * @param handle CCNHandle used to get the latest version. * @param startingSegmentNumber If we are requiring content to be a segment, what segment number * do we want. If null, and findASegment is true, uses SegmentationProfile.baseSegment(). * @param findASegment are we requiring returned content to be a segment of this version * @return A ContentObject with the latest version, or null if the query timed out. * @result Returns a matching ContentObject, verified. * @throws IOException */ private static ContentObject getLatestVersion(ContentName startingVersion, PublisherPublicKeyDigest publisher, long timeout, ContentVerifier verifier, CCNHandle handle, Long startingSegmentNumber, boolean findASegment) throws IOException { if (Log.isLoggable(Log.FAC_IO, Level.FINE)){ Log.fine(Log.FAC_IO, "getFirstBlockOfLatestVersion: getting version later than {0} called with timeout: {1}", startingVersion, timeout); } if (null == verifier) { // TODO DKS normalize default behavior verifier = handle.keyManager().getDefaultVerifier(); } long startTime = System.currentTimeMillis(); long interestTime = 0; long elapsedTime = 0; long respondTime; long remainingTime = timeout; long attemptTimeout = SystemConfiguration.GLV_ATTEMPT_TIMEOUT; boolean noTimeout = false; if (timeout == SystemConfiguration.NO_TIMEOUT) { //glv called with no timeout... should probably return what we have as soon as we have something //to return and have suffered a timeout if (Log.isLoggable(Log.FAC_IO, Level.FINEST)) Log.finest(Log.FAC_IO, "gLV called with NO_TIMEOUT"); noTimeout = true; //if something comes back, we should try for one more interest and then return what we have } else if (timeout == 0) { if (Log.isLoggable(Log.FAC_IO, Level.FINEST)) Log.finest(Log.FAC_IO, "gLV called with timeout = 0, should just return the first thing we get"); } ContentName prefix = startingVersion; if (hasTerminalVersion(prefix)) { prefix = startingVersion.parent(); } int versionedLength = prefix.count() + 1; ContentObject result = null; ContentObject lastResult = null; ArrayList<byte[]> excludeList = new ArrayList<byte[]>(); while ( (remainingTime > 0 && elapsedTime < timeout) || (noTimeout || timeout == 0)) { if (Log.isLoggable(Log.FAC_IO, Level.FINER)) Log.finer(Log.FAC_IO, "gLV timeout: {0} remainingTime: {1} attemptTimeout: {2}", timeout, remainingTime, attemptTimeout); lastResult = result; //attempts++; Interest getLatestInterest = null; if (findASegment) { getLatestInterest = firstBlockLatestVersionInterest(startingVersion, publisher); } else { getLatestInterest = latestVersionInterest(startingVersion, null, publisher); } if (excludeList.size() > 0) { //we have explicit excludes, add them to this interest byte [][] e = new byte[excludeList.size()][]; excludeList.toArray(e); getLatestInterest.exclude().add(e); } if (Log.isLoggable(Log.FAC_IO, Level.FINER)) Log.finer(Log.FAC_IO, "timeout {0} startTime: {1} elapsedTime: {2} remainingTime: {3} new elapsedTime = {4}", timeout, startTime, elapsedTime, remainingTime, (System.currentTimeMillis() - startTime)); interestTime = System.currentTimeMillis(); long tempT; if (noTimeout) { tempT = timeout; } else if (timeout == 0) { tempT = attemptTimeout; } else { if (remainingTime < timeout - elapsedTime) { tempT = remainingTime; } else { tempT = timeout - elapsedTime; } } result = handle.get(getLatestInterest, tempT); elapsedTime = System.currentTimeMillis() - startTime; respondTime = System.currentTimeMillis() - interestTime; if (result == null && respondTime == 0) { Log.warning(Log.FAC_IO, "gLV: handle.get returned null and did not wait the full timeout time for the object (timeout: {0} responseTime: {1}", timeout, respondTime); return null; } remainingTime = timeout - elapsedTime; if (Log.isLoggable(Log.FAC_IO, Level.FINE)) { Log.fine(Log.FAC_IO, "gLV INTEREST: {0}", getLatestInterest); Log.fine(Log.FAC_IO, "gLV trying handle.get with timeout: {0}", tempT); Log.fine(Log.FAC_IO, "gLVTime sending Interest from gLV at {0} started at: {1}", System.currentTimeMillis(), startTime); Log.fine(Log.FAC_IO, "gLVTime returned from handle.get in {0} ms",respondTime); Log.fine(Log.FAC_IO, "gLV remaining time is now {0} ms", remainingTime); } if (null != result){ if (Log.isLoggable(Log.FAC_IO, Level.INFO)) Log.info(Log.FAC_IO, "gLV getLatestVersion: retrieved latest version object {0} type: {1}", result.name(), result.signedInfo().getTypeName()); //did it verify? //if it doesn't verify, we need to try harder to get a different content object (exclude this digest) //make this a loop? if (!verifier.verify(result)) { //excludes = addVersionToExcludes(excludes, result.name()); if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLV result did not verify, trying to find a verifiable answer"); excludeList = addVersionToExcludes(excludeList, result.name()); //note: need to use the full name, but want to exclude this particular digest. This means we can't cut off the segment marker. //Interest retry = new Interest(SegmentationProfile.segmentRoot(result.name()), publisher); //retry.maxSuffixComponents(1); Interest retry = new Interest(result.name(), publisher); boolean verifyDone = false; while (!verifyDone) { if (retry.exclude() == null) retry.exclude(new Exclude()); retry.exclude().add(new byte[][] {result.digest()}); if (Log.isLoggable(Log.FAC_IO, Level.FINE)) { Log.fine(Log.FAC_IO, "gLV result did not verify! doing retry!! {0}", retry); Log.fine(Log.FAC_IO, "gLVTime sending retry interest at {0}", System.currentTimeMillis()); } //try to send the interest with the response time the bad content object was returned with //if (timeout == 0) result = handle.get(retry, attemptTimeout); //else // result = handle.get(retry, respondTime); if (result!=null) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLV we got something back: {0}", result.name()); if (verifier.verify(result)) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLV the returned answer verifies"); verifyDone = true; } else { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLV this answer did not verify either... try again"); } } else { //result is null, we didn't find a verifiable answer if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLV did not get a verifiable answer back"); verifyDone = true; } } //TODO if this is the latest version and we exclude it, we might not have anything to send back... we should reset the starting version if (Log.isLoggable(Log.FAC_IO, Level.FINE)) { Log.fine(Log.FAC_IO, "the latest version did not verify and we might not have anything to send back..."); if (lastResult == null) Log.fine(Log.FAC_IO, "lastResult is null... we have nothing to send back"); else Log.fine(Log.FAC_IO, "lastResult is NOT null, we have something to send back!"); } } if (result!=null) { //else { //it verified! are we done? //first check if we need to get the first segment... if (findASegment) { //yes, we need to have the first segment.... // Now we know the version. Did we luck out and get first block? if (VersioningProfile.isVersionedFirstSegment(prefix, result, startingSegmentNumber)) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "getFirstBlockOfLatestVersion: got first block on first try: " + result.name()); } else { //not the first segment... // This isn't the first block. Might be simply a later (cached) segment, or might be something // crazy like a repo_start_write. So what we want is to get the version of this new block -- if getLatestVersion // is doing its job, we now know the version we want (if we already knew that, we called super.getFirstBlock // above. If we get here, _baseName isn't versioned yet. So instead of taking segmentRoot of what we got, // which works fine only if we have the wrong segment rather than some other beast entirely (like metadata). // So chop off the new name just after the (first) version, and use that. If getLatestVersion is working // right, that should be the right thing. ContentName notFirstBlockVersion = result.name().cut(versionedLength); if (Log.isLoggable(Log.FAC_IO, Level.INFO)) Log.info(Log.FAC_IO, "CHILD SELECTOR FAILURE: getFirstBlockOfLatestVersion: Have version information, now querying first segment of " + startingVersion); // this will verify result = SegmentationProfile.getSegment(notFirstBlockVersion, startingSegmentNumber, null, timeout - elapsedTime, verifier, handle); // now that we have the latest version, go back for the first block. //if this isn't the first segment... then we should exclude it. otherwise, we can use it! if (result == null) { //we couldn't get a new segment... if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLV could not get the first segment of the version we just found... should exclude the version"); //excludes = addVersionToExcludes(excludes, startingVersion); excludeList = addVersionToExcludes(excludeList, notFirstBlockVersion); } } } else { //no need to get the first segment! //this is already verified! } //if result is not null, we really have something to try since it also verified if (result != null) { //this could be our answer... set to lastResult and see if we have time to do better lastResult = result; if (noTimeout) { //we want to keep trying for something new //we don't want to wait forever... we have something to hand back. try one more time and then hand back what we have timeout = attemptTimeout; remainingTime = attemptTimeout; } else if (timeout == 0) { //caller just wants the first answer... if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLV we got an answer and the caller wants the first thing we found, returning"); return result; } else if (remainingTime > 0) { //we still have time to try for a better answer, but we shouldn't wait the full timeout //The full timeout is the time to wait until there the answer is null. //now we should wait the gLV attempt time. if (remainingTime > attemptTimeout) { //this is the first time we got something back remainingTime = attemptTimeout; } else { //we already tried again for a better answer... but we got something again... try again, //since the remaining time is less than the attempt time, don't adjust } if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLV we still have time to try for a better answer: remaining time = {0}", remainingTime); } else { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLV time is up, return what we have"); //attempts = SystemConfiguration.GET_LATEST_VERSION_ATTEMPTS; } } else { //result is null //will be handled below } }//the result verified } //we got something back if (result == null) { // we didn't get anything if (Log.isLoggable(Log.FAC_IO, Level.INFO)) Log.info(Log.FAC_IO, "getFirstBlockOfLatestVersion: no block available for later version of {0}", startingVersion); //we didn't get a new version... we can return the last one we received if it isn't null. if (lastResult!=null) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) { Log.fine(Log.FAC_IO, "gLV returning the last result that wasn't null... "); Log.fine(Log.FAC_IO, "gLV returning: {0}",lastResult.name()); } return lastResult; } else { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLV we didn't get anything, and we haven't had anything at all... try with remaining long timeout"); //if remaining time is done.. then we should return null if (remainingTime > 0) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "we did not get anything back from our interest, but we still have time remaining. timeout: {0} elapsedTime {1} remainingTime {2}", timeout, elapsedTime, remainingTime); timeout = remainingTime; } } } //Log.fine("gLV (after) attemptTimeout: {0} remainingTime: {1} (timeout: {2})", attemptTimeout, remainingTime, timeout); if (result!=null) startingVersion = SegmentationProfile.segmentRoot(result.name()); } if(result!=null) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLV returning: {0}", result.name()); } return result; } /** * Find a particular segment of the latest version of a name * - if no version given, gets the desired segment of the latest version * - if a starting version given, gets the latest version available *after* that version or times out * Will ensure that what it returns is a segment of a version of that object. * Also makes sure to return the latest version with a SegmentationProfile.baseSegment() marker. * * @param desiredName The name of the object we are looking for the first segment of. * If (VersioningProfile.hasTerminalVersion(desiredName) == false), will get latest version it can * find of desiredName. * If desiredName has a terminal version, will try to find the first block of content whose * version is *after* desiredName (i.e. getLatestVersion starting from desiredName). * @param startingSegmentNumber The desired block number, or SegmentationProfile.baseSegment() if null. * @param publisher, if one is specified. * @param timeout * @return The first block of a stream with a version later than desiredName, or null if timeout is reached. * This block is verified. * @throws IOException */ public static ContentObject getFirstBlockOfLatestVersion(ContentName startingVersion, Long startingSegmentNumber, PublisherPublicKeyDigest publisher, long timeout, ContentVerifier verifier, CCNHandle handle) throws IOException { return getLatestVersion(startingVersion, publisher, timeout, verifier, handle, startingSegmentNumber, true); } /** * Single-attempt function to get the first version found; and if there are multiple * versions at the network point where that version is found, it will retrieve * the latest of them. So if your ccnd cache has multiple versions, it will return * the latest of those, but won't move past them to get a later one in the repository * or one hop away. This should be used if you believe there is only a single version * of a piece of content available, or if you care more about fast answers than ensuring * you have the latest version available. * if you ask again passing in the version found, you will find a version later than that * if one is available (i.e. each response will be the latest version * a given responder knows about. Further queries will move past that responder to other responders, * who may have newer information.) * * @param name If the name ends in a version then this method explicitly looks for a newer version * than that, and will time out if no such later version exists. If the name does not end in a * version then this call just looks for the latest version. * @param publisher Currently unused, will limit query to a specific publisher. * @param timeout This is the time to wait until you get any response. If nothing is returned, this method will return null. * @param verifier Used to verify the returned content objects * @param handle CCNHandle used to get the latest version * @return A ContentObject with the latest version, or null if the query timed out. * @result Returns a matching ContentObject, verified. * @throws IOException */ public static ContentObject getAnyLaterVersion(ContentName startingVersion, PublisherPublicKeyDigest publisher, long timeout, ContentVerifier verifier, CCNHandle handle) throws IOException { return getLocalLatestVersion(startingVersion, publisher, timeout, verifier, handle, null, false); } /** * Find a particular segment of the closest version available of this object later than * the version given. If there are multiple versions available at that point, will get * the latest among them. See getAnyLaterVersion. * - if no version given, gets the desired segment of the latest version * - if a starting version given, gets the latest version available *after* that version or times out * Will ensure that what it returns is a segment of a version of that object. * Also makes sure to return the latest version with a SegmentationProfile.baseSegment() marker. * * @param desiredName The name of the object we are looking for the first segment of. * If (VersioningProfile.hasTerminalVersion(desiredName) == false), will get latest version it can * find of desiredName. * If desiredName has a terminal version, will try to find the first block of content whose * version is *after* desiredName (i.e. getLatestVersion starting from desiredName). * @param startingSegmentNumber The desired block number, or SegmentationProfile.baseSegment() if null. * @param publisher, if one is specified. * @param timeout * @return The first block of a stream with a version later than desiredName, or null if timeout is reached. * This block is verified. * @throws IOException */ public static ContentObject getFirstBlockOfAnyLaterVersion(ContentName startingVersion, Long startingSegmentNumber, PublisherPublicKeyDigest publisher, long timeout, ContentVerifier verifier, CCNHandle handle) throws IOException { return getLocalLatestVersion(startingVersion, publisher, timeout, verifier, handle, startingSegmentNumber, true); } protected static ContentObject getLocalLatestVersion(ContentName startingVersion, PublisherPublicKeyDigest publisher, long timeout, ContentVerifier verifier, CCNHandle handle, Long startingSegmentNumber, boolean findASegment) throws IOException { if (Log.isLoggable(Log.FAC_IO, Level.INFO)) Log.info(Log.FAC_IO, "getLocalLatestVersion: getting version later than {0} called with timeout: {1}", startingVersion, timeout); if (null == verifier) { // TODO DKS normalize default behavior verifier = handle.keyManager().getDefaultVerifier(); } //TODO This timeout is set to SystemConfiguration.MEDIUM_TIMEOUT to work around the problem //in ccnd where some interests take >300ms (and sometimes longer, have seen periodic delays >800ms) //when that bug is found and fixed, this can be reduced back to the SHORT_TIMEOUT. //long attemptTimeout = SystemConfiguration.SHORT_TIMEOUT; long attemptTimeout = SystemConfiguration.MEDIUM_TIMEOUT; if (timeout == SystemConfiguration.NO_TIMEOUT) { //the timeout sent in is equivalent to null... try till we don't hear something back //we will reset the remaining time after each return... } else if (timeout > 0 && timeout < attemptTimeout) { attemptTimeout = timeout; } long stopTime = System.currentTimeMillis() + timeout; ContentName prefix = startingVersion; if (hasTerminalVersion(prefix)) { prefix = startingVersion.parent(); } int versionedLength = prefix.count() + 1; ContentObject result = null; ArrayList<byte[]> excludeList = new ArrayList<byte[]>(); while ((null == result) && ((timeout == SystemConfiguration.NO_TIMEOUT) || (System.currentTimeMillis() < stopTime))) { Interest getLatestInterest = null; if (findASegment) { getLatestInterest = firstBlockLatestVersionInterest(startingVersion, publisher); } else { getLatestInterest = latestVersionInterest(startingVersion, null, publisher); } if (excludeList.size() > 0) { //we have explicit excludes, add them to this interest byte [][] e = new byte[excludeList.size()][]; excludeList.toArray(e); getLatestInterest.exclude().add(e); } result = handle.get(getLatestInterest, timeout); if (null != result) { if (Log.isLoggable(Log.FAC_IO, Level.INFO)) Log.info(Log.FAC_IO, "gLLV getLocalLatestVersion: retrieved latest version object {0} type: {1}", result.name(), result.signedInfo().getTypeName()); //did it verify? //if it doesn't verify, we need to try harder to get a different content object (exclude this digest) //make this a loop? if (!verifier.verify(result)) { // DKS TODO FIX -- this is incorrect; even if this one fails to verify it doesn't // mean that the version is bad, if you do this attacker can shadow good versions with bad //excludes = addVersionToExcludes(excludes, result.name()); if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLLV result did not verify, trying to find a verifiable answer"); excludeList = addVersionToExcludes(excludeList, result.name()); //note: need to use the full name, but want to exclude this particular digest. This means we can't cut off the segment marker. //Interest retry = new Interest(SegmentationProfile.segmentRoot(result.name()), publisher); //retry.maxSuffixComponents(1); Interest retry = new Interest(result.name(), publisher); boolean verifyDone = false; while (!verifyDone) { if (retry.exclude() == null) retry.exclude(new Exclude()); retry.exclude().add(new byte[][] {result.digest()}); if (Log.isLoggable(Log.FAC_IO, Level.FINE)) { Log.fine(Log.FAC_IO, "gLLV result did not verify! doing retry!! {0}", retry); Log.fine(Log.FAC_IO, "gLLVTime sending retry interest at {0}", System.currentTimeMillis()); } result = handle.get(retry, attemptTimeout); if (result!=null) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLLV we got something back: {0}", result.name()); if(verifier.verify(result)) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLLV the returned answer verifies"); verifyDone = true; } else { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLLV this answer did not verify either... try again"); } } else { //result is null, we didn't find a verifiable answer if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLLV did not get a verifiable answer back"); verifyDone = true; } } //TODO if this is the latest version and we exclude it, we might not have anything to send back... we should reset the starting version } if (result != null) { //else { //it verified! are we done? //first check if we need to get the first segment... if (findASegment) { //yes, we need to have the first segment.... // Now we know the version. Did we luck out and get first block? if (VersioningProfile.isVersionedFirstSegment(prefix, result, startingSegmentNumber)) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "getFirstBlockOfLatestVersion: got first block on first try: {0}", result.name()); } else { //not the first segment... // This isn't the first block. Might be simply a later (cached) segment, or might be something // crazy like a repo_start_write. So what we want is to get the version of this new block -- if getLatestVersion // is doing its job, we now know the version we want (if we already knew that, we called super.getFirstBlock // above. If we get here, _baseName isn't versioned yet. So instead of taking segmentRoot of what we got, // which works fine only if we have the wrong segment rather than some other beast entirely (like metadata). // So chop off the new name just after the (first) version, and use that. If getLatestVersion is working // right, that should be the right thing. ContentName notFirstBlockVersion = result.name().cut(versionedLength); if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.info(Log.FAC_IO, "CHILD SELECTOR FAILURE: getFirstBlockOfLatestVersion: Have version information, now querying first segment of " + startingVersion); // this will verify //don't count this against the gLV timeout. result = SegmentationProfile.getSegment(notFirstBlockVersion, startingSegmentNumber, null, timeout, verifier, handle); // now that we have the latest version, go back for the first block. //if this isn't the first segment... then we should exclude it. otherwise, we can use it! if(result == null) { //we couldn't get a new segment... if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLV could not get the first segment of the version we just found... should exclude the version"); //excludes = addVersionToExcludes(excludes, startingVersion); excludeList = addVersionToExcludes(excludeList, notFirstBlockVersion); } } } else { //no need to get the first segment! //this is already verified! } } //the result verified } //we got something back } if (result != null) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "gLLV returning: {0}", result.name()); } return result; } /** * Version of isFirstSegment that expects names to be versioned, and allows that desiredName * won't know what version it wants but will want some version. */ public static boolean isVersionedFirstSegment(ContentName desiredName, ContentObject potentialFirstSegment, Long startingSegmentNumber) { if ((null != potentialFirstSegment) && (SegmentationProfile.isSegment(potentialFirstSegment.name()))) { if (Log.isLoggable(Log.FAC_IO, Level.FINER)) Log.finer(Log.FAC_IO, "is {0} a first segment of {1}", potentialFirstSegment.name(), desiredName); // In theory, the segment should be at most a versioning component different from desiredName. // In the case of complex segmented objects (e.g. a KeyDirectory), where there is a version, // then some name components, then a segment, desiredName should contain all of those other // name components -- you can't use the usual versioning mechanisms to pull first segment anyway. if (!desiredName.isPrefixOf(potentialFirstSegment.name())) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "Desired name : {0} is not a prefix of segment: {1}", desiredName, potentialFirstSegment.name()); return false; } int difflen = potentialFirstSegment.name().count() - desiredName.count(); if (difflen > 2) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "Have {0} extra components between {1} and desired {2}", difflen, potentialFirstSegment.name(), desiredName); return false; } // Now need to make sure that if the difference is more than 1, that difference is // a version component. if ((difflen == 2) && (!isVersionComponent(potentialFirstSegment.name().component(potentialFirstSegment.name().count()-2)))) { if (Log.isLoggable(Log.FAC_IO, Level.FINE)) Log.fine(Log.FAC_IO, "The {0} extra component between {1} and desired {2} is not a version.", difflen, potentialFirstSegment.name(), desiredName); } if ((null != startingSegmentNumber) && (SegmentationProfile.baseSegment() != startingSegmentNumber)) { return (startingSegmentNumber.longValue() == SegmentationProfile.getSegmentNumber(potentialFirstSegment.name())); } else { return SegmentationProfile.isFirstSegment(potentialFirstSegment.name()); } } return false; } /** * Adds version components to the exclude list for the getLatestVersion method. * @param excludeList current excludes * @param name component to add to the exclude list * @return updated exclude list */ private static ArrayList<byte[]> addVersionToExcludes(ArrayList<byte[]> excludeList, ContentName name) { try { excludeList.add(VersioningProfile.getLastVersionComponent(name)); } catch (VersionMissingException e) { Log.warning(Log.FAC_IO, "failed to exclude content object version that did not verify: {0}",name); } return excludeList; } public static byte[] versionComponentFromStripped(byte[] bs) { if (null == bs) return null; byte [] versionComponent = new byte[bs.length + 1]; versionComponent[0] = VERSION_MARKER; System.arraycopy(bs, 0, versionComponent, 1, bs.length); return versionComponent; } public static byte[] stripVersionMarker(byte[] version) throws VersionMissingException { if (null == version) return null; if (VERSION_MARKER != version[0]) { throw new VersionMissingException("This is not a version component!"); } byte [] stripped = new byte[version.length - 1]; System.arraycopy(version, 1, stripped, 0, stripped.length); return stripped; } }