/* * A CCNx chat library. * * Copyright (C) 2008, 2009, 2010, 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.apps.ccnchat; import java.io.IOException; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.logging.Level; import org.ccnx.ccn.CCNHandle; import org.ccnx.ccn.config.ConfigurationException; import org.ccnx.ccn.config.UserConfiguration; import org.ccnx.ccn.impl.CCNFlowControl.SaveType; import org.ccnx.ccn.impl.support.Log; import org.ccnx.ccn.io.content.CCNStringObject; import org.ccnx.ccn.io.content.ContentEncodingException; import org.ccnx.ccn.profiles.security.KeyProfile; import org.ccnx.ccn.protocol.ContentName; import org.ccnx.ccn.protocol.KeyLocator; import org.ccnx.ccn.protocol.MalformedContentNameStringException; import org.ccnx.ccn.protocol.PublisherPublicKeyDigest; /** * Based on a client/server chat example in Robert Sedgewick's Algorithms * in Java. * * This is the base class that does all the CCN networking. It is * instantiated by the UI class and then called in a blocking listen() call. * * The UI uses sendMessage() to send a message to the network and implements * the CCNChatCallback interface to receive a message from the network. * * The UI should call shutdown() on close. */ public final class CCNChatNet { /** * The callback from the network to the UI to display * a message from the network. */ public interface CCNChatCallback { /** * Implemented by concrete UI class * Receive a message from the network * @param message */ public void recvMessage(String message); } // ================================================================== // The public API to the UI class /** * Construct a CCNChat * @param callback The callback to the UI to receive a message * @param namespace The namespace of the Chat channel * @throws MalformedContentNameStringException */ public CCNChatNet(CCNChatCallback callback, String namespace) throws MalformedContentNameStringException { _callback = callback; _namespace = ContentName.fromURI(namespace); _namespaceStr = namespace; _friendlyNameToDigestHash = new HashMap<PublisherPublicKeyDigest, String>(); } /** * Send a message out to the network. * @param message The text string to send * @throws IOException * @throws ContentEncodingException */ public synchronized void sendMessage(String message) throws ContentEncodingException, IOException { _writeString.save(message); } /** * Turn off everything. * @throws IOException */ public void shutdown() throws IOException { _finished = true; if (null != _readString) { _readString.cancelInterest(); showMessage(SYSTEM, now(), "Shutting down " + _namespace + "..."); } } /** * Some UIs (like the text one) want to turn off all logging */ public void setLogging(Level level) { Log.setLevel(Log.FAC_ALL, level); } /** * This actual CCN loop to send/receive messages. Called by * the UI class. This method blocks! If the UI is not multi-threaded, * you should start a thread to hold listen(). * * When shutdown() is called, listen() will exit. * * @throws ConfigurationException * @throws IOException * @throws MalformedContentNameStringException */ public void listen() throws ConfigurationException, IOException, MalformedContentNameStringException { //Also publish your keys under the chat "channel name" namespace if (_namespace.toString().startsWith("ccnx:/")) { UserConfiguration.setDefaultNamespacePrefix(_namespace.toString().substring(5)); } else { UserConfiguration.setDefaultNamespacePrefix(_namespace.toString()); } CCNHandle tempReadHandle = CCNHandle.getHandle(); // Writing must be on a different handle, to enable us to read back text we have // written when nobody else is reading. CCNHandle tempWriteHandle = CCNHandle.open(); _readString = new CCNStringObject(_namespace, (String)null, SaveType.RAW, tempReadHandle); _readString.updateInBackground(true); String introduction = UserConfiguration.userName() + " has entered " + _namespace; _writeString = new CCNStringObject(_namespace, introduction, SaveType.RAW, tempWriteHandle); _writeString.save(); // Publish the user's friendly name under a new ContentName String friendlyNameNamespaceStr = _namespaceStr + "/members/"; _friendlyNameNamespace = KeyProfile.keyName(ContentName.fromURI(friendlyNameNamespaceStr), _writeString.getContentPublisher()); Log.info("**** Friendly Namespace is " + _friendlyNameNamespace); //read the string here..... _readNameString = new CCNStringObject(_friendlyNameNamespace, (String)null, SaveType.RAW, tempReadHandle); _readNameString.updateInBackground(true); String publishedNameStr = UserConfiguration.userName(); Log.info("*****I am adding my own friendly name as " + publishedNameStr); _writeNameString = new CCNStringObject(_friendlyNameNamespace, publishedNameStr, SaveType.RAW, tempWriteHandle); _writeNameString.save(); try { addNameToHash(_writeNameString.getContentPublisher(), _writeNameString.string()); } catch (IOException e) { System.err.println("Unable to read from " + _writeNameString + "for writing to hashMap"); e.printStackTrace(); } // Need to do synchronization for updates that come in while we're processing last one. while (!_finished) { try { synchronized(_readString) { _readString.wait(CYCLE_TIME); } } catch (InterruptedException e) { } if (_readString.isSaved()) { Timestamp thisUpdate = _readString.getVersion(); if ((null == _lastUpdate) || thisUpdate.after(_lastUpdate)) { Log.info("Got an update: " + _readString.getVersion()); _lastUpdate = thisUpdate; //lookup friendly name to display for this user..... String userFriendlyName = getFriendlyName(_readString.getContentPublisher()); if (userFriendlyName.equals("")) { // Its not in the hashMap.. So, try and read the user's friendly name from the ContentName and then add it to the hashMap.... String userNameStr = _namespaceStr + "/members/"; _friendlyNameNamespace = KeyProfile.keyName(ContentName.fromURI(userNameStr), _readString.getContentPublisher()); try { _readNameString = new CCNStringObject(_friendlyNameNamespace, (String)null, SaveType.RAW, tempReadHandle); } catch (Exception e) { } _readNameString.update(WAIT_TIME_FOR_FRIENDLY_NAME); // for now, I am just waiting for 2.5 secs.. Otherwise, I might have to update in background and have a callback if (_readNameString.available()) { if (! _readString.getContentPublisher().equals(_readNameString.getContentPublisher())) { showMessage(_readString.getContentPublisher(), _readString.getPublisherKeyLocator(), thisUpdate, _readString.string()); } else { addNameToHash(_readNameString.getContentPublisher(), _readNameString.string()); showMessage(_readNameString.string(), thisUpdate, _readString.string()); } } else { showMessage(_readString.getContentPublisher(), _readString.getPublisherKeyLocator(), thisUpdate, _readString.string()); } } else { showMessage(userFriendlyName, thisUpdate, _readString.string()); } } } } } // ================================================================== // Internal methods private final CCNChatCallback _callback; private final ContentName _namespace; private final String _namespaceStr; private Timestamp _lastUpdate; private boolean _finished = false; private static final long CYCLE_TIME = 1000; private static final String SYSTEM = "System"; private static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm"); private static final int WAIT_TIME_FOR_FRIENDLY_NAME = 2500; // Separate read and write libraries so we will read our own updates, // and don't have to treat our inputs differently than others. private CCNStringObject _readString; private CCNStringObject _writeString; // We use these for storing the friendly names of users. private CCNStringObject _readNameString; private CCNStringObject _writeNameString; // this is where we store the friendly name of the user private HashMap<PublisherPublicKeyDigest, String> _friendlyNameToDigestHash; private ContentName _friendlyNameNamespace; private String getFriendlyName(PublisherPublicKeyDigest digest) { if (_friendlyNameToDigestHash.containsKey(digest)) { return _friendlyNameToDigestHash.get(digest); } else { Log.info("We DON'T have an entry in our hash for this " + digest); return ""; } } private void addNameToHash(PublisherPublicKeyDigest digest, String friendlyName) { _friendlyNameToDigestHash.put(digest,friendlyName); } /** * Add a message to the output. * @param message */ private void showMessage(String sender, Timestamp time, String message) { _callback.recvMessage("[" + sender + " " + DATE_FORMAT.format(time) + "]: " + message + "\n"); } private void showMessage(PublisherPublicKeyDigest publisher, KeyLocator keyLocator, Timestamp time, String message) { // Start with key fingerprints. Move up to user names. showMessage(publisher.shortFingerprint().substring(0, 8), time, message); } private static Timestamp now() { return new Timestamp(System.currentTimeMillis()); } }