/*
* Part of the CCNx Java Library.
*
* Copyright (C) 2011 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.versioning;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.ccnx.ccn.CCNContentHandler;
import org.ccnx.ccn.CCNHandle;
import org.ccnx.ccn.impl.CCNStats;
import org.ccnx.ccn.impl.CCNStats.CCNCategorizedStatistics;
import org.ccnx.ccn.impl.CCNStats.CCNStatistics;
import org.ccnx.ccn.protocol.ContentName;
import org.ccnx.ccn.protocol.ContentObject;
import org.ccnx.ccn.protocol.Interest;
import org.ccnx.ccn.protocol.MalformedContentNameStringException;
/**
* Given a base name, retrieve all versions. We have maintained a similar method
* naming to CCNHandle (expressInterest, cancelInterest, close), except we take
* a ContentName input instead of an Interest input. A future extension might be to
* take an Interest to make it more drop-in replacement for existing CCNHandle methods.
*
* IMPORTANT: The CCNInterestListener that gets the data should not block for long
* and should not hijack the incoming thread.
*
* NOTE: The retry is not implemented. Interests are re-expressed every 4 seconds
* as per normal, and they continue to be expressed until canceled.
*
* This object is meant to be private for one application, which provides the
* CCNHandle to use. It may be shared between multiple threads. The way the
* retry expression works is intended for one app to use that has an understanding
* of its needs.
*
* The class will:
* - return all available versions of a base name, without duplicates
* - allow the user to supply a list of versions to exclude (e.g. they
* have already been seen by the application)
* - allow the user to supply a hard-cutoff starting time
* - allow the user to supply the interest re-expression rate,
* which may be very slow and use our own timer not the one built in
* to ccnx.
*
* Because the list of excluded version can be very long, this
* class manages expressing multiple interests.
*
* All the work is done down in the inner class BasenameState, which is the state
* stored per basename and tracks the interests issued for that basename. It
* is really just a holder for VersioningInterestManager plus state about the
* set of listeners.
*/
public class VersioningInterest implements CCNCategorizedStatistics {
// ==============================================================================
// Public API
/**
* @param handle
* @param listener
*/
public VersioningInterest(CCNHandle handle) {
_handle = handle;
}
/**
* Express an interest for #name. We will assume that #name does not
* include a version, and we construct an interest that will only match
* 3 additional components to #name (version/segment/digest).
*
* When the default CCN timeout is exceeded, we stop responding.
*
* If there is already an interest for the same (name, listener), no action is taken.
*
* The return value from #listener is ignored, the listener does not need to re-express
* an interest. Interests are re-expressed automatically until canceled.
*
* @param name
* @param listener
* @throws IOException
*/
public void expressInterest(ContentName name, CCNContentHandler handler) throws IOException {
expressInterest(name, handler, null, null);
}
/**
* As above, and provide a set of versions to exclude
* The return value from #listener is ignored, the listener does not need to re-express
* an interest. Interests are re-expressed automatically until canceled.
*
* @param name
* @param handler
* @param retrySeconds
* @param exclusions may be null
* @throws IOException
*/
public void expressInterest(ContentName name, CCNContentHandler handler, Set<VersionNumber> exclusions) throws IOException {
expressInterest(name, handler, exclusions, null);
}
/**
* As above, and provide a set of versions to exclude and a hard floor startingVersion, any version
* before that will be ignored.
*
* The return value from #listener is ignored, the listener does not need to re-express
* an interest. Interests are re-expressed automatically until canceled.
*
* @param name
* @param handler
* @param retrySeconds
* @param exclusions may be null
* @param startingVersion the minimum version to include (may be null)
* @throws IOException
*/
public void expressInterest(ContentName name, CCNContentHandler handler, Set<VersionNumber> exclusions, VersionNumber startingVeersion) throws IOException {
addInterest(name, handler, exclusions, startingVeersion);
}
/**
* Kill off all interests.
*/
public void close() {
removeAll();
}
/**
* Cancel a specific interest
* @param name
* @param handler
*/
public void cancelInterest(ContentName name, CCNContentHandler handler) {
removeInterest(name, handler);
}
/**
* in case we're GC'd without a close(). Don't rely on this.
*/
protected void finalize() throws Throwable {
try {
removeAll();
} finally {
super.finalize();
}
}
/**
* return the statistics for the interests corresponding to name
* @param name A ContentName or a URI-encoded string
* @return May be null if no interest expressed for name
*/
public CCNStats getStatsByName(Object name) throws ClassCastException {
ContentName cn = null;
if( name instanceof ContentName )
cn = (ContentName) name;
else if( name instanceof String )
try {
cn = ContentName.fromURI((String) name);
} catch (MalformedContentNameStringException e) {
}
if( null == cn )
throw new ClassCastException("Name must be a ContentName or a URI string");
synchronized(_map) {
BasenameState data = _map.get(cn);
if( null != data )
return data.getStats();
return null;
}
}
public Object[] getCategoryNames() {
synchronized(_map) {
return _map.keySet().toArray();
}
}
// ==============================================================================
// Internal implementation
private final CCNHandle _handle;
private final Map<ContentName, BasenameState> _map = new HashMap<ContentName, BasenameState>();
private void addInterest(ContentName name, CCNContentHandler handler, Set<VersionNumber> exclusions, VersionNumber startingVersion) throws IOException {
BasenameState data;
synchronized(_map) {
data = _map.get(name);
if( null == data ) {
data = new BasenameState(_handle, name, exclusions, startingVersion);
_map.put(name, data);
data.addListener(handler);
data.start();
} else {
data.addListener(handler);
}
}
}
/**
* Remove a listener. If it is the last listener, remove from map and
* kill all interests.
* @param name
* @param listener
*/
private void removeInterest(ContentName name, CCNContentHandler handler) {
BasenameState data;
synchronized(_map) {
data = _map.get(name);
if( null != data ) {
data.removeListener(handler);
if( data.size() == 0 ) {
data.stop();
_map.remove(name);
}
}
}
}
private void removeAll() {
synchronized(_map) {
Iterator<BasenameState> iter = _map.values().iterator();
while( iter.hasNext() ) {
BasenameState bns = iter.next();
bns.stop();
iter.remove();
}
}
}
// ======================================================================
// This is the state stored per base name
private static class BasenameState implements CCNContentHandler, CCNStatistics {
public BasenameState(CCNHandle handle, ContentName basename, Set<VersionNumber> exclusions, VersionNumber startingVersion) {
_vim = new VersioningInterestManager(handle, basename, exclusions, startingVersion, this);
}
/**
* @param listener
* @param retrySeconds IGNORED, not implemented
* @return true if added, false if existed or only retrySeconds updated
*/
public boolean addListener(CCNContentHandler handler) {
if( handler == null) return false;
synchronized(_handlers) {
return _handlers.add(handler);
}
}
/**
* @return true if removed, false if not found
*/
public boolean removeListener(CCNContentHandler handler) {
if( handler == null) return false;
synchronized(_handlers) {
return _handlers.remove(handler);
}
}
public int size() {
synchronized(_handlers) {
return _handlers.size();
}
}
/**
* start issuing interests. No data is passed to
* any listener in the stopped state
* @throws IOException
*/
public void start() throws IOException {
_running = true;
_vim.start();
}
/**
* Cancel all interests for the name
*/
public void stop() {
_running = false;
_vim.stop();
}
/**
* Pass any received data up to the user.
* @param data
* @param interest
* @return null
*/
public Interest handleContent(ContentObject data, Interest interest) {
// when we're stopped, we do not pass any data
if( ! _running )
return null;
synchronized(_handlers) {
for(CCNContentHandler handler : _handlers) {
try {
handler.handleContent(data, interest);
} catch(Exception e){
e.printStackTrace();
}
}
}
return null;
}
public CCNStats getStats() {
return _vim.getStats();
}
// =======
private final Set<CCNContentHandler> _handlers = new HashSet<CCNContentHandler>();
private final VersioningInterestManager _vim;
private boolean _running = false;
}
}