/* * The Sun Project JXTA(TM) Software License * * Copyright (c) 2001-2007 Sun Microsystems, Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The end-user documentation included with the redistribution, if any, must * include the following acknowledgment: "This product includes software * developed by Sun Microsystems, Inc. for JXTA(TM) technology." * Alternately, this acknowledgment may appear in the software itself, if * and wherever such third-party acknowledgments normally appear. * * 4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA" must * not be used to endorse or promote products derived from this software * without prior written permission. For written permission, please contact * Project JXTA at http://www.jxta.org. * * 5. Products derived from this software may not be called "JXTA", nor may * "JXTA" appear in their name, without prior written permission of Sun. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SUN * MICROSYSTEMS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * JXTA is a registered trademark of Sun Microsystems, Inc. in the United * States and other countries. * * Please see the license information page at : * <http://www.jxta.org/project/www/license.html> for instructions on use of * the license in source files. * * ==================================================================== * This software consists of voluntary contributions made by many individuals * on behalf of Project JXTA. For more information on Project JXTA, please see * http://www.jxta.org. * * This license is based on the BSD license adopted by the Apache Foundation. */ package net.jxta.impl.content.srdisocket; import net.jxta.content.*; import net.jxta.document.Advertisement; import net.jxta.document.AdvertisementFactory; import net.jxta.document.Document; import net.jxta.document.MimeMediaType; import net.jxta.id.ID; import net.jxta.id.IDFactory; import net.jxta.impl.content.ModuleWrapperFactory; import net.jxta.logging.Logging; import net.jxta.peergroup.PeerGroup; import net.jxta.pipe.PipeID; import net.jxta.pipe.PipeService; import net.jxta.platform.Module; import net.jxta.platform.ModuleSpecID; import net.jxta.protocol.ContentShareAdvertisement; import net.jxta.protocol.ModuleImplAdvertisement; import net.jxta.protocol.PipeAdvertisement; import net.jxta.socket.JxtaServerSocket; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.Thread.UncaughtExceptionHandler; import java.net.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.logging.Level; import java.util.logging.Logger; /** * Reference implementation of the ContentProvider interface. This * implementation works * as follows: * <ul> * <li> * Being the default/fallback implementation, this provider makes no * assumptions as to the data contained within the Content instance. * It may be static or dynamic content and may or may not be * different across multiple peers who are sharing the same Content ID. * Each client will be served using a single call to a * <code>Content</code>'s document stream. * </li> * <li> * Content instances are shared by calling the shareContent(Content) * method and then making another peer aware of the returned * ContentAdvertisement. How the remote peer is made aware of the * ContentAdvertisement is of no concern. * </li> * <li> * The ContentAdvertisement passed to <code>retrieveContent</code> * is assumed to contain only the information required to rebuild * a Content instance in the presence of the data itself. * </li> * <li> * No validation is performed on the transferred data. All validation * must be done out-of-band and post-transfer. * </li> * </ul> */ public class SRDISocketContentProvider implements ContentProviderSPI { /** * Logger instance. */ private static final Logger LOG = Logger.getLogger(SRDISocketContentProvider.class.getName()); /** * The number of milliseconds the accept loop will sleep when an * IOException prevents the creation of the server socket. */ private static final int ACCEPT_RETRY_DELAY = Integer.getInteger(SRDISocketContentProvider.class.getName() + ".acceptRetryDelay", 5 * 1000); /** * Module spec ID for this provider. */ private static final String MODULE_SPEC_ID = "urn:jxta:uuid-AC3AA08FC4A14C15A78A88B4D4F87554" + "A7A79198AC274BF38DDBA376EB9A788406"; /** * Parsed and ready-to-use version of MODULE_SPEC_ID. */ private static final ModuleSpecID specID; // Initialized at construction private final Map<ID, SRDIContentShare> shares = new HashMap<ID, SRDIContentShare>(); private CopyOnWriteArrayList<ContentProviderListener> listeners = new CopyOnWriteArrayList<ContentProviderListener>(); // Initialized by init private PeerGroup peerGroup; private ScheduledExecutorService executor; private PipeAdvertisement pipeAdv; // Initialized and managed by start/stop private boolean running = false; ////////////////////////////////////////////////////////////////////////// // Inner classes: /** * Executor thread factory to configure reasonable thread names and * settings, etc.. */ private class ThreadFactoryImpl implements ThreadFactory, UncaughtExceptionHandler { private ThreadGroup threadGroup; public ThreadFactoryImpl(PeerGroup group) { StringBuilder name = new StringBuilder(); name.append(group.getPeerGroupName()); name.append(" - "); name.append(SRDISocketContentProvider.class.getName()); name.append(" pool"); threadGroup = new ThreadGroup(name.toString()); threadGroup.setDaemon(true); } public Thread newThread(Runnable runnable) { Thread thread = new Thread(threadGroup, runnable); thread.setUncaughtExceptionHandler(this); return thread; } public void uncaughtException(Thread thread, Throwable throwable) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, "Uncaught throwable in pool thread: " + thread, throwable); } } } /** * Proxy for clientExecution(). */ private class Client implements Runnable { private Socket socket; public Client(Socket clientSocket) { socket = clientSocket; } public void run() { clientExecution(socket); } } ////////////////////////////////////////////////////////////////////////// // Constructors and initializers: /** * Static initializer. */ static { try { URI specURI = new URI(MODULE_SPEC_ID); specID = (ModuleSpecID) IDFactory.fromURI(specURI); } catch (URISyntaxException urisx) { throw (new RuntimeException( "Illegal ModuleSpecURI in code: " + MODULE_SPEC_ID, urisx)); } } ////////////////////////////////////////////////////////////////////////// // ContentProviderSPI interface methods: /** * {@inheritDoc} */ public void init(PeerGroup group, ID assignedID, Advertisement implAdv) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("initProvider(): group=" + group); } peerGroup = group; executor = Executors.newScheduledThreadPool( 5, new ThreadFactoryImpl(group)); pipeAdv = (PipeAdvertisement) AdvertisementFactory.newAdvertisement( PipeAdvertisement.getAdvertisementType()); pipeAdv.setType(PipeService.UnicastType); PipeID pipeID = IDFactory.newPipeID(peerGroup.getPeerGroupID()); pipeAdv.setPipeID(pipeID); } /** * {@inheritDoc} */ public synchronized int startApp(String[] args) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("startApp()"); } if (running) { return Module.START_OK; } if (peerGroup.getPipeService() == null) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.warning("Stalled until there is a PipeService"); } return Module.START_AGAIN_STALLED; } running = true; // Start the accept loop executor.execute(new Runnable() { public void run() { acceptExecution(); } }); return Module.START_OK; } /** * {@inheritDoc} */ public synchronized void stopApp() { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("stopApp()"); } if (!running) { return; } /* * XXX 20070911 mcumings: We really need to be able to abort all * ContentTransfer instances that we've created that are still * in-flight. Right now the ContentTransfers will silently * fail if the ScheduledExecutorService is shutdown while the * transfer is in-flight. I don't like the idea of maintaining * references to every ContentTransfer instance, but I also don't * like the idea of each instance using it's own dedicated thread. * Suggestions? */ running = false; notifyAll(); } /** * {@inheritDoc} */ public ContentProviderSPI getInterface() { return (ContentProviderSPI) ModuleWrapperFactory.newWrapper( new Class[]{ContentProviderSPI.class}, this); } /** * {@inheritDoc} */ public Advertisement getImplAdvertisement() { ModuleImplAdvertisement adv = (ModuleImplAdvertisement) AdvertisementFactory.newAdvertisement( ModuleImplAdvertisement.getAdvertisementType()); adv.setModuleSpecID(specID); adv.setCode(getClass().getName()); adv.setProvider("https://jxta.dev.java.net/"); adv.setDescription("ContentProvider implementation using JxtaSockets"); return adv; } ////////////////////////////////////////////////////////////////////////// // ContentProvider interface methods: /** * {@inheritDoc} */ public void addContentProviderListener(ContentProviderListener listener) { listeners.add(listener); } /** * {@inheritDoc} */ public void removeContentProviderListener(ContentProviderListener listener) { listeners.remove(listener); } /** * {@inheritDoc} */ public ContentTransfer retrieveContent(ContentID contentID) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("retrieveContent(" + contentID + ")"); } synchronized (this) { if (!running) { return null; } } synchronized (shares) { ContentShare share = getShare(contentID); if (share != null) { return new NullContentTransfer(this, share.getContent()); } } return new SRDISocketContentTransfer( this, executor, peerGroup, contentID); } /** * {@inheritDoc} */ public ContentTransfer retrieveContent(ContentShareAdvertisement adv) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("retrieveContent(ContentShareAdvertisement)"); } synchronized (this) { if (!running) { return null; } } synchronized (shares) { ContentShare share = getShare(adv.getContentID()); if (share != null) { return new NullContentTransfer(this, share.getContent()); } } return new SRDISocketContentTransfer(this, executor, peerGroup, adv); } /** * {@inheritDoc} */ public List<ContentShare> shareContent(Content content) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("shareContent(): Content=" + content); } PipeAdvertisement pAdv; synchronized (this) { if (pipeAdv == null) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Cannot create share before initialization"); } return null; } pAdv = pipeAdv; } List<ContentShare> result = new ArrayList<ContentShare>(1); ID id = content.getContentID(); SRDIContentShare share; synchronized (shares) { share = getShare(id); if (share == null) { share = new SRDIContentShare(this, content, pAdv); shares.put(id, share); result.add(share); } } if (result.size() == 0) { /* * This content was already shared. We'll skip notifying our * listeners but will return it in the results. */ result.add(share); } else { fireContentShared(result); } return result; } /** * {@inheritDoc} */ public boolean unshareContent(ContentID contentID) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("unhareContent(): ContentID=" + contentID); } ContentShare oldShare; synchronized (shares) { oldShare = shares.remove(contentID); } if (oldShare == null) { return false; } else { fireContentUnshared(contentID); return true; } } /** * {@inheritDoc} */ public void findContentShares(int maxNum, ContentProviderListener listener) { List<ContentShare> shareList; synchronized (shares) { shareList = new ArrayList<ContentShare>(Math.min(maxNum, shares.size())); for (ContentShare share : shares.values()) { if (shareList.size() >= maxNum) { break; } shareList.add(share); } } listener.contentSharesFound( new ContentProviderEvent.Builder(this, shareList) .lastRecord(true) .build()); } ////////////////////////////////////////////////////////////////////////// // Package methods: /** * Returns the peer peerGroup the service is running in. * * @return PeerGroup instance this service is running in */ protected PeerGroup getPeerGroup() { return peerGroup; } ////////////////////////////////////////////////////////////////////////// // Private methods: /** * Server execution mainline. */ private void acceptExecution() { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Acceptor thread starting"); } JxtaServerSocket serverSocket = null; try { while (true) { synchronized (this) { if (!running) { break; } } try { if (serverSocket == null) { LOG.fine("Creating new server socket"); serverSocket = new JxtaServerSocket(peerGroup, pipeAdv); } if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) { LOG.finer("Waiting to accept client..."); } Socket socket = serverSocket.accept(); if (socket != null) { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Incoming socket connection"); } executor.execute(new Client(socket)); } } catch (SocketTimeoutException socktox) { if (Logging.SHOW_FINEST && LOG.isLoggable(Level.FINEST)) { LOG.finest("Socket timed out"); } } catch (IOException iox) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, "Caught exception in acceptor loop", iox); } // Close and deref the current socket try { serverSocket.close(); } catch (IOException iox2) { LOG.log(Level.WARNING, "Could not close socket", iox); } finally { serverSocket = null; } // Wait a while before the next attempt try { Thread.sleep(ACCEPT_RETRY_DELAY); } catch (InterruptedException intx) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, "Interrupted", intx); } } } catch (RuntimeException rtx) { LOG.log(Level.WARNING, "Caught runtime exception", rtx); throw(rtx); } } } finally { LOG.info("Exiting"); if (serverSocket != null) { try { serverSocket.close(); } catch (IOException iox) { LOG.log(Level.WARNING, "Could not close socket", iox); } } } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Accceptor thread exiting"); } } /** * Per-client server execution mainline. */ private void clientExecution(Socket socket) { SocketAddress remote = socket.getRemoteSocketAddress(); SRDIContentShare share = null; try { if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Client executing against socket: " + socket); } InputStream inStream = socket.getInputStream(); ContentRequest request = ContentRequest.readFromStream(inStream); ContentResponse response = new ContentResponse(request); share = getShare(request.getContentID()); response.setSuccess(share != null); if (share != null) { share.fireShareSessionOpened(remote); } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Client response being sent:\n" + response.getDocument(MimeMediaType.XMLUTF8)); } OutputStream outStream = socket.getOutputStream(); response.writeToStream(outStream); if (response.getSuccess()) { // Send the content data if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Client transfer starting"); } // Notify listeners of access by remote peer share.fireShareSessionAccessed(remote); Content content = share.getContent(); Document contentDocument = content.getDocument(); contentDocument.sendToStream(outStream); outStream.flush(); } if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) { LOG.fine("Client transaction completed"); } } catch (IOException iox) { if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "Caught exception in client thread", iox); } } catch (RuntimeException rtx) { if (Logging.SHOW_SEVERE && LOG.isLoggable(Level.SEVERE)) { LOG.log(Level.SEVERE, "Caught runtime exception", rtx); } throw (rtx); } finally { if (share != null) { share.fireShareSessionClosed(remote); } try { socket.close(); } catch (IOException ignore) { if (Logging.SHOW_FINEST && LOG.isLoggable(Level.FINEST)) { LOG.log(Level.FINEST, "Ignoring exception", ignore); } } } } /** * Returns the content share entry for the specified ContentID. * * @return content share */ private SRDIContentShare getShare(ID id) { synchronized (shares) { return shares.get(id); } } /** * Notify our listeners that the provided shares are being exposed. * * @param shares list of fresh shares */ private void fireContentShared(List<ContentShare> shares) { ContentProviderEvent event = null; for (ContentProviderListener listener : listeners) { if (event == null) { event = new ContentProviderEvent.Builder(this, shares) .build(); } listener.contentShared(event); } } /** * Notify our listeners that the provided shares are that are no * longer being exposed. * * @param contentID ContentID of the content which is no longer * being shared */ private void fireContentUnshared(ContentID contentID) { ContentProviderEvent event = null; for (ContentProviderListener listener : listeners) { if (event == null) { event = new ContentProviderEvent.Builder(this, contentID) .build(); } listener.contentUnshared(event); } } }