/*
* 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.defprovider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.Thread.UncaughtExceptionHandler;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
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;
import net.jxta.logging.Logging;
import net.jxta.content.Content;
import net.jxta.content.ContentID;
import net.jxta.content.ContentShare;
import net.jxta.content.ContentTransfer;
import net.jxta.content.ContentProviderSPI;
import net.jxta.content.ContentProviderEvent;
import net.jxta.content.ContentProviderListener;
import net.jxta.content.NullContentTransfer;
import net.jxta.endpoint.Message;
import net.jxta.protocol.ContentShareAdvertisement;
import net.jxta.id.ID;
import net.jxta.peergroup.PeerGroup;
import net.jxta.pipe.PipeMsgEvent;
import net.jxta.pipe.PipeMsgListener;
import net.jxta.pipe.PipeService;
import net.jxta.protocol.PipeAdvertisement;
import net.jxta.document.Advertisement;
import net.jxta.document.AdvertisementFactory;
import net.jxta.document.MimeMediaType;
import net.jxta.document.StructuredDocument;
import net.jxta.document.StructuredDocumentFactory;
import net.jxta.document.XMLDocument;
import net.jxta.endpoint.ByteArrayMessageElement;
import net.jxta.endpoint.MessageElement;
import net.jxta.endpoint.TextDocumentMessageElement;
import net.jxta.id.IDFactory;
import net.jxta.impl.content.ModuleWrapperFactory;
import net.jxta.pipe.InputPipe;
import net.jxta.pipe.OutputPipe;
import net.jxta.pipe.PipeID;
import net.jxta.platform.Module;
import net.jxta.platform.ModuleSpecID;
import net.jxta.protocol.ModuleImplAdvertisement;
/**
* Reference implementation of the ContentService. 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. 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
* stream.
* </li>
* <li>
* Content is shared by calling the shareContent(Content) method and
* then making another peer aware of the returned
* ContentShareAdvertisement. How the remote peer is made aware of
* the ContentShareAdvertisement is of no concern.
* </li>
* <li>
* No validation is performed on the transferred data. All validation
* must be done out-of-band and post-transfer.
* </li>
* <li>
* Service to remote peers will be performed internally as follows:
* <ul>
* <li>
* Requestors who have requested data first will receive higher
* priority on subsequent requests. This biases responses
* toward a single remote peer, allowing that peer to complete
* as quickly as possible.
* </li>
* <li>
* Only a certain number of requestors will be able to be
* served concurrently. Those requestors who cannot be served
* will be completely ignored.
* </li>
* <li>
* The requestors are expected, though not required, to
* access data linearly.
* </li>
* </ul>
* </li>
* </ul>
*
* See the <code>DataRequest</code> and <code>DataResponse</code>
* classes for the protocol description.
*
* This implementation currently uses a rather bogus method for locating
* data sources. This bogus implementation amounts to a periodic
* ResolverService-based broadcast and should be rewritten to use a
* subscription service of some sort.
*/
public class DefaultContentProvider implements
ContentProviderSPI, PipeMsgListener, ActiveTransferTrackerListener {
/**
* Logger instance.
*/
private static final Logger LOG =
Logger.getLogger(DefaultContentProvider.class.getName());
/**
* Maximum incoming message queue size. Once full, additional incoming
* requests will be dropped.
*/
private static final int MAX_QUEUE_SIZE =
Integer.getInteger(DefaultContentProvider.class.getName()
+ ".maxQueue", 256).intValue();
/**
* Message namespace used to identify our message elements.
*/
protected static final String MSG_NAMESPACE = "DefCont";
/**
* Message element name used to identify our data requests/responses.
*/
protected static final String MSG_ELEM_NAME = "DR";
/**
* Module spec ID for this provider.
*/
private static final String MODULE_SPEC_ID =
"urn:jxta:uuid-AC3AA08FC4A14C15A78A88B4D4F87554"
+ "CDC361792F3F4EF9A6488BE56396AAEB06";
/**
* Parsed and ready-to-use version of MODULE_SPEC_ID.
*/
private static final ModuleSpecID specID;
// Initialized at construction
private final Map<ID, DefaultContentShare> shares =
new HashMap<ID, DefaultContentShare>();
private final CopyOnWriteArrayList<ContentProviderListener> listeners =
new CopyOnWriteArrayList<ContentProviderListener>();
private final Queue<PipeMsgEvent> msgQueue =
new ArrayBlockingQueue<PipeMsgEvent>(MAX_QUEUE_SIZE);
// Initialized by init
private PeerGroup peerGroup;
private ScheduledExecutorService executor;
private PipeAdvertisement pipeAdv;
// Initialized and managed by start/stop
private ActiveTransferTracker tracker;
private InputPipe requestPipe;
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(DefaultContentProvider.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);
}
}
}
//////////////////////////////////////////////////////////////////////////
// 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);
tracker = new ActiveTransferTracker(group, executor);
tracker.addActiveTransferListener(this);
}
/**
* {@inheritDoc}
*/
public synchronized int startApp(String[] args) {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("startApp()");
}
if (running) {
return Module.START_OK;
}
running = true;
if (requestPipe == null) {
try {
PipeService pipeService = peerGroup.getPipeService();
if (pipeService == null) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Stalled until there is a pipe service");
}
return Module.START_AGAIN_STALLED;
}
requestPipe = pipeService.createInputPipe(pipeAdv, this);
} catch (IOException iox) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Could not create input pipe", iox);
}
requestPipe = null;
return Module.START_AGAIN_STALLED;
}
}
// Start the accept loop
executor.execute(new Runnable() {
public void run() {
try {
processMessages();
} catch (InterruptedException intx) {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Interrupted", intx);
}
Thread.interrupted();
}
}
});
tracker.start();
return Module.START_OK;
}
/**
* {@inheritDoc}
*/
public synchronized void stopApp() {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("stopApp()");
}
if (!running) {
return;
}
tracker.stop();
msgQueue.clear();
/*
* 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 a simple, "
+ "proprietary, portable, but slow protocol");
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 DefaultContentTransfer(this, executor, peerGroup, contentID);
}
/**
* {@inheritDoc}
*/
public ContentTransfer retrieveContent(ContentShareAdvertisement adv) {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("retrieveContent(" + adv + ")");
}
synchronized(this) {
if (!running) {
return null;
}
}
synchronized(shares) {
ContentShare share = getShare(adv.getContentID());
if (share != null) {
return new NullContentTransfer(this, share.getContent());
}
}
return new DefaultContentTransfer(this, executor, peerGroup, adv);
}
/**
* {@inheritDoc}
*/
public List<ContentShare> shareContent(Content content) {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("shareContent(): Content=" + content + " " + this);
}
PipeAdvertisement pAdv;
synchronized(this) {
pAdv = pipeAdv;
}
if (pipeAdv == null) {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("Cannot create share before initialization");
}
return null;
}
List<ContentShare> result = new ArrayList<ContentShare>(1);
ID id = content.getContentID();
DefaultContentShare share;
synchronized(shares) {
share = getShare(id);
if (share == null) {
share = new DefaultContentShare(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 = new ArrayList<ContentShare>();
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 peer group the service is running in
*/
protected PeerGroup getPeerGroup() {
return peerGroup;
}
//////////////////////////////////////////////////////////////////////////
// PipeMsgListener interface methods:
/**
* Processes incoming Content service requests.
*
* @param pme incoming pipe message event
*/
public synchronized void pipeMsgEvent(PipeMsgEvent pme) {
if (!running) {
return;
}
if (msgQueue.offer(pme)) {
notifyAll();
} else {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("Dropped message due to full queue");
}
}
}
//////////////////////////////////////////////////////////////////////////
// ActiveTransferTrackerListener interface methods:
/**
* {@inheritDoc}
*/
public void sessionCreated(ActiveTransfer transfer) {
DefaultContentShare share = transfer.getContentShare();
share.fireShareSessionOpened(transfer);
}
/**
* {@inheritDoc}
*/
public void sessionCollected(ActiveTransfer transfer) {
DefaultContentShare share = transfer.getContentShare();
share.fireShareSessionClosed(transfer);
}
//////////////////////////////////////////////////////////////////////////
// Private methods:
/**
* Returns the content share entry for the specified ContentID.
*
* @return content share
*/
private DefaultContentShare getShare(ID id) {
synchronized(shares) {
return shares.get(id);
}
}
/**
* Reentrant method used for multi-threaded processing of incoming
* messages. This method and all those it calls must remain perfectly
* reentrant.
*/
private void processMessages() throws InterruptedException {
PipeMsgEvent pme;
Message msg;
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("Worker thread starting");
}
while (true) {
synchronized(this) {
if (!running) {
break;
}
pme = msgQueue.poll();
if (pme == null) {
wait();
continue;
}
}
try {
msg = pme.getMessage();
processMessage(msg);
} catch (Exception x) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Uncaught exception", x);
}
}
}
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("Worker thread closing up shop");
}
}
/**
* Process the incoming message.
*/
private void processMessage(Message msg) {
MessageElement msge;
ListIterator it;
StructuredDocument doc;
DataRequest req;
if (Logging.SHOW_FINEST && LOG.isLoggable(Level.FINEST)) {
LOG.finest("Incoming message:\n" + msg.toString() + "\n");
}
it = msg.getMessageElementsOfNamespace(MSG_NAMESPACE);
while (it.hasNext()) {
msge = (MessageElement) it.next();
if (!MSG_ELEM_NAME.endsWith(msge.getElementName())) {
// Not a data request
continue;
}
try {
doc = StructuredDocumentFactory.newStructuredDocument(msge);
req = new DataRequest(doc);
} catch (IOException iox) {
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Could not process message", iox);
}
return;
}
if (Logging.SHOW_FINEST && LOG.isLoggable(Level.FINEST)) {
LOG.finest("Request: "
+ req.getDocument(MimeMediaType.XMLUTF8).toString());
}
processDataRequest(req);
}
}
/**
* Processes an incoming data request.
*/
private void processDataRequest(DataRequest req) {
ByteArrayOutputStream byteOut = null;
DataResponse resp;
DefaultContentShare share;
int written;
if (Logging.SHOW_FINEST && LOG.isLoggable(Level.FINEST)) {
LOG.finest("DataRequest:");
LOG.finest(" ContentID: " + req.getContentID());
LOG.finest(" Offset : " + req.getOffset());
LOG.finest(" Length : " + req.getLength());
LOG.finest(" QID : " + req.getQueryID());
LOG.finest(" PipeAdv: " + req.getResponsePipe());
}
share = getShare(req.getContentID());
if (share == null) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Content not shared");
}
return;
}
try {
ActiveTransfer session = tracker.getSession(
share, req.getResponsePipe());
byteOut = new ByteArrayOutputStream();
written = session.getData(
req.getOffset(), req.getLength(), byteOut);
// Send response
resp = new DataResponse(req);
if (written <= 0) {
written = -written;
resp.setEOF(true);
}
resp.setLength(written);
share.fireShareAccessed(session, resp);
sendDataResponse(resp, session.getOutputPipe(),
(written == 0) ? null : byteOut.toByteArray());
} catch (TooManyClientsException tmcx) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Too many concurrent clients. Discarding.");
}
} catch (IOException iox) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Exception while handling data request", iox);
}
}
}
/**
* Sends a response to the destination specified.
*/
private void sendDataResponse(DataResponse resp, OutputPipe destPipe,
byte[] data) {
MessageElement msge;
XMLDocument doc;
Message msg;
msg = new Message();
doc = (XMLDocument) resp.getDocument(MimeMediaType.XMLUTF8);
msge = new TextDocumentMessageElement(MSG_ELEM_NAME, doc, null);
msg.addMessageElement(MSG_NAMESPACE, msge);
if (data != null) {
msge = new ByteArrayMessageElement("data",
new MimeMediaType("application", "octet-stream"),
data, null);
msg.addMessageElement(MSG_NAMESPACE, msge);
}
if (Logging.SHOW_FINER && LOG.isLoggable(Level.FINER)) {
LOG.finer("Sending response: " + msg.toString());
}
try {
if (destPipe.send(msg)) {
return;
}
} catch (IOException iox) {
if (Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "IOException during message send", iox);
}
}
if (Logging.SHOW_FINE && LOG.isLoggable(Level.FINE)) {
LOG.fine("Did not send message");
}
}
/**
* 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);
}
}
}