// // $Id$ // // Narya library - tools for developing networked games // Copyright (C) 2002-2011 Three Rings Design, Inc., All Rights Reserved // http://code.google.com/p/narya/ // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.crowd.server; import java.util.List; import java.util.Map; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.samskivert.util.HashIntMap; import com.samskivert.util.Interval; import com.samskivert.util.MethodFinder; import com.samskivert.util.StringUtil; import com.threerings.presents.data.ClientObject; import com.threerings.presents.data.InvocationMarshaller; import com.threerings.presents.dobj.AccessController; import com.threerings.presents.dobj.DObject; import com.threerings.presents.dobj.DSet; import com.threerings.presents.dobj.DynamicListener; import com.threerings.presents.dobj.EntryUpdatedEvent; import com.threerings.presents.dobj.MessageEvent; import com.threerings.presents.dobj.MessageListener; import com.threerings.presents.dobj.NamedSetAdapter; import com.threerings.presents.dobj.ObjectAddedEvent; import com.threerings.presents.dobj.ObjectDeathListener; import com.threerings.presents.dobj.ObjectDestroyedEvent; import com.threerings.presents.dobj.ObjectRemovedEvent; import com.threerings.presents.dobj.OidListListener; import com.threerings.presents.dobj.RootDObjectManager; import com.threerings.presents.dobj.SetAdapter; import com.threerings.presents.server.InvocationDispatcher; import com.threerings.presents.server.InvocationManager; import com.threerings.presents.server.InvocationProvider; import com.threerings.crowd.chat.data.ChatCodes; import com.threerings.crowd.chat.data.SpeakMarshaller; import com.threerings.crowd.chat.server.SpeakHandler; import com.threerings.crowd.data.BodyObject; import com.threerings.crowd.data.OccupantInfo; import com.threerings.crowd.data.Place; import com.threerings.crowd.data.PlaceConfig; import com.threerings.crowd.data.PlaceObject; import com.threerings.crowd.server.CrowdObjectAccess.PlaceAccessController; import com.google.inject.Inject; import com.google.inject.Injector; import static com.threerings.crowd.Log.log; /** * The place manager is the server-side entity that handles all place-related interaction. It * subscribes to the place object and reacts to message and other events. Behavior specific to a * place (or class of places) should live in the place manager. An intelligently constructed * hierarchy of place manager classes working in concert with invocation services should provide * the majority of the server-side functionality of an application built on the Presents platform. * *
The base place manager class takes care of the necessary interactions with the place * registry to manage place registration. It handles the place-related component of chatting. It * also provides the basis for place-based access control. * *
A derived class is expected to handle initialization, cleanup and operational functionality
* via the calldown functions {@link #didInit}, {@link #didStartup}, and {@link #didShutdown} as
* well as through event listeners.
*/
public class PlaceManager
implements MessageListener, SpeakHandler.SpeakerValidator
{
/**
* An interface used to allow the registration of standard message handlers to be invoked by
* the place manager when particular types of message events are received.
*
* @deprecated Use dynamically bound methods instead. See {@link DynamicListener}.
*/
@Deprecated
public static interface MessageHandler
{
/**
* Invokes this message handler on the supplied event.
*
* @param event the message event received.
* @param pmgr the place manager for which the message is being handled.
*/
void handleEvent (MessageEvent event, PlaceManager pmgr);
}
/**
* Used to call methods on this place manager's delegates.
*/
public static abstract class DelegateOp
{
public DelegateOp (Class extends PlaceManagerDelegate> delegateClass) {
_delegateClass = delegateClass;
}
/** Applies an operation to the supplied delegate. */
public abstract void apply (PlaceManagerDelegate delegate);
public boolean shouldApply (PlaceManagerDelegate delegate) {
return _delegateClass.isInstance(delegate);
}
protected Class extends PlaceManagerDelegate> _delegateClass;
}
/**
* Returns a reference to our place configuration object.
*/
public PlaceConfig getConfig ()
{
return _config;
}
/**
* Returns a {@link Place} instance that identifies this place.
*/
public Place getLocation ()
{
return new Place(_plobj.getOid());
}
/**
* Returns the place object managed by this place manager.
*/
public PlaceObject getPlaceObject ()
{
return _plobj;
}
/**
* Applies the supplied occupant operation to each occupant currently present in this place.
*/
public void applyToOccupants (OccupantOp op)
{
if (_plobj != null) {
for (OccupantInfo info : _plobj.occupantInfo) {
op.apply(info);
}
}
}
/**
* Calls the supplied updater on the canonical occupant info record for the specified body
* (which must be an occupant of this place) and broadcasts the update to all other occupants.
*
* @return true if the updater was called and the update sent, false if the body could not be
* located (was not an occupant of this place) or the updater made no modifications.
*
* @exception ClassCastException thrown if the type of the supplied updater does not match the
* type of {@link OccupantInfo} record used for the occupant. Caveat utilitor.
*/
public null should be returned if initialization is to be
* allowed to proceed.
*/
public String checkPermissions ()
{
return null;
}
/**
* Called by the place manager after the place object has been successfully created.
*/
public void startup (PlaceObject plobj)
{
// keep track of this
_plobj = plobj;
// we usually want to create and register a speaker service instance that clients can use
// to speak in this place
if (shouldCreateSpeakService()) {
plobj.setSpeakService(addProvider(createSpeakHandler(plobj), SpeakMarshaller.class));
}
// we'll need to hear about place object events
plobj.addListener(this);
plobj.addListener(_bodyUpdater);
plobj.addListener(_occListener);
plobj.addListener(_deathListener);
// configure this place's access controller
plobj.setAccessController(getAccessController());
// let our derived classes do their thang
try {
didStartup();
} catch (Throwable t) {
log.warning("Manager choked in didStartup()", "where", where(), t);
}
// since we start empty, we need to immediately assume shutdown
checkShutdownInterval();
}
/**
* Causes the place object being managed by this place manager to be destroyed and the place
* manager to shut down.
*/
public void shutdown ()
{
// destroy the object and everything will follow from that
_omgr.destroyObject(_plobj.getOid());
// make sure we don't have any shutdowner in the queue
cancelShutdowner();
}
/**
* Provides an opportunity for the place manager to prevent bodies from entering.
*
* @return null if the body can enter, otherwise a translatable message explaining
* the reason the body is blocked from entering
*/
public String ratifyBodyEntry (BodyObject body)
{
return null;
}
/**
* This is called to inform the manager that a body is on the way in. This is called at the
* very beginning of the entry process before the client is informed that it is allowed to
* enter. This will be followed by a call to {@link #bodyEntered} once all events relating to
* body entry have been processed.
*/
public void bodyWillEnter (BodyObject body)
{
// create a new occupant info instance and insert it into our canonical table
OccupantInfo info = body.createOccupantInfo(_plobj);
_occInfo.put(info.getBodyOid(), info);
_plobj.startTransaction();
try {
addOccupantInfo(body, info.clone());
} finally {
_plobj.commitTransaction();
}
}
/**
* Called to inform a manager that a body is about to leave this place. This will be followed
* by a call to {@link #bodyLeft} once all events relating to body entry have been processed.
*/
public void bodyWillLeave (BodyObject body)
{
_plobj.startTransaction();
try {
// remove their occupant info (which is keyed on oid)
_plobj.removeFromOccupantInfo(body.getOid());
// and remove them from the occupant list
_plobj.removeFromOccupants(body.getOid());
} finally {
_plobj.commitTransaction();
}
}
/**
* Registers a particular message handler instance to be used when processing message events
* with the specified name.
*
* @param name the message name of the message events that should be handled by this handler.
* @param handler the handler to be registered.
*
* @deprecated Use dynamically bound methods instead. See {@link DynamicListener}.
*/
@Deprecated
public void registerMessageHandler (String name, MessageHandler handler)
{
// create our handler map if necessary
if (_msghandlers == null) {
_msghandlers = Maps.newHashMap();
}
_msghandlers.put(name, handler);
}
// from interface MessageListener
public void messageReceived (MessageEvent event)
{
if (_msghandlers != null) {
MessageHandler handler = _msghandlers.get(event.getName());
if (handler != null) {
handler.handleEvent(event, this);
}
}
// If the message is directed at us, see if it's a request for a method invocation
if (event.isPrivate()) { // aka if (event instanceof ServerMessageEvent)
// the first argument should be the client object of the caller or null if it is
// a server-originated event
int srcoid = event.getSourceOid();
DObject source = (srcoid <= 0) ? null : _omgr.getObject(srcoid);
Object[] args = event.getArgs(), nargs;
if (args == null) {
nargs = new Object[] { source };
} else {
nargs = new Object[args.length+1];
nargs[0] = source;
System.arraycopy(args, 0, nargs, 1, args.length);
}
// Lazily create our dispatcher now that it's actually getting a message
if (_dispatcher == null) {
Class> clazz = getClass();
MethodFinder finder = _dispatcherFinders.get(clazz);
if (finder == null) {
finder = new MethodFinder(clazz);
_dispatcherFinders.put(clazz, finder);
}
_dispatcher = new DynamicListenersuper.didInit().
*/
protected void didInit ()
{
// initialize our delegates
applyToDelegates(new DelegateOp(PlaceManagerDelegate.class) {
@Override
public void apply (PlaceManagerDelegate delegate) {
delegate.didInit(_config);
}
});
}
/**
* Called if the permissions check failed, to give place managers a chance to do any cleanup
* that might be necessary due to their early initialization or permissions checking code.
*/
protected void permissionsFailed ()
{
}
/**
* @return true if we should create a speaker service for our place object so that clients can
* use it to speak in this place.
*/
protected boolean shouldCreateSpeakService ()
{
return true;
}
/**
* Creates an access controller for this place's distributed object, which by default is {@link
* PlaceAccessController}.
*/
protected AccessController getAccessController ()
{
return _injector.getInstance(PlaceAccessController.class);
}
/**
* Derived classes should override this (and be sure to call super.didStartup())
* to perform any startup time initialization. The place object will be available by the time
* this method is executed.
*/
protected void didStartup ()
{
// let our delegates know that we've started up
applyToDelegates(new DelegateOp(PlaceManagerDelegate.class) {
@Override
public void apply (PlaceManagerDelegate delegate) {
delegate.didStartup(_plobj);
}
});
}
/**
* Called when this place has been destroyed and the place manager has shut down (via a call to
* {@link #shutdown}). Derived classes can override this method and perform any necessary
* shutdown time processing.
*/
protected void didShutdown ()
{
// clear out our listenership
_plobj.removeListener(this);
_plobj.removeListener(_bodyUpdater);
_plobj.removeListener(_occListener);
_plobj.removeListener(_deathListener);
// clear out our invocation service registrations
for (InvocationMarshaller marsh : _marshallers) {
_invmgr.clearDispatcher(marsh);
}
// let our delegates know that we've shut down
applyToDelegates(new DelegateOp(PlaceManagerDelegate.class) {
@Override
public void apply (PlaceManagerDelegate delegate) {
delegate.didShutdown();
}
});
// if shutting down emptied the place and scheduled the shutdowner, clear that out
cancelShutdowner();
}
/**
* Registers an invocation provider and notes the registration such that it will be
* automatically cleared when this manager shuts down.
*/
protected 0 indicates that the place should never be
* shutdown.
*/
protected long idleUnloadPeriod ()
{
return 5 * 60 * 1000L;
}
/**
* An extensible way to add to the string representation of this class. Override this (being
* sure to call super) and append your info to the buffer.
*/
protected void toString (StringBuilder buf)
{
buf.append("place=").append(_plobj);
buf.append(", config=").append(_config);
}
/**
* Creates the speak handler for this place. Derived classes can customize the speak handler if
* they so desire.
*/
protected SpeakHandler createSpeakHandler (PlaceObject plobj)
{
return new SpeakHandler(_locator, plobj, this);
}
/** Listens for occupant updates. */
protected SetAdapter