/*
* Part of the CCNx Java Library.
*
* Copyright (C) 2008, 2009, 2010 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.io;
import java.io.IOException;
import java.util.EnumSet;
import java.util.logging.Level;
import org.ccnx.ccn.CCNHandle;
import org.ccnx.ccn.config.SystemConfiguration;
import org.ccnx.ccn.impl.security.crypto.ContentKeys;
import org.ccnx.ccn.impl.support.Log;
import org.ccnx.ccn.io.content.CCNNetworkObject;
import org.ccnx.ccn.io.content.ContentDecodingException;
import org.ccnx.ccn.io.content.ContentGoneException;
import org.ccnx.ccn.io.content.ContentNotReadyException;
import org.ccnx.ccn.io.content.Header;
import org.ccnx.ccn.io.content.UpdateListener;
import org.ccnx.ccn.io.content.Header.HeaderObject;
import org.ccnx.ccn.profiles.metadata.MetadataProfile;
import org.ccnx.ccn.protocol.ContentName;
import org.ccnx.ccn.protocol.ContentObject;
import org.ccnx.ccn.protocol.PublisherPublicKeyDigest;
/**
* A CCN input stream that expects content names to be versioned, and streams to have a Header
* containing file-level metadata about each stream. See CCNVersionedInputStream for
* a description of versioning behavior, and CCNFileOutputStream for a description of
* header information. The header is read asynchronously, and may not be available at all until the complete
* stream has been written (in other words, the publisher typically writes the header last).
* Stream data can be read normally before the header has been read, and the consumer
* may opt to ignore the header completely, in which case this acts exactly like a
* CCNVersionedInputStream. In fact, a CCNVersionedInputStream can be used
* to read data read by CCNFileOutputStream (except for the header). Using a
* CCNFileInputStream to read something not written by a CCNFileOutputStream or one
* of its subclasses (in other words, something without a header) will still try to retrieve
* the (nonexistent) header in the background, but will not cause an error unless someone tries to access
* the header data itself.
*
* Headers are named according to definitions in the SegmentationProfile.
*
*/
public class CCNFileInputStream extends CCNVersionedInputStream implements UpdateListener {
/**
* The header information for that object, once
* we've read it.
*/
protected HeaderObject _header = null;
/**
* Temporary backwards-compatibility move...
*/
protected HeaderObject _oldHeader = null;
/**
* Set up an input stream to read segmented CCN content under a given versioned name.
* Content is assumed to be unencrypted, or keys will be retrieved automatically via another
* process.
* Will use the default handle given by CCNHandle#getHandle().
* Note that this constructor does not currently retrieve any
* data; data is not retrieved until read() is called. This will change in the future, and
* this constructor will retrieve the first block.
*
* @param baseName Name to read from. If it ends with a version, will retrieve that
* specific version. If not, will find the latest version available. If it ends with
* both a version and a segment number, will start to read from that segment of that version.
* @throws IOException Not currently thrown, will be thrown when constructors retrieve first block.
*/
public CCNFileInputStream(ContentName baseName) throws IOException {
super(baseName);
}
/**
* Set up an input stream to read segmented CCN content under a given versioned name.
* Content is assumed to be unencrypted, or keys will be retrieved automatically via another
* process.
* Will use the default handle given by CCNHandle#getHandle().
* Note that this constructor does not currently retrieve any
* data; data is not retrieved until read() is called. This will change in the future, and
* this constructor will retrieve the first block.
*
* @param baseName Name to read from. If it ends with a version, will retrieve that
* specific version. If not, will find the latest version available. If it ends with
* both a version and a segment number, will start to read from that segment of that version.
* @param handle The CCN handle to use for data retrieval. If null, the default handle
* given by CCNHandle#getHandle() will be used.
* @throws IOException Not currently thrown, will be thrown when constructors retrieve first block.
*/
public CCNFileInputStream(ContentName baseName, CCNHandle handle)
throws IOException {
super(baseName, handle);
}
/**
* Set up an input stream to read segmented CCN content under a given versioned name.
* Content is assumed to be unencrypted, or keys will be retrieved automatically via another
* process.
* Will use the default handle given by CCNHandle#getHandle().
* Note that this constructor does not currently retrieve any
* data; data is not retrieved until read() is called. This will change in the future, and
* this constructor will retrieve the first block.
*
* @param baseName Name to read from. If it ends with a version, will retrieve that
* specific version. If not, will find the latest version available. If it ends with
* both a version and a segment number, will start to read from that segment of that version.
* @param publisher The key we require to have signed this content. If null, will accept any publisher
* (subject to higher-level verification).
* @param handle The CCN handle to use for data retrieval. If null, the default handle
* given by CCNHandle#getHandle() will be used.
* @throws IOException Not currently thrown, will be thrown when constructors retrieve first block.
*/
public CCNFileInputStream(ContentName baseName, PublisherPublicKeyDigest publisher,
CCNHandle handle) throws IOException {
this(baseName, null, publisher, handle);
}
/**
* Set up an input stream to read segmented CCN content under a given versioned name.
* Content is assumed to be unencrypted, or keys will be retrieved automatically via another
* process.
* Will use the default handle given by CCNHandle#getHandle().
* Note that this constructor does not currently retrieve any
* data; data is not retrieved until read() is called. This will change in the future, and
* this constructor will retrieve the first block.
*
* @param baseName Name to read from. If it ends with a version, will retrieve that
* specific version. If not, will find the latest version available. If it ends with
* both a version and a segment number, will start to read from that segment of that version.
* @param startingSegmentNumber Alternative specification of starting segment number. If
* null, will be SegmentationProfile#baseSegment().
* @param handle The CCN handle to use for data retrieval. If null, the default handle
* given by CCNHandle#getHandle() will be used.
* @throws IOException Not currently thrown, will be thrown when constructors retrieve first block.
*/
public CCNFileInputStream(ContentName baseName, Long startingSegmentNumber, CCNHandle handle)
throws IOException {
this(baseName, startingSegmentNumber, null, handle);
}
/**
* Set up an input stream to read segmented CCN content under a given versioned name.
* Content is assumed to be unencrypted, or keys will be retrieved automatically via another
* process.
* Will use the default handle given by CCNHandle#getHandle().
* Note that this constructor does not currently retrieve any
* data; data is not retrieved until read() is called. This will change in the future, and
* this constructor will retrieve the first block.
*
* @param baseName Name to read from. If it ends with a version, will retrieve that
* specific version. If not, will find the latest version available. If it ends with
* both a version and a segment number, will start to read from that segment of that version.
* @param startingSegmentNumber Alternative specification of starting segment number. If
* null, will be SegmentationProfile#baseSegment().
* @param publisher The key we require to have signed this content. If null, will accept any publisher
* (subject to higher-level verification).
* @param handle The CCN handle to use for data retrieval. If null, the default handle
* given by CCNHandle#getHandle() will be used.
* @throws IOException Not currently thrown, will be thrown when constructors retrieve first block.
*/
public CCNFileInputStream(ContentName baseName, Long startingSegmentNumber,
PublisherPublicKeyDigest publisher, CCNHandle handle)
throws IOException {
super(baseName, startingSegmentNumber, publisher, handle);
}
/**
* Set up an input stream to read segmented CCN content under a given versioned name.
* Will use the default handle given by CCNHandle#getHandle().
* Note that this constructor does not currently retrieve any
* data; data is not retrieved until read() is called. This will change in the future, and
* this constructor will retrieve the first block.
*
* @param baseName Name to read from. If it ends with a version, will retrieve that
* specific version. If not, will find the latest version available. If it ends with
* both a version and a segment number, will start to read from that segment of that version.
* @param startingSegmentNumber Alternative specification of starting segment number. If
* null, will be SegmentationProfile#baseSegment().
* @param publisher The key we require to have signed this content. If null, will accept any publisher
* (subject to higher-level verification).
* @param keys The keys to use to decrypt this content. If null, assumes content unencrypted, or another
* process will be used to retrieve the keys.
* @param handle The CCN handle to use for data retrieval. If null, the default handle
* given by CCNHandle#getHandle() will be used.
* @throws IOException Not currently thrown, will be thrown when constructors retrieve first block.
*/
public CCNFileInputStream(ContentName baseName, Long startingSegmentNumber,
PublisherPublicKeyDigest publisher, ContentKeys keys, CCNHandle handle)
throws IOException {
super(baseName, startingSegmentNumber, publisher, keys, handle);
}
/**
* Set up an input stream to read segmented CCN content starting with a given
* ContentObject that has already been retrieved. Content is assumed
* to be unencrypted, or keys will be retrieved automatically via another
* process.
* @param startingSegment The first segment to read from. If this is not the
* first segment of the stream, reading will begin from this point.
* We assume that the signature on this segment was verified by our caller.
* @param flags any stream flags that must be set to handle even this first block (otherwise
* they can be set with setFlags prior to read). Can be null.
* @param handle The CCN handle to use for data retrieval. If null, the default handle
* given by CCNHandle#getHandle() will be used.
* @throws IOException
*/
public CCNFileInputStream(ContentObject startingSegment, EnumSet<FlagTypes> flags, CCNHandle handle)
throws IOException {
super(startingSegment, flags, handle);
}
/**
* Set up an input stream to read segmented CCN content starting with a given
* ContentObject that has already been retrieved.
* @param startingSegment The first segment to read from. If this is not the
* first segment of the stream, reading will begin from this point.
* We assume that the signature on this segment was verified by our caller.
* @param keys The keys to use to decrypt this content. Null if content unencrypted, or another
* process will be used to retrieve the keys.
* @param flags any stream flags that must be set to handle even this first block (otherwise
* they can be set with setFlags prior to read). Can be null.
* @param handle The CCN handle to use for data retrieval. If null, the default handle
* given by CCNHandle#getHandle() will be used.
* @throws IOException
*/
public CCNFileInputStream(ContentObject startingSegment,
ContentKeys keys, EnumSet<FlagTypes> flags, CCNHandle handle) throws IOException {
super(startingSegment, keys, flags, handle);
}
/**
* @return true if we have started the header retrieval process. To begin the process,
* we must first know what version of the content we are reading.
*/
protected boolean headerRequested() {
return (null != _header);
}
/**
*
* @return true if we have retrieved the header.
*/
public boolean hasHeader() {
return (headerRequested() && (_header.available() && !_header.isGone()) ||
((null != _oldHeader) && (_oldHeader.available() && !_oldHeader.isGone())));
}
/**
* Callers who wish to access the header should call this first; it will wait until the header
* has been successfully retrieved (if the retrieval has started).
* @throws ContentNotReadyException if we have not requested the header yet.
*/
public void waitForHeader(Long timeout) throws ContentNotReadyException {
if (!headerRequested())
throw new ContentNotReadyException("Not enough information available to request header!");
_header.waitForData((null != timeout) ? timeout : SystemConfiguration.getDefaultTimeout()); // should take timeout
// wait for old header implicitly; probably too long.
}
public void waitForHeader() throws ContentNotReadyException {
waitForHeader(null);
}
/**
* Accesses the header data if it has been requested.
* @return the Header for this stream.
* @throws ContentNotReadyException if we have not retrieved the header yet, or it hasn't been requested.
* @throws ContentGoneException if the header has been deleted.
* @throws ErrorStateException
*/
public Header header() throws ContentNotReadyException, ContentGoneException, ErrorStateException {
if (!headerRequested())
throw new ContentNotReadyException("Not enough information available to request header!");
if (_header.available() || (null == _oldHeader)) {
return _header.header();
}
return _oldHeader.header();
}
/**
* Request the header in the background.
* @param baseName name of the content, including the version, from which the header name will be derived.
* @param publisher expected publisher
* @throws IOException If the header cannot be retrieved.
* @throws ContentDecodingException If the header cannot be decoded.
*/
protected void requestHeader(ContentName baseName, PublisherPublicKeyDigest publisher)
throws ContentDecodingException, IOException {
if (headerRequested())
return; // done already
// Ask for the header, but update it in the background, as it may not be there yet.
_header = new HeaderObject(MetadataProfile.headerName(baseName), null, null, publisher, null, _handle);
if (Log.isLoggable(Log.FAC_IO, Level.INFO))
Log.info(Log.FAC_IO, "Retrieving header : " + _header.getBaseName() + " in background.");
_header.updateInBackground(false, this);
if (SystemConfiguration.OLD_HEADER_NAMES) {
_oldHeader = new HeaderObject(MetadataProfile.oldHeaderName(baseName), null, null, publisher, null, _handle);
if (Log.isLoggable(Log.FAC_IO, Level.INFO))
Log.info(Log.FAC_IO, "Retrieving header under old name: " + _oldHeader.getBaseName() + " in background.");
_oldHeader.updateInBackground(false, this);
}
}
/**
* Once we have retrieved the first segment of this stream using CCNVersionedInputStream#getFirstSegment(),
* initiate header retrieval.
*/
@Override
public ContentObject getFirstSegment() throws IOException {
// Give up efficiency where we try to detect auto-caught header, and just
// use superclass method to really get us a first content block, then
// go after the header. Later on we can worry about re-adding the optimization.
// This helps because the superclass method dereferences any links, so when we retrieve
// the header we use the resolved name of the content to do so, which is more likely
// to be correct.
ContentObject result = super.getFirstSegment();
if (null == result) {
throw new IOException("Cannot retrieve first block of " + _baseName + "!");
}
// Have to wait to request the header till we know what version we're looking for.
// Don't want to request the header if this stream is LINK or GONE.
if (!headerRequested() && (!result.isGone()) && (!result.isLink())) {
requestHeader(_baseName, result.signedInfo().getPublisherKeyID());
}
return result;
}
@Override
public long skip(long n) throws IOException {
if (Log.isLoggable(Log.FAC_IO, Level.FINE))
Log.fine(Log.FAC_IO, "in skip({0})", n);
if (n < 0) {
return 0;
}
if (!hasHeader()){
return super.skip(n);
}
int[] toGetBlockAndOffset = null;
long toGetPosition = 0;
long currentBlock = -1;
int currentBlockOffset = 0;
long currentPosition = 0;
if (_currentSegment == null) {
//we do not have a block already
//skip position is n
currentPosition = 0;
toGetPosition = n;
} else {
//we already have a block... need to handle some tricky cases
currentBlock = segmentNumber();
currentBlockOffset = (int)super.tell();
currentPosition = _header.segmentLocationToPosition(currentBlock, currentBlockOffset);
toGetPosition = currentPosition + n;
}
//make sure we don't skip past end of the object
if (toGetPosition >= _header.length()) {
toGetPosition = _header.length();
_atEOF = true;
}
toGetBlockAndOffset = _header.positionToSegmentLocation(toGetPosition);
//make sure the position makes sense
//is this a valid block?
if (toGetBlockAndOffset[0] >= _header.segmentCount()){
//this is not a valid block number, subtract 1
if (toGetBlockAndOffset[0] > 0) {
toGetBlockAndOffset[0]--;
}
//now we have the last block if the position was too long
}
//is the offset > 0?
if (toGetBlockAndOffset[1] < 0) {
toGetBlockAndOffset[1] = 0;
}
//now we should get the block and check the offset
// TODO: once first block is always set in a constructor this conditional can be removed
if (_currentSegment == null)
setFirstSegment(getSegment(toGetBlockAndOffset[0]));
else
setCurrentSegment(getSegment(toGetBlockAndOffset[0]));
if (_currentSegment == null) {
//we had an error getting the block
throw new IOException("Error getting block "+toGetBlockAndOffset[0]+" in CCNInputStream.skip("+n+")");
} else {
//we have a valid block!
//first make sure the offset is valid
if (toGetBlockAndOffset[1] <= _currentSegment.contentLength()) {
//this is good, our offset is somewhere in this block
} else {
//our offset is past the end of our block, reset to the end.
toGetBlockAndOffset[1] = _currentSegment.contentLength();
}
_segmentReadStream.skip(toGetBlockAndOffset[1]);
return _header.segmentLocationToPosition(toGetBlockAndOffset[0], toGetBlockAndOffset[1]) - currentPosition;
}
}
@Override
protected int segmentCount() throws IOException {
if (hasHeader()) {
return _header.segmentCount();
}
return super.segmentCount();
}
@Override
public void seek(long position) throws IOException {
if (Log.isLoggable(Log.FAC_IO, Level.FINE))
Log.fine(Log.FAC_IO, "Seeking stream to {0}: have header? {1}", position, hasHeader());
if (hasHeader()) {
int [] blockAndOffset = _header.positionToSegmentLocation(position);
if (Log.isLoggable(Log.FAC_IO, Level.FINE)) {
Log.fine(Log.FAC_IO, "seek: position: {0} block: {1} offset: {2} currentSegment: {3}",
position, blockAndOffset[0], blockAndOffset[1], currentSegmentNumber());
}
if (currentSegmentNumber() == blockAndOffset[0]) {
//already have the correct block
if (super.tell() == blockAndOffset[1]){
//already have the correct offset
} else {
// Reset and skip.
if (_segmentReadStream.markSupported()) {
_segmentReadStream.reset();
} else {
setCurrentSegment(_currentSegment);
}
_segmentReadStream.skip(blockAndOffset[1]);
}
return;
}
// TODO: once first block is always set in a constructor this conditional can be removed
if (_currentSegment == null)
setFirstSegment(getSegment(blockAndOffset[0]));
else
setCurrentSegment(getSegment(blockAndOffset[0]));
super.skip(blockAndOffset[1]);
long check = _header.segmentLocationToPosition(blockAndOffset[0], blockAndOffset[1]);
if (Log.isLoggable(Log.FAC_IO, Level.FINE))
Log.fine(Log.FAC_IO, "current position: block "+blockAndOffset[0]+" _blockOffset "+super.tell()+" ("+check+")");
if (_currentSegment != null) {
_atEOF=false;
}
// Might be at end of stream, so different value than came in...
//long check = _header.blockLocationToPosition(blockAndOffset[0], blockAndOffset[1]);
//Log.info(Log.FAC_IO, "return val check: "+check);
//return _header.blockLocationToPosition(blockAndOffset[0], blockAndOffset[1]);
//skip(check);
//Log.info(Log.FAC_IO, " _blockOffset "+_blockOffset);
} else {
super.seek(position);
}
}
@Override
public long tell() throws IOException {
if (hasHeader()) {
return _header.segmentLocationToPosition(segmentNumber(), (int)super.tell());
} else {
return super.tell();
}
}
@Override
public long length() throws IOException {
if (hasHeader()) {
return _header.length();
}
return super.length();
}
public void newVersionAvailable(CCNNetworkObject<?> newVersion, boolean wasSave) {
if (!headerRequested()) {
if (Log.isLoggable(Log.FAC_IO, Level.WARNING)) {
Log.warning(Log.FAC_IO, "CCNFileInputStream: got a notification of a new header version {0} when none requested!",
newVersion.getVersionedName());
}
return;
}
// One of our headers has come back. Cancel the other one.
if (MetadataProfile.isHeader(newVersion.getBaseName())) {
// cancel the old one
if (null != _oldHeader) {
if (Log.isLoggable(Log.FAC_IO, Level.FINE)) {
Log.fine(Log.FAC_IO, "CCNFileInputStream: retrieved new header {0}, canceling request for old one.",
newVersion.getVersionedName());
}
_oldHeader.cancelInterest();
}
} else if (null != _header) {
if (Log.isLoggable(Log.FAC_IO, Level.FINE)) {
Log.fine(Log.FAC_IO, "CCNFileInputStream: retrieved old header {0}, canceling request for new one.",
newVersion.getVersionedName());
}
_header.cancelInterest();
}
}
@Override
public void close() throws IOException{
Log.info(Log.FAC_IO, "CCNFileInputStream: close {0}. closing header objects and will make call to shut down pipelining", _baseName);
//need to close the header object to cancel any outstanding interests
if (_header != null) {
_header.close();
}
if (_oldHeader!=null) {
_oldHeader.close();
}
//then make sure any pipelining state is also cleaned up...
super.close();
}
}