Convert Narya (most of the way) over to a Maven Ant task based build. The

ActionScript bits remain belligerent, but the Java stuff is mostly shipshape.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@6222 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Michael Bayne
2010-10-22 21:12:29 +00:00
parent 555b865bbf
commit 9d2ca42eac
434 changed files with 163 additions and 208 deletions
@@ -0,0 +1,33 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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;
import com.samskivert.util.Logger;
/**
* Contains a reference to the log object used by this project.
*/
public class NaryaLog
{
/** We dispatch our log messages through this logger. */
public static Logger log = Logger.getLogger("com.threerings.narya");
}
@@ -0,0 +1,33 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin;
import com.samskivert.util.Logger;
/**
* Contains a reference to the log object used by the Admin services.
*/
public class Log
{
/** We dispatch our log messages through this logger. */
public static Logger log = Logger.getLogger("com.threerings.narya.admin");
}
@@ -0,0 +1,47 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.client;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
/**
* Defines the client side of the admin invocation services.
*/
public interface AdminService extends InvocationService
{
/**
* Used to communicate a response to a {@link AdminService#getConfigInfo} request.
*/
public static interface ConfigInfoListener extends InvocationListener
{
/**
* Delivers a successful response to a {@link AdminService#getConfigInfo} request.
*/
void gotConfigInfo (String[] keys, int[] oids);
}
/**
* Requests the list of config objects.
*/
void getConfigInfo (Client client, ConfigInfoListener listener);
}
@@ -0,0 +1,114 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.client;
import java.lang.reflect.Field;
import javax.swing.JTextField;
import com.samskivert.util.StringUtil;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.util.PresentsContext;
import static com.threerings.admin.Log.log;
/**
* Used to display and edit a particular distributed object field.
*/
public class AsStringFieldEditor extends FieldEditor
{
public AsStringFieldEditor (PresentsContext ctx, Field field, DObject object)
{
super(ctx, field, object);
// and a text entry field to display the field value
add(_value = new JTextField());
_value.addActionListener(this);
// we want to let the user know if they remove focus from a text
// box without changing a field that it's not saved
_value.addFocusListener(this);
}
@Override
protected Object getDisplayValue ()
throws Exception
{
String text = _value.getText();
if (_field.getType().equals(Integer.class) ||
_field.getType().equals(Integer.TYPE)) {
return new Integer(text);
} else if (_field.getType().equals(Short.class) ||
_field.getType().equals(Short.TYPE)) {
return new Short(text);
} else if (_field.getType().equals(Byte.class) ||
_field.getType().equals(Byte.TYPE)) {
return new Byte(text);
} else if (_field.getType().equals(Long.class) ||
_field.getType().equals(Long.TYPE)) {
return new Long(text);
} else if (_field.getType().equals(Float.class) ||
_field.getType().equals(Float.TYPE)) {
return new Float(text);
} else if (_field.getType().equals(Double.class) ||
_field.getType().equals(Double.TYPE)) {
return new Double(text);
} else if (_field.getType().equals(String.class)) {
return text;
} else if (_field.getType().equals(String[].class)) {
return StringUtil.parseStringArray(_value.getText());
} else if (_field.getType().equals(int[].class)) {
return StringUtil.parseIntArray(_value.getText());
} else if (_field.getType().equals(float[].class)) {
return StringUtil.parseFloatArray(_value.getText());
} else if (_field.getType().equals(long[].class)) {
return StringUtil.parseLongArray(_value.getText());
} else if (_field.getType().equals(Boolean.TYPE)) {
return new Boolean(_value.getText().equalsIgnoreCase("true"));
} else {
log.warning("Unknown field type '" + _field.getName() + "': " +
_field.getType().getName() + ".");
return null;
}
}
@Override
protected void displayValue (Object value)
{
_value.setText(StringUtil.toString(value, "", ""));
}
protected JTextField _value;
}
@@ -0,0 +1,75 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.client;
import java.lang.reflect.Field;
import javax.swing.JCheckBox;
import javax.swing.JPanel;
import com.samskivert.swing.GroupLayout;
import com.samskivert.swing.HGroupLayout;
import com.samskivert.swing.Spacer;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.util.PresentsContext;
/**
* Provides "editing" of boolean fields.
*/
public class BooleanFieldEditor extends FieldEditor
{
public BooleanFieldEditor (PresentsContext ctx, Field field, DObject object)
{
super(ctx, field, object);
JPanel jpan = new JPanel(new HGroupLayout(HGroupLayout.STRETCH));
// add a checkbox to display the field value
jpan.add(_value = new JCheckBox(), GroupLayout.FIXED);
// add a spacer so that clicks to the right of the checkbox
// don't toggle it
jpan.add(new Spacer(1, 1));
_value.addActionListener(this);
add(jpan);
// we want to let the user know if they remove focus from a text
// box without changing a field that it's not saved
_value.addFocusListener(this);
}
@Override
protected Object getDisplayValue ()
throws Exception
{
return Boolean.valueOf(_value.isSelected());
}
@Override
protected void displayValue (Object value)
{
_value.setSelected(Boolean.TRUE.equals(value));
}
protected JCheckBox _value;
}
@@ -0,0 +1,155 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.client;
import java.awt.Component;
import java.util.Comparator;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import com.samskivert.util.QuickSort;
import com.samskivert.swing.VGroupLayout;
import com.threerings.presents.util.PresentsContext;
import static com.threerings.admin.Log.log;
/**
* Fetches a list of the configuration objects in use by the server and
* displays their fields in a tree widget to be viewed and edited.
*/
public class ConfigEditorPanel extends JPanel
implements AdminService.ConfigInfoListener
{
/**
* Constructs an editor panel which will use the supplied context to
* access the distributed object services.
*/
public ConfigEditorPanel (PresentsContext ctx)
{
this(ctx, null);
}
/**
* Constructs an editor panel with the specified pane defaulting to
* selected.
*/
public ConfigEditorPanel (PresentsContext ctx, String defaultPane)
{
_ctx = ctx;
_defaultPane = defaultPane;
setLayout(new VGroupLayout(VGroupLayout.STRETCH, VGroupLayout.STRETCH,
VGroupLayout.DEFAULT_GAP, VGroupLayout.CENTER));
// create our objects tabbed pane
add(_oeditors = new JTabbedPane(JTabbedPane.LEFT));
// If they don't fit, make them scroll, since wrapped vertical tabs eats insane sceen space
_oeditors.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
// add a handy label at the bottom
add(new JLabel("Fields outline in red have been modified but not yet committed."),
VGroupLayout.FIXED);
add(new JLabel("Press return in a modified field to commit the change."),
VGroupLayout.FIXED);
}
@Override
public void addNotify ()
{
super.addNotify();
// ship off a getConfigInfo request to find out what config
// objects are available for editing
AdminService service = _ctx.getClient().requireService(AdminService.class);
service.getConfigInfo(_ctx.getClient(), this);
}
@Override
public void removeNotify ()
{
super.removeNotify();
// when we're hidden, we want to clear out our subscriptions
int ccount = _oeditors.getComponentCount();
for (int ii = 0; ii < ccount; ii++) {
Component comp = _oeditors.getComponent(ii);
if (comp instanceof JScrollPane) {
JScrollPane scrolly = (JScrollPane)comp;
ObjectEditorPanel opanel = (ObjectEditorPanel)scrolly.getViewport().getView();
opanel.cleanup();
}
}
_oeditors.removeAll();
}
/**
* Called in response to our getConfigInfo server-side service request.
*/
public void gotConfigInfo (final String[] keys, final int[] oids)
{
// make sure we're still added
if (!isDisplayable()) {
return;
}
Integer indexes[] = new Integer[keys.length];
for (int ii = 0; ii < indexes.length; ii++) {
indexes[ii] = ii;
}
QuickSort.sort(indexes, new Comparator<Integer>() {
public int compare (Integer i1, Integer i2) {
return keys[i1].compareTo(keys[i2]);
}
});
// create object editor panels for each of the categories
for (Integer ii : indexes) {
ObjectEditorPanel panel = new ObjectEditorPanel(_ctx, keys[ii], oids[ii]);
JScrollPane scrolly = new JScrollPane(panel);
_oeditors.addTab(keys[ii], scrolly);
if (keys[ii].equals(_defaultPane)) {
_oeditors.setSelectedComponent(scrolly);
}
}
}
// documentation inherited from interface
public void requestFailed (String reason)
{
log.warning("Failed to get config info", "reason", reason);
}
/** Our client context. */
protected PresentsContext _ctx;
/** Holds our object editors. */
protected JTabbedPane _oeditors;
/** Our default tab pane. */
protected String _defaultPane;
}
@@ -0,0 +1,165 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.client;
import java.util.HashMap;
import com.google.common.collect.Maps;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.ClientAdapter;
import com.threerings.presents.dobj.DObjectManager;
import com.threerings.presents.dobj.ObjectAccessException;
import com.threerings.presents.dobj.Subscriber;
import com.threerings.admin.data.AdminCodes;
import com.threerings.admin.data.ConfigObject;
import static com.threerings.admin.Log.log;
/**
* Handles subscribing to admin config objects.
*/
public class ConfigObjectManager implements AdminService.ConfigInfoListener
{
public ConfigObjectManager (Client client)
{
_serverconfig = Maps.newHashMap();
_client = client;
_client.addClientObserver(new ClientAdapter() {
@Override
public void clientWillLogon (Client client) {
client.addServiceGroup(AdminCodes.ADMIN_GROUP);
}
@Override
public void clientDidLogon (Client client) {
_dobjmgr = _client.getDObjectManager();
_service = client.requireService(AdminService.class);
getConfigInfo();
}
@Override
public void clientDidLogoff (Client client) {
// Clean up our subscription to the server's configuration
for (ConfigObjectSubscriber _csubscriber : _csubscribers) {
_csubscriber.cleanup();
}
}
});
}
/**
* Returns the ConfigObject identified by the given key.
*/
public ConfigObject getServerConfig (String key)
{
return _serverconfig.get(key);
}
// documentation inherited from interface AdminService.ConfigInfoListener
public void gotConfigInfo (String[] keys, int[] oids)
{
_csubscribers = new ConfigObjectSubscriber[keys.length];
for (int ii = 0; ii < keys.length; ii++) {
_csubscribers[ii] = new ConfigObjectSubscriber();
_csubscribers[ii].subscribeConfig(keys[ii], oids[ii]);
}
}
// documentation inherited from interface AdminService.ConfigInfoListener
public void requestFailed (String reason)
{
log.warning("Oh bugger, we didn't get the config data: " + reason);
}
/**
* Convenience: generate a getConfigInfo request to the AdminService from the external class,
* instead from within the anonymous inner class.
*/
protected void getConfigInfo ()
{
_service.getConfigInfo(_client, this);
}
/**
* This class takes care of the details of subscribing to and placing an individual
* ConfigObject that the server knows about into a HashMap.
*/
protected class ConfigObjectSubscriber implements Subscriber<ConfigObject>
{
/**
* This method requests that we place a subscription to the ConfigObject with the given
* oid, identified by the key; when the object becomes available, it's added to our
* serverconfig map.
*/
public void subscribeConfig (String key, int oid) {
_key = key;
_oid = oid;
_dobjmgr.subscribeToObject(_oid, this);
}
// documentation inherited from interface Subscriber
public void objectAvailable (ConfigObject object) {
_cobj = object;
_serverconfig.put(_key, _cobj);
}
// documentation inherited from interface Subscriber
public void requestFailed (int oid, ObjectAccessException cause) {
log.warning("Unable to subscribe to config object " + _key);
}
/**
* Signals that we should stop subscribing to our ConfigObject, and flush out the entry
* from the serverconfig map.
*/
public void cleanup () {
// clear out our subscription
_dobjmgr.unsubscribeFromObject(_oid, this);
_cobj = null;
_serverconfig.remove(_key);
}
/** The object that we are tracking. */
protected ConfigObject _cobj;
/** The name of the config object to which we are subscribing. */
protected String _key;
/** The oid of the object that we're tracking. */
protected int _oid;
}
/** An array of handlers that each subscribe to a single ConfigObject. */
protected ConfigObjectSubscriber[] _csubscribers;
/** Our local copy of the server-side runtime configuration. */
protected HashMap<String, ConfigObject> _serverconfig;
/** Our distributed object manager. */
protected DObjectManager _dobjmgr;
/** Our admin service that we're using to fetch data. */
protected AdminService _service;
/** Our client object. */
protected Client _client;
}
@@ -0,0 +1,396 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.client;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Iterator;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.samskivert.util.ComparableArrayList;
import com.samskivert.swing.ObjectEditorTable;
import com.samskivert.swing.event.CommandEvent;
import com.threerings.presents.dobj.AttributeChangeListener;
import com.threerings.presents.dobj.AttributeChangedEvent;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.DSet;
import com.threerings.presents.dobj.EntryAddedEvent;
import com.threerings.presents.dobj.EntryRemovedEvent;
import com.threerings.presents.dobj.EntryUpdatedEvent;
import com.threerings.presents.dobj.SetListener;
/**
* Allows simple editing of DSets within a distributed object.
*/
public class DSetEditor<E extends DSet.Entry> extends JPanel
implements ActionListener
{
/**
* An interface for a plugin defining how the editor interacts with its underlying data.
*/
public interface Accessor<F extends DSet.Entry>
{
void added ();
void removed ();
void updateEntry (String setName, DSet.Entry entry);
ObjectEditorTable.FieldInterpreter getInterp (ObjectEditorTable.FieldInterpreter interp);
}
/**
* Construct a DSet editor to merely display the specified set.
*
* @param setter The object that contains the set.
* @param setName The name of the set in the object.
* @param entryClass The Class of the DSet.Entry elements contained in the set.
*/
public DSetEditor (DObject setter, String setName, Class<?> entryClass)
{
this(setter, setName, entryClass, null);
}
/**
* Construct a DSetEditor, allowing the specified fields to be edited.
*
* @param setter The object that contains the set.
* @param setName The name of the set in the object.
* @param entryClass The Class of the DSet.Entry elements contained in the set.
* @param editableFields the names of the fields in the entryClass that should be editable.
*/
public DSetEditor (
DObject setter, String setName, Class<?> entryClass, String[] editableFields)
{
this(setter, setName, entryClass, editableFields, null);
}
/**
* Construct a DSetEditor with a custom FieldInterpreter.
*
* @param setter The object that contains the set.
* @param setName The name of the set in the object.
* @param entryClass the Class of the DSet.Entry elements contained in the set.
* @param editableFields The names of the fields in the entryClass that should be editable.
* @param interp The FieldInterpreter to use.
*/
public DSetEditor (
DObject setter, String setName, Class<?> entryClass, String[] editableFields,
ObjectEditorTable.FieldInterpreter interp)
{
this(setter, setName, entryClass, editableFields, interp, null, null);
}
/**
* Construct a DSetEditor that only displays entries that match the given Predicate.
*
* @param setter The object that contains the set.
* @param setName The name of the set in the object.
* @param entryClass The Class of the DSet.Entry elements contained in the set.
* @param editableFields The names of the fields in the entryClass that should be editable.
* @param interp The FieldInterpreter to use.
* @param displayFields The fields to display, or null for all.
* @param entryFilter The Predicate to use.
*/
public DSetEditor (
DObject setter, String setName, Class<?> entryClass, String[] editableFields,
ObjectEditorTable.FieldInterpreter interp, String[] displayFields, Predicate<E> entryFilter)
{
super(new BorderLayout());
_setName = setName;
_entryFilter = entryFilter;
_entryClass = entryClass;
_editableFields = editableFields;
_interp = interp;
_displayFields = displayFields;
setAccessor(new DObjectAccessor<E>(setter, setName));
}
/**
* Sets the logic for how this editor interacts with its underlying data.
*/
public void setAccessor (Accessor<E> accessor)
{
removeAll();
_accessor = accessor;
_table = new ObjectEditorTable(_entryClass, _editableFields, _accessor.getInterp(_interp),
_displayFields);
add(new JScrollPane(_table), BorderLayout.CENTER);
}
/**
* Get the table being used to display the set.
*/
public JTable getTable ()
{
return _table;
}
/**
* Get the currently selected entry.
*/
public DSet.Entry getSelectedEntry ()
{
return (DSet.Entry)_table.getSelectedObject();
}
@Override
public Dimension getPreferredSize ()
{
Dimension d = super.getPreferredSize();
d.height = Math.min(d.height, MIN_HEIGHT);
return d;
}
@Override
public void addNotify ()
{
super.addNotify();
_accessor.added();
_table.addActionListener(this);
}
@Override
public void removeNotify ()
{
_accessor.removed();
_table.removeActionListener(this);
super.removeNotify();
}
/**
* Handles the addition of an entry, assuming our filter allows it.
*/
protected void addEntry (E entry)
{
if (_entryFilter == null || _entryFilter.apply(entry)) {
int index = _keys.insertSorted(getKey(entry));
_table.insertDatum(entry, index);
}
}
/**
* Takes care of removing a key from
*/
protected void removeKey (Comparable<?> key)
{
int index = _keys.indexOf(key);
if (index != -1) {
_keys.remove(index);
_table.removeDatum(index);
}
}
// documentation inherited from interface ActionListener
public void actionPerformed (ActionEvent event)
{
CommandEvent ce = (CommandEvent)event;
_accessor.updateEntry(_setName, (DSet.Entry)ce.getArgument());
}
public void setData (ComparableArrayList<Comparable<Object>> keys, Object[] data)
{
_keys = keys;
_table.setData(data);
}
public Predicate<E> getFilter ()
{
return _entryFilter;
}
public String getSetName ()
{
return _setName;
}
@SuppressWarnings("unchecked")
protected static Comparable<Object> getKey (DSet.Entry entry)
{
return (Comparable<Object>)entry.getKey();
}
protected class DObjectAccessor<F extends E>
implements AttributeChangeListener, SetListener<F>, Accessor<F>
{
public DObjectAccessor (DObject obj, String setName)
{
_obj = obj;
_setName = setName;
}
public ObjectEditorTable.FieldInterpreter getInterp (
ObjectEditorTable.FieldInterpreter interp)
{
return interp;
}
public void added ()
{
_obj.addListener(this);
refreshSet();
refreshData();
}
public void removed ()
{
_obj.removeListener(this);
}
public void refreshSet ()
{
_set = _obj.getSet(_setName);
}
public void updateEntry (String setName, DSet.Entry entry)
{
_obj.updateSet(setName, entry);
}
// documentation inherited from interface SetListener
public void entryAdded (EntryAddedEvent<F> event)
{
if (event.getName().equals(_setName)) {
addEntry(event.getEntry());
}
}
// documentation inherited from interface SetListener
public void entryRemoved (EntryRemovedEvent<F> event)
{
if (event.getName().equals(_setName)) {
removeKey(event.getKey());
}
}
protected void refreshData ()
{
ComparableArrayList<Comparable<Object>> keys =
new ComparableArrayList<Comparable<Object>>();
E[] entries;
if (_entryFilter == null) {
entries = createArray();
} else {
// Do some shuffling to get out a filtered array.
Iterator<F> itr = Iterators.filter(iterator(), _entryFilter);
ArrayList<F> list = Lists.newArrayList();
Iterators.addAll(list, itr);
@SuppressWarnings("unchecked") F[] tmp = (F[])new DSet.Entry[list.size()];
entries = tmp;
list.toArray(entries);
}
for (E entry : entries) {
keys.insertSorted(getKey(entry));
}
setData(keys, entries); // this works because DSet itself is sorted
}
// documentation inherited from interface SetListener
public void entryUpdated (EntryUpdatedEvent<F> event)
{
if (event.getName().equals(_setName)) {
E entry = event.getEntry();
int index = _keys.indexOf(entry.getKey());
if (index != -1) {
// We have it, so either update or remove
if (_entryFilter == null || _entryFilter.apply(entry)) {
_table.updateDatum(entry, index);
} else {
removeKey(entry.getKey());
}
} else {
// We DON'T have it, so try to add it in case we care about it
addEntry(entry);
}
}
}
// documentation inherited from interface SetListener
public void attributeChanged (AttributeChangedEvent event)
{
if (event.getName().equals(_setName)) {
// the whole set changed so we need to refetch it from the object
refreshSet();
refreshData();
}
}
public E[] createArray ()
{
@SuppressWarnings("unchecked") F[] tmp = (F[])new DSet.Entry[_set.size()];
F[] entries = tmp;
_set.toArray(entries);
return entries;
}
public Iterator<F> iterator ()
{
return _set.iterator();
}
protected DObject _obj;
protected DSet<F> _set;
protected String _setName;
}
/** The name of the set in that object. */
protected String _setName;
/** An optional predicate to decide whether actually care about displaying a given entry. */
protected Predicate<E> _entryFilter;
/** Provides access to our data we're editing. */
protected Accessor<E> _accessor;
/** The table used to edit. */
protected ObjectEditorTable _table;
/** An array we use to track our entries' positions by key. */
protected ComparableArrayList<Comparable<Object>> _keys;
protected Class<?> _entryClass;
protected String[] _editableFields;
protected ObjectEditorTable.FieldInterpreter _interp;
protected String[] _displayFields;
/** The minimum height for our editor UI. */
protected static final int MIN_HEIGHT = 200;
}
@@ -0,0 +1,237 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.client;
import java.lang.reflect.Field;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;
import com.google.common.base.Objects;
import com.samskivert.swing.HGroupLayout;
import com.threerings.presents.dobj.AttributeChangeListener;
import com.threerings.presents.dobj.AttributeChangedEvent;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.ObjectAccessException;
import com.threerings.presents.util.PresentsContext;
import static com.threerings.admin.Log.log;
/**
* Used to display and edit a particular distributed object field.
*/
public abstract class FieldEditor extends JPanel
implements AttributeChangeListener, ActionListener, FocusListener
{
/** The interface defining how the editor interacts with its data. */
public interface Accessor
{
void added ();
void removed ();
void set (Field field, Object value);
Object get (Field field);
}
public FieldEditor (PresentsContext ctx, Field field, DObject object)
{
_ctx = ctx;
_field = field;
setAccessor(new DObjectAccessor(object));
// create our interface elements
setLayout(new HGroupLayout(HGroupLayout.STRETCH));
// a label to display the field name
add(_label = new JLabel(_field.getName()));
// set up our default border
updateBorder(false);
}
/**
* Sets the plugin for how we interact with our data.
*/
public void setAccessor (Accessor accessor)
{
_accessor = accessor;
}
@Override
public void addNotify ()
{
super.addNotify();
// listen to the object while we're visible
_accessor.added();
displayValue(getValue());
}
@Override
public void removeNotify ()
{
super.removeNotify();
// stop listening when we're hidden
_accessor.removed();
}
// documentation inherited from interface
public void attributeChanged (AttributeChangedEvent event)
{
noteUpdatedExternally();
}
/** Update ourselves to reflect a change from outside the editor. */
public void noteUpdatedExternally ()
{
displayValue(getValue());
updateBorder(false);
}
// documentation inherited from interface
public void actionPerformed (ActionEvent event)
{
Object value = null;
try {
value = getDisplayValue();
} catch (Exception e) {
updateBorder(true);
return;
}
// submit an attribute changed event with the new value
if (!Objects.equal(value, getValue())) {
_accessor.set(_field, value);
}
}
// documentation inherited from interface
public void focusGained (FocusEvent event)
{
// nothing doing
}
// documentation inherited from interface
public void focusLost (FocusEvent event)
{
// make sure the value is not changed from the value in the
// object; if it is, set a modified border
Object dvalue = null;
try {
dvalue = getDisplayValue();
} catch (Exception e) {
log.warning("Failed to parse display value " + e + ".");
displayValue(getValue());
}
updateBorder(!Objects.equal(dvalue, getValue()));
}
/**
* Returns the currently displayed value.
*/
protected abstract Object getDisplayValue ()
throws Exception;
/**
* Reads the value from the distributed object field and updates the
* display with it.
*/
protected abstract void displayValue (Object value);
/**
* Returns the current object value.
*/
protected Object getValue ()
{
return _accessor.get(_field);
}
/**
* Sets the appropriate border on this field editor based on whether
* or not the field is modified.
*/
protected void updateBorder (boolean modified)
{
if (modified) {
setBorder(BorderFactory.createMatteBorder(2, 2, 2, 2, Color.red));
} else {
setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
}
}
/**
* A simple accessor that knows how to interact with a DObject - this is normally what is used.
*/
protected class DObjectAccessor
implements Accessor
{
public DObjectAccessor (DObject obj)
{
_obj = obj;
}
public void added ()
{
_obj.addListener(FieldEditor.this);
}
public void removed ()
{
_obj.removeListener(FieldEditor.this);
}
public void set (Field field, Object value)
{
try {
_obj.changeAttribute(field.getName(), value);
} catch (ObjectAccessException oae) {
log.warning("Failed to update field " + field.getName() + ": " + oae);
}
}
public Object get (Field field)
{
try {
return field.get(_obj);
} catch (Exception e) {
log.warning("Failed to fetch field", "field", field, "object", _obj, "error", e);
return null;
}
}
protected DObject _obj;
}
protected PresentsContext _ctx;
protected Field _field;
protected Accessor _accessor;
protected JLabel _label;
}
@@ -0,0 +1,131 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.client;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import javax.swing.BorderFactory;
import com.samskivert.swing.ScrollablePanel;
import com.samskivert.swing.VGroupLayout;
import com.samskivert.swing.util.SwingUtil;
import com.threerings.presents.dobj.ObjectAccessException;
import com.threerings.presents.dobj.Subscriber;
import com.threerings.presents.util.PresentsContext;
import com.threerings.presents.util.SafeSubscriber;
import com.threerings.admin.data.ConfigObject;
import static com.threerings.admin.Log.log;
/**
* Used to edit the distributed object fields of a particular
* configuration object. When the panel is first shown, it will subscribe
* to the object and display its fields. It will not automatically
* unsubscribe when it is hidden, but rather {@link #cleanup} must be
* called to let it know that it's not going to be shown again soon and it
* is safe for it to clear out its subscription.
*
* @see ConfigEditorPanel
*/
public class ObjectEditorPanel extends ScrollablePanel
implements Subscriber<ConfigObject>
{
/**
* Creates an object editor panel for the specified configuration
* object.
*/
public ObjectEditorPanel (PresentsContext ctx, String key, int oid)
{
super(new VGroupLayout(VGroupLayout.NONE, VGroupLayout.STRETCH,
VGroupLayout.DEFAULT_GAP, VGroupLayout.TOP));
setBorder(BorderFactory.createEmptyBorder(BORDER, BORDER, BORDER, BORDER));
// keep this business around
_ctx = ctx;
_key = key;
// we'll use this to safely subscribe to and unsubscribe from the
// config object
_safesub = new SafeSubscriber<ConfigObject>(oid, this);
_safesub.subscribe(_ctx.getDObjectManager());
}
@Override
public boolean getScrollableTracksViewportWidth ()
{
return true;
}
/**
* This method must be called to let the object editor panel know that
* it's OK for it to remove its subscription to its config object.
*/
public void cleanup ()
{
// clear out our subscription
_safesub.unsubscribe(_ctx.getDObjectManager());
_object = null;
// clear out our field editors
removeAll();
}
// documentation inherited from interface
public void objectAvailable (ConfigObject object)
{
// keep this for later
_object = object;
// create our field editors
try {
Field[] fields = object.getClass().getFields();
for (Field field : fields) {
// if the field is anything but a plain old public field,
// we don't want to edit it
if (field.getModifiers() == Modifier.PUBLIC) {
add(_object.getEditor(_ctx, field));
}
}
} catch (SecurityException se) {
log.warning("Unable to introspect DObject!? " + se);
}
SwingUtil.refresh(this);
}
// documentation inherited from interface
public void requestFailed (int oid, ObjectAccessException cause)
{
log.warning("Unable to subscribe to config object: " + cause);
}
protected PresentsContext _ctx;
protected String _key;
protected SafeSubscriber<ConfigObject> _safesub;
protected ConfigObject _object;
protected static final int BORDER = 5;
}
@@ -0,0 +1,141 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.client;
import java.lang.reflect.Field;
import javax.swing.JComboBox;
import com.google.common.base.Objects;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.util.PresentsContext;
import static com.threerings.admin.Log.log;
/**
* Allows editing of a dobj field using a pulldown.
*/
public class PulldownFieldEditor extends FieldEditor
{
/**
* An object that nicely represents a pulldown choice.
*/
public static class Choice
{
/** The value we represent. */
public Object value;
/**
* Construct a pulldown choice.
*/
public Choice (String displayName, Object val)
{
if (displayName == null) {
throw new NullPointerException("displayName cannot be null.");
}
_name = displayName;
value = val;
}
@Override
public String toString ()
{
return _name;
}
/** The string representation of this choice. */
protected String _name;
}
/**
* Construct a PulldownFieldEditor.
*/
public PulldownFieldEditor (PresentsContext ctx, Field field, DObject obj)
{
super(ctx, field, obj);
add(_value = new JComboBox());
}
/**
* Add a PulldownChoice object as a choice for the pulldown.
*/
public void addChoice (Choice choice)
{
_value.addItem(choice);
}
/**
* Add the specified object as a choice. The name will be the
* toString() of the object.
*/
public void addChoice (Object choice)
{
String name = (choice == null) ? "null" : choice.toString();
addChoice(new Choice(name, choice));
}
@Override
public void addNotify ()
{
super.addNotify();
_value.addActionListener(this);
}
@Override
public void removeNotify ()
{
_value.removeActionListener(this);
super.removeNotify();
}
@Override
protected Object getDisplayValue ()
throws Exception
{
Object obj = _value.getSelectedItem();
if (obj == null) {
return null;
}
return ((Choice)obj).value;
}
@Override
protected void displayValue (Object value)
{
for (int ii = _value.getItemCount() - 1; ii >= 0; ii--) {
Choice choice = (Choice)_value.getItemAt(ii);
if (Objects.equal(value, choice.value)) {
_value.setSelectedIndex(ii);
return;
}
}
// cause shit to blow up minorly
log.warning("Value in dobj is not settable, disabling choice.", new Exception());
_value.setEnabled(false);
}
/** Holds the value we're editing. */
protected JComboBox _value;
}
@@ -0,0 +1,288 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.client;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Set;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import com.google.common.base.Functions;
import com.google.common.base.Predicate;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.samskivert.util.Logger;
import com.samskivert.util.QuickSort;
import com.samskivert.util.StringUtil;
import com.samskivert.swing.ObjectEditorTable;
import com.threerings.presents.dobj.AttributeChangeListener;
import com.threerings.presents.dobj.AttributeChangedEvent;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.DSet;
import com.threerings.presents.dobj.EntryAddedEvent;
import com.threerings.presents.dobj.EntryRemovedEvent;
import com.threerings.presents.dobj.EntryUpdatedEvent;
import com.threerings.presents.dobj.SetListener;
/**
* Allows simple editing of DSets within a distributed object and easily groups entries into tabs
* based on the content of some field.
*/
public class TabbedDSetEditor<E extends DSet.Entry> extends JPanel
implements AttributeChangeListener, SetListener<E>
{
/**
* Defines how DSetEditor data-access plugins should be created.
*/
public interface AccessorFactory
{
public <E extends DSet.Entry> DSetEditor.Accessor<E> createAccessor (DSetEditor<E> editor);
}
/**
* Used to divide various entires into different groups.
*/
public static abstract class EntryGrouper<E extends DSet.Entry>
{
/**
* Subclasses implement the actual logic to figure out a group names from an entry here.
*/
protected abstract String[] computeGroups (E entry);
/**
* Returns a predicate that returns true if the given entry is in the given group.
*/
protected Predicate<E> getPredicate (final String group) {
return new Predicate<E>() {
public boolean apply (E entry) {
String[] groups = computeGroups(entry);
for (String g: groups) {
if (g.equals(group)) {
return true;
}
}
return false;
}
};
}
/**
* Grinds through the
* @param entries
*/
public void computeGroups (Iterable<E> entries) {
for (E entry : entries) {
String[] groups = computeGroups(entry);
for (String group : groups) {
_allGroups.add(group);
}
}
}
/**
* Returns all the groups we know about, ordered as they should be displayed.
*/
public String[] getAllGroups () {
String[] list = _allGroups.toArray(new String[_allGroups.size()]);
QuickSort.sort(list, getComparator());
return list;
}
protected Comparator<Object> getComparator () {
return LEXICAL_CASE_INSENSITIVE;
}
protected Set<String> _allGroups = Sets.newHashSet();
protected static final Comparator<Object> LEXICAL_CASE_INSENSITIVE = Ordering
.from(String.CASE_INSENSITIVE_ORDER)
.onResultOf(Functions.toStringFunction())
.nullsLast();
}
public static class FieldGrouper<E extends DSet.Entry> extends EntryGrouper<E>
{
public FieldGrouper (String fieldName, Class<?> entryClass) {
try {
_field = entryClass.getField(fieldName);
} catch (NoSuchFieldException nsfe) {
throw new IllegalArgumentException(Logger.format(
"Group field not found in prototype class!",
"proto", entryClass, "groupField", fieldName));
}
}
@Override
protected String[] computeGroups (E entry) {
try {
return new String[] { StringUtil.toString(_field.get(entry)) };
} catch (IllegalAccessException iae) {
// This ain't good, but let's soldier on.
return new String[] { "<bogus>" };
}
}
protected final Field _field;
}
/**
* Convenience function to make an edittor that groups based on the values of a given field.
*/
public TabbedDSetEditor (
DObject setter, String setName, Class<?> entryClass, String[] editableFields,
ObjectEditorTable.FieldInterpreter interp, String groupField)
{
this(setter, setName, entryClass, editableFields, interp,
new FieldGrouper<E>(groupField, entryClass));
}
/**
* A set of tabs containing DSetEditors grouping entries by the String value stored in
* a given field of the Entry.
*/
public TabbedDSetEditor (
DObject setter, String setName, Class<?> entryClass, String[] editableFields,
ObjectEditorTable.FieldInterpreter interp, EntryGrouper<E> grouper)
{
// Stash all this for later
_setter = setter;
_setName = setName;
_entryClass = entryClass;
_editableFields = editableFields;
_interp = interp;
_grouper = grouper;
_tabs = new JTabbedPane();
add(_tabs);
}
/**
* Assigns the factory that creates data-access plugins for our set our DSetEditors.
*/
public void setAccessorFactory (AccessorFactory accessorFactory)
{
_accessorFactory = accessorFactory;
}
protected void computeTabs ()
{
_grouper.computeGroups(_setter.<E>getSet(_setName));
String[] groups = _grouper.getAllGroups();
for (String group : groups) {
if (!_editors.containsKey(group)) {
DSetEditor<E> editor = createEditor(
_setter, _setName, _entryClass, _editableFields, _interp, _grouper, group);
if (_accessorFactory != null) {
editor.setAccessor(_accessorFactory.createAccessor(editor));
}
_tabs.add(group, editor);
_editors.put(group, editor);
}
}
// TODO: Prune any now-empty tabs
}
/**
* Creates a DSetEditor for displaying the given group.
*/
protected DSetEditor<E> createEditor (
DObject setter, String setName, Class<?> entryClass, String[] editableFields,
ObjectEditorTable.FieldInterpreter interp, EntryGrouper<E> grouper, String group)
{
return new DSetEditor<E>(setter, setName, entryClass, editableFields,
interp, getDisplayFields(group), grouper.getPredicate(group));
}
/**
* Choose which fields to display for the given group.
*/
protected String[] getDisplayFields (String group)
{
return null; // Override to display only a subset
}
@Override
public void addNotify ()
{
super.addNotify();
_setter.addListener(this);
// populate our tabs
computeTabs();
}
@Override
public void removeNotify ()
{
_setter.removeListener(this);
super.removeNotify();
}
public void attributeChanged (AttributeChangedEvent event)
{
if (event.getName().equals(_setName)) {
computeTabs();
}
}
public void entryAdded (EntryAddedEvent<E> event)
{
if (event.getName().equals(_setName)) {
computeTabs();
}
}
public void entryRemoved (EntryRemovedEvent<E> event)
{
if (event.getName().equals(_setName)) {
computeTabs();
}
}
public void entryUpdated (EntryUpdatedEvent<E> event)
{
if (event.getName().equals(_setName)) {
computeTabs();
}
}
protected final DObject _setter;
protected final String _setName;
protected final Class<?> _entryClass;
protected final String[] _editableFields;
protected final ObjectEditorTable.FieldInterpreter _interp;
protected final EntryGrouper<E> _grouper;
protected AccessorFactory _accessorFactory;
protected JTabbedPane _tabs;
protected HashMap<String, DSetEditor<E>> _editors = Maps.newHashMap();
}
@@ -0,0 +1,33 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.data;
import com.threerings.presents.data.InvocationCodes;
/**
* Codes and consants relating to the admin services.
*/
public interface AdminCodes extends InvocationCodes
{
/** Defines our invocation service group. */
public static final String ADMIN_GROUP = "presents.admin";
}
@@ -0,0 +1,90 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.data;
import javax.annotation.Generated;
import com.threerings.admin.client.AdminService;
import com.threerings.presents.client.Client;
import com.threerings.presents.data.InvocationMarshaller;
import com.threerings.presents.dobj.InvocationResponseEvent;
/**
* Provides the implementation of the {@link AdminService} interface
* that marshalls the arguments and delivers the request to the provider
* on the server. Also provides an implementation of the response listener
* interfaces that marshall the response arguments and deliver them back
* to the requesting client.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from AdminService.java.")
public class AdminMarshaller extends InvocationMarshaller
implements AdminService
{
/**
* Marshalls results to implementations of {@link AdminService.ConfigInfoListener}.
*/
public static class ConfigInfoMarshaller extends ListenerMarshaller
implements ConfigInfoListener
{
/** The method id used to dispatch {@link #gotConfigInfo}
* responses. */
public static final int GOT_CONFIG_INFO = 1;
// from interface ConfigInfoMarshaller
public void gotConfigInfo (String[] arg1, int[] arg2)
{
_invId = null;
omgr.postEvent(new InvocationResponseEvent(
callerOid, requestId, GOT_CONFIG_INFO,
new Object[] { arg1, arg2 }, transport));
}
@Override // from InvocationMarshaller
public void dispatchResponse (int methodId, Object[] args)
{
switch (methodId) {
case GOT_CONFIG_INFO:
((ConfigInfoListener)listener).gotConfigInfo(
(String[])args[0], (int[])args[1]);
return;
default:
super.dispatchResponse(methodId, args);
return;
}
}
}
/** The method id used to dispatch {@link #getConfigInfo} requests. */
public static final int GET_CONFIG_INFO = 1;
// from interface AdminService
public void getConfigInfo (Client arg1, AdminService.ConfigInfoListener arg2)
{
AdminMarshaller.ConfigInfoMarshaller listener2 = new AdminMarshaller.ConfigInfoMarshaller();
listener2.listener = arg2;
sendRequest(arg1, GET_CONFIG_INFO, new Object[] {
listener2
});
}
}
@@ -0,0 +1,50 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.data;
import java.lang.reflect.Field;
import javax.swing.JPanel;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.util.PresentsContext;
import com.threerings.admin.client.AsStringFieldEditor;
import com.threerings.admin.client.BooleanFieldEditor;
/**
* Base class for runtime config distributed objects. Used to allow
* config objects to supply custom object editing UI.
*/
public class ConfigObject extends DObject
{
/**
* Returns the editor panel for the specified field.
*/
public JPanel getEditor (PresentsContext ctx, Field field)
{
if (field.getType().equals(Boolean.TYPE)) {
return new BooleanFieldEditor(ctx, field, this);
} else {
return new AsStringFieldEditor(ctx, field, this);
}
}
}
@@ -0,0 +1,71 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.server;
import javax.annotation.Generated;
import com.threerings.admin.client.AdminService;
import com.threerings.admin.data.AdminMarshaller;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationDispatcher;
import com.threerings.presents.server.InvocationException;
/**
* Dispatches requests to the {@link AdminProvider}.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from AdminService.java.")
public class AdminDispatcher extends InvocationDispatcher<AdminMarshaller>
{
/**
* Creates a dispatcher that may be registered to dispatch invocation
* service requests for the specified provider.
*/
public AdminDispatcher (AdminProvider provider)
{
this.provider = provider;
}
@Override
public AdminMarshaller createMarshaller ()
{
return new AdminMarshaller();
}
@Override
public void dispatchRequest (
ClientObject source, int methodId, Object[] args)
throws InvocationException
{
switch (methodId) {
case AdminMarshaller.GET_CONFIG_INFO:
((AdminProvider)provider).getConfigInfo(
source, (AdminService.ConfigInfoListener)args[0]
);
return;
default:
super.dispatchRequest(source, methodId, args);
return;
}
}
}
@@ -0,0 +1,63 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.server;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationException;
import com.threerings.presents.server.InvocationManager;
import com.threerings.admin.client.AdminService;
import com.threerings.admin.data.AdminCodes;
/**
* Handles admin stuffs.
*/
@Singleton
public class AdminManager
implements AdminProvider
{
@Inject public AdminManager (InvocationManager invmgr)
{
invmgr.registerDispatcher(new AdminDispatcher(this), AdminCodes.ADMIN_GROUP);
}
// from interface AdminProvider
public void getConfigInfo (ClientObject caller, AdminService.ConfigInfoListener listener)
throws InvocationException
{
// we don't have to validate the request because the user can't do anything with the keys
// or oids unless they're an admin (we put the burden of doing that checking on the creator
// of the config object because we would otherwise need some mechanism to determine whether
// a user is an admin and we don't want to force some primitive system on the service user)
String[] keys = _registry.getKeys();
int[] oids = new int[keys.length];
for (int ii = 0; ii < keys.length; ii++) {
oids[ii] = _registry.getObject(keys[ii]).getOid();
}
listener.gotConfigInfo(keys, oids);
}
@Inject protected ConfigRegistry _registry;
}
@@ -0,0 +1,43 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.server;
import javax.annotation.Generated;
import com.threerings.admin.client.AdminService;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationException;
import com.threerings.presents.server.InvocationProvider;
/**
* Defines the server-side of the {@link AdminService}.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from AdminService.java.")
public interface AdminProvider extends InvocationProvider
{
/**
* Handles a {@link AdminService#getConfigInfo} request.
*/
void getConfigInfo (ClientObject caller, AdminService.ConfigInfoListener arg1)
throws InvocationException;
}
@@ -0,0 +1,429 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.server;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import com.google.common.collect.Maps;
import com.samskivert.io.ByteArrayOutInputStream;
import com.samskivert.util.StringUtil;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.Streamable;
import com.threerings.presents.dobj.AccessController;
import com.threerings.presents.dobj.AttributeChangeListener;
import com.threerings.presents.dobj.AttributeChangedEvent;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.DSet;
import com.threerings.presents.dobj.ElementUpdateListener;
import com.threerings.presents.dobj.ElementUpdatedEvent;
import com.threerings.presents.dobj.EntryAddedEvent;
import com.threerings.presents.dobj.EntryRemovedEvent;
import com.threerings.presents.dobj.EntryUpdatedEvent;
import com.threerings.presents.dobj.ObjectAccessException;
import com.threerings.presents.dobj.SetListener;
import static com.threerings.admin.Log.log;
/**
* Provides a registry of configuration distributed objects. Using distributed object to store
* runtime configuration data can be exceptionally useful in that clients (with admin privileges)
* can view and update the running server's configuration parameters on the fly.
*
* <p> Users of the service are responsible for creating their own configuration objects which are
* then registered via this class. The config object registry then performs a few functions:
*
* <ul>
* <li> It populates the config object with values from the persistent configuration information.
* <li> It mirrors object updates out to the persistent configuration repository.
* <li> It makes the set of registered objects available for inspection and modification via the
* admin client interface.
* </ul>
*
* <p> Users of this service will want to use {@link AccessController}s on their configuration
* distributed objects to prevent non-administrators from subscribing to or modifying the objects.
*/
public abstract class ConfigRegistry
{
/**
* Creates a ConfigRegistry that isn't transitioning.
*/
public ConfigRegistry ()
{
this(false);
}
/**
* Creates a ConfigRegistry.
*
* @param transitioning if true, serialized Streamable instances stored in the registry will
* be written back out immediately to allow them to be transitioned to new class names.
*/
public ConfigRegistry (boolean transitioning)
{
_transitioning = transitioning;
}
/**
* Registers the supplied configuration object with the system.
*
* @param key a string that identifies this object. These are generally hierarchical in nature
* (of the form <code>system.subsystem</code>), for example: <code>yohoho.crew</code>.
* @param path The the path in the persistent configuration repository. This may mean
* something to the underlying persistent store, for example in the preferences backed
* implementation it defines the path to the preferences node in the package hierarchy.
* @param object the object to be registered.
*/
public void registerObject (String key, String path, DObject object)
{
ObjectRecord record = createObjectRecord(path, object);
record.init();
_configs.put(key, record);
}
/**
* Returns the config object mapped to the specified key, or null if none exists for that key.
*/
public DObject getObject (String key)
{
ObjectRecord record = _configs.get(key);
return (record == null) ? null : record.object;
}
/**
* Returns an array containing the keys of all registered configuration objects.
*/
public String[] getKeys ()
{
return _configs.keySet().toArray(new String[_configs.size()]);
}
/**
* Creates an object record derivation that will handle the management of the specified object.
*/
protected abstract ObjectRecord createObjectRecord (String path, DObject object);
/**
* Create an ObjectInputStream to read serialized config entries.
*/
protected ObjectInputStream createObjectInputStream (InputStream bin)
{
return new ObjectInputStream(bin);
}
/**
* Create an ObjectOutputStream to write serialized config entries.
*/
protected ObjectOutputStream createObjectOutputStream (OutputStream bin)
{
return new ObjectOutputStream(bin);
}
/**
* Contains all necessary info for a configuration object registration.
*/
protected abstract class ObjectRecord
implements AttributeChangeListener, SetListener<DSet.Entry>, ElementUpdateListener
{
public DObject object;
public ObjectRecord (DObject obj)
{
object = obj;
}
public void init ()
{
// read in the initial configuration settings from the persistent config repository
Class<?> cclass = object.getClass();
try {
Field[] fields = cclass.getFields();
for (Field field : fields) {
int mods = field.getModifiers();
if ((mods & Modifier.STATIC) != 0 || (mods & Modifier.PUBLIC) == 0 ||
(mods & Modifier.TRANSIENT) != 0) {
continue;
}
initField(field);
}
// listen for attribute updates
object.addListener(this);
} catch (SecurityException se) {
log.warning("Unable to reflect on " + cclass.getName() + ": " + se + ". " +
"Refusing to monitor object.");
}
}
// from SetListener
public void entryAdded (EntryAddedEvent<DSet.Entry> event)
{
serializeAttribute(event.getName());
}
// from SetListener
public void entryUpdated (EntryUpdatedEvent<DSet.Entry> event)
{
serializeAttribute(event.getName());
}
// from SetListener
public void entryRemoved (EntryRemovedEvent<DSet.Entry> event)
{
serializeAttribute(event.getName());
}
// from ElementUpdateListener
public void elementUpdated (ElementUpdatedEvent event)
{
Object value;
try {
value = object.getAttribute(event.getName());
} catch (ObjectAccessException oae) {
log.warning("Exception getting field", "name", event.getName(), "exception", oae);
return;
}
updateValue(event.getName(), value);
}
// from AttributeChangeListener
public void attributeChanged (AttributeChangedEvent event)
{
// mirror this configuration update to the persistent config
Object value = event.getValue();
if (value instanceof DSet<?>) {
serializeAttribute(event.getName());
} else {
updateValue(event.getName(), value);
}
}
protected void updateValue (String name, Object value)
{
String key = nameToKey(name);
if (value instanceof Boolean) {
setValue(key, ((Boolean)value).booleanValue());
} else if (value instanceof Byte) {
setValue(key, ((Byte)value).byteValue());
} else if (value instanceof Short) {
setValue(key, ((Short)value).shortValue());
} else if (value instanceof Integer) {
setValue(key, ((Integer)value).intValue());
} else if (value instanceof Long) {
setValue(key, ((Long)value).longValue());
} else if (value instanceof Float) {
setValue(key, ((Float)value).floatValue());
} else if (value instanceof String) {
setValue(key, (String)value);
} else if (value instanceof float[]) {
setValue(key, (float[])value);
} else if (value instanceof int[]) {
setValue(key, (int[])value);
} else if (value instanceof String[]) {
setValue(key, (String[])value);
} else if (value instanceof long[]) {
setValue(key, (long[])value);
} else {
log.info("Unable to flush config obj change", "cobj", object.getClass().getName(),
"key", key, "type", value.getClass().getName(), "value", value);
}
}
/**
* Initializes a single field of a config distributed object from its corresponding value
* in the associated config repository.
*/
protected void initField (Field field)
{
String key = nameToKey(field.getName());
Class<?> type = field.getType();
try {
if (type.equals(Boolean.TYPE)) {
boolean defval = field.getBoolean(object);
field.setBoolean(object, getValue(key, defval));
} else if (type.equals(Byte.TYPE)) {
byte defval = field.getByte(object);
field.setByte(object, getValue(key, defval));
} else if (type.equals(Short.TYPE)) {
short defval = field.getShort(object);
field.setShort(object, getValue(key, defval));
} else if (type.equals(Integer.TYPE)) {
int defval = field.getInt(object);
field.setInt(object, getValue(key, defval));
} else if (type.equals(Long.TYPE)) {
long defval = field.getLong(object);
field.setLong(object, getValue(key, defval));
} else if (type.equals(Float.TYPE)) {
float defval = field.getFloat(object);
field.setFloat(object, getValue(key, defval));
} else if (type.equals(String.class)) {
String defval = (String)field.get(object);
field.set(object, getValue(key, defval));
} else if (type.equals(int[].class)) {
int[] defval = (int[])field.get(object);
field.set(object, getValue(key, defval));
} else if (type.equals(float[].class)) {
float[] defval = (float[])field.get(object);
field.set(object, getValue(key, defval));
} else if (type.equals(String[].class)) {
String[] defval = (String[])field.get(object);
field.set(object, getValue(key, defval));
} else if (type.equals(long[].class)) {
long[] defval = (long[])field.get(object);
field.set(object, getValue(key, defval));
} else if (Streamable.class.isAssignableFrom(type)) {
// don't freak out if the conf is blank.
String value = getValue(key, "");
if (StringUtil.isBlank(value)) {
return;
}
try {
ByteArrayInputStream bin =
new ByteArrayInputStream(StringUtil.unhexlate(value));
ObjectInputStream oin = createObjectInputStream(bin);
Object deserializedValue = oin.readObject();
field.set(object, deserializedValue);
if (_transitioning) {
// Use serialize rather than serializeAttribute so we don't get
// ObjectAccessExceptions
serialize(key, nameToKey(key), deserializedValue);
}
} catch (Exception e) {
log.warning("Failure decoding config value", "type", type, "field", field,
"exception", e);
}
} else {
log.warning("Can't init field of unknown type",
"cobj", object.getClass().getName(), "key", key,
"type", type.getName());
}
} catch (IllegalAccessException iae) {
log.warning("Can't set field", "cobj", object.getClass().getName(), "key", key,
"error", iae);
}
}
/**
* Get the specified attribute from the configuration object, and serialize it.
*/
protected void serializeAttribute (String attributeName)
{
String key = nameToKey(attributeName);
Object value;
try {
value = object.getAttribute(attributeName);
} catch (ObjectAccessException oae) {
log.warning("Exception getting field", "name", attributeName, "error", oae);
return;
}
if (value instanceof Streamable) {
serialize(attributeName, key, value);
} else {
log.info("Unable to flush config obj change", "cobj", object.getClass().getName(),
"key", key, "type", value.getClass().getName(), "value", value);
}
}
/**
* Save the specified object as serialized data associated with the specified key.
*/
protected void serialize (String name, String key, Object value)
{
ByteArrayOutInputStream out = new ByteArrayOutInputStream();
ObjectOutputStream oout = createObjectOutputStream(out);
try {
oout.writeObject(value);
oout.flush();
setValue(key, StringUtil.hexlate(out.toByteArray()));
} catch (IOException ioe) {
log.info("Error serializing value " + value);
}
}
/**
* Converts a config object field name (someConfigMember) to a configuration key
* (some_config_member).
*/
protected String nameToKey (String attributeName)
{
return StringUtil.unStudlyName(attributeName).toLowerCase();
}
protected abstract boolean getValue (String field, boolean defval);
protected abstract byte getValue (String field, byte defval);
protected abstract short getValue (String field, short defval);
protected abstract int getValue (String field, int defval);
protected abstract long getValue (String field, long defval);
protected abstract float getValue (String field, float defval);
protected abstract String getValue (String field, String defval);
protected abstract int[] getValue (String field, int[] defval);
protected abstract float[] getValue (String field, float[] defval);
protected abstract long[] getValue (String field, long[] defval);
protected abstract String[] getValue (String field, String[] defval);
protected abstract void setValue (String field, boolean value);
protected abstract void setValue (String field, byte value);
protected abstract void setValue (String field, short value);
protected abstract void setValue (String field, int value);
protected abstract void setValue (String field, long value);
protected abstract void setValue (String field, float value);
protected abstract void setValue (String field, String value);
protected abstract void setValue (String field, int[] value);
protected abstract void setValue (String field, float[] value);
protected abstract void setValue (String field, long[] value);
protected abstract void setValue (String field, String[] value);
}
/** A mapping from identifying key to config object. */
protected HashMap<String, ObjectRecord> _configs = Maps.newHashMap();
/** If we need to transition serialized Streamables to a new class format in init.. */
protected boolean _transitioning;
}
@@ -0,0 +1,352 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.server;
import java.util.HashMap;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.util.Invoker;
import com.samskivert.util.StringUtil;
import com.samskivert.jdbc.WriteOnlyUnit;
import com.samskivert.depot.DatabaseException;
import com.samskivert.depot.PersistenceContext;
import com.threerings.presents.annotation.MainInvoker;
import com.threerings.presents.dobj.DObject;
import com.threerings.admin.server.persist.ConfigRepository;
import static com.threerings.admin.Log.log;
/**
* Implements the {@link ConfigRegistry} using a JDBC database as a persistent store for the
* configuration information. <em>Note:</em> config objects should only be created during server
* startup because they will result in synchronous requests to load up the initial configuration
* data from the database. This ensures that systems initialized after the config registry can
* safely make use of configuration information.
*/
@Singleton
public class DatabaseConfigRegistry extends ConfigRegistry
{
/**
* Creates a configuration registry and prepares it for operation.
*
* @param ctx will provide access to our database.
* @param invoker this will be used to perform all database activity (except first time
* initialization) so as to avoid blocking the distributed object thread.
*/
@Inject public DatabaseConfigRegistry (PersistenceContext ctx, @MainInvoker Invoker invoker)
{
this(ctx, invoker, false);
}
/**
* Creates a configuration registry and prepares it for operation.
*
* @param ctx will provide access to our database.
* @param invoker this will be used to perform all database activity (except first time
* initialization) so as to avoid blocking the distributed object thread.
* @param transitioning if the values in the database need to be transitioned to a new format
*/
public DatabaseConfigRegistry (PersistenceContext ctx, Invoker invoker, boolean transitioning)
{
this(ctx, invoker, "", transitioning);
}
/**
* Creates a configuration registry and prepares it for operation.
*
* @param ctx will provide access to our database.
* @param invoker this will be used to perform all database activity (except first time
* initialization) so as to avoid blocking the distributed object thread.
* @param node if this config registry is accessed by multiple servers which wish to maintain
* separate configs, then specify a node for each server
*/
public DatabaseConfigRegistry (PersistenceContext ctx, Invoker invoker, String node)
{
this(ctx, invoker, node, false);
}
/**
* Creates a configuration registry and prepares it for operation.
*
* @param ctx will provide access to our database.
* @param invoker this will be used to perform all database activity (except first time
* initialization) so as to avoid blocking the distributed object thread.
* @param node if this config registry is accessed by multiple servers which wish to maintain
* separate configs, then specify a node for each server
* @param transitioning if the values in the database need to be transitioned to a new format
*/
public DatabaseConfigRegistry (PersistenceContext ctx, Invoker invoker, String node,
boolean transitioning)
{
super(transitioning);
_repo = new ConfigRepository(ctx);
_invoker = invoker;
_node = StringUtil.isBlank(node) ? "" : node;
}
@Override // from ConfigRegistry
protected ObjectRecord createObjectRecord (String path, DObject object)
{
return new DatabaseObjectRecord(path, object);
}
/** Stores settings in a database. */
protected class DatabaseObjectRecord extends ObjectRecord
{
public DatabaseObjectRecord (String path, DObject object)
{
super(object);
_path = path;
}
@Override
public void init ()
{
// load up our persistent data synchronously because we should be in the middle of
// server startup when it's OK to do database access on the main thread and we need to
// be completely initialized when we return from this call so that subsequent systems
// can predictably make use of the configuration information that we load
try {
_data = _repo.loadConfig(_node, _path);
} catch (DatabaseException pe) {
log.warning("Failed to load object configuration", "path", _path, pe);
_data = Maps.newHashMap();
}
super.init();
}
@Override
protected boolean getValue (String field, boolean defval) {
String value = _data.get(field);
if (value != null) {
return "true".equalsIgnoreCase(value);
}
return defval;
}
@Override
protected byte getValue (String field, byte defval) {
String value = _data.get(field);
try {
if (value != null) {
return Byte.parseByte(value);
}
} catch (Exception e) {
// ignore bogus values and return the default
}
return defval;
}
@Override
protected short getValue (String field, short defval) {
String value = _data.get(field);
try {
if (value != null) {
return Short.parseShort(value);
}
} catch (Exception e) {
// ignore bogus values and return the default
}
return defval;
}
@Override
protected int getValue (String field, int defval) {
String value = _data.get(field);
try {
if (value != null) {
return Integer.parseInt(value);
}
} catch (Exception e) {
// ignore bogus values and return the default
}
return defval;
}
@Override
protected long getValue (String field, long defval) {
String value = _data.get(field);
try {
if (value != null) {
return Long.parseLong(value);
}
} catch (Exception e) {
// ignore bogus values and return the default
}
return defval;
}
@Override
protected float getValue (String field, float defval) {
String value = _data.get(field);
try {
if (value != null) {
return Float.parseFloat(value);
}
} catch (Exception e) {
// ignore bogus values and return the default
}
return defval;
}
@Override
protected String getValue (String field, String defval) {
String value = _data.get(field);
return (value == null) ? defval : value;
}
@Override
protected int[] getValue (String field, int[] defval) {
String value = _data.get(field);
try {
if (value != null) {
int[] avalue = StringUtil.parseIntArray(value);
if (avalue != null) {
return avalue;
}
}
} catch (Exception e) {
// ignore bogus values and return the default
}
return defval;
}
@Override
protected float[] getValue (String field, float[] defval) {
String value = _data.get(field);
try {
if (value != null) {
float[] avalue = StringUtil.parseFloatArray(value);
if (avalue != null) {
return avalue;
}
}
} catch (Exception e) {
// ignore bogus values and return the default
}
return defval;
}
@Override
protected long[] getValue (String field, long[] defval) {
String value = _data.get(field);
try {
if (value != null) {
long[] avalue = StringUtil.parseLongArray(value);
if (avalue != null) {
return avalue;
}
}
} catch (Exception e) {
// ignore bogus values and return the default
}
return defval;
}
@Override
protected String[] getValue (String field, String[] defval) {
String value = _data.get(field);
try {
if (value != null) {
return StringUtil.parseStringArray(value);
}
} catch (Exception e) {
// ignore bogus values and return the default
}
return defval;
}
@Override
protected void setValue (String field, boolean value) {
setAndFlush(field, String.valueOf(value));
}
@Override
protected void setValue (String field, byte value) {
setAndFlush(field, String.valueOf(value));
}
@Override
protected void setValue (String field, short value) {
setAndFlush(field, String.valueOf(value));
}
@Override
protected void setValue (String field, int value) {
setAndFlush(field, String.valueOf(value));
}
@Override
protected void setValue (String field, long value) {
setAndFlush(field, String.valueOf(value));
}
@Override
protected void setValue (String field, float value) {
setAndFlush(field, String.valueOf(value));
}
@Override
protected void setValue (String field, String value) {
setAndFlush(field, value);
}
@Override
protected void setValue (String field, int[] value) {
setAndFlush(field, StringUtil.toString(value, "", ""));
}
@Override
protected void setValue (String field, float[] value) {
setAndFlush(field, StringUtil.toString(value, "", ""));
}
@Override
protected void setValue (String field, long[] value) {
setAndFlush(field, StringUtil.toString(value, "", ""));
}
@Override
protected void setValue (String field, String[] value) {
setAndFlush(field, StringUtil.joinEscaped(value));
}
protected void setAndFlush (String field, String value) {
_data.put(field, value);
flush(field, value);
}
protected void flush (final String field, final String value) {
String iname = "updateConfig(" + _path + ", " + field + ", value=" + value + ")";
_invoker.postUnit(new WriteOnlyUnit(iname) {
@Override
public void invokePersist () throws Exception {
_repo.updateConfig(_node, _path, field, value);
}
});
}
protected String _path;
protected HashMap<String, String> _data;
}
protected ConfigRepository _repo;
protected Invoker _invoker;
protected String _node;
}
@@ -0,0 +1,129 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.server;
import java.util.ArrayList;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.util.Invoker;
import com.samskivert.depot.PersistenceContext;
import com.threerings.io.Streamable;
import com.threerings.util.StreamableTuple;
import com.threerings.presents.annotation.MainInvoker;
import com.threerings.presents.dobj.AttributeChangedEvent;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.peer.server.PeerManager;
import static com.threerings.admin.Log.log;
/**
* A database backed config registry that registers with the peer system and synchronizes with its
* peers when configuration fields are changed.
*/
@Singleton
public class PeeredDatabaseConfigRegistry extends DatabaseConfigRegistry
{
@Inject public PeeredDatabaseConfigRegistry (
PersistenceContext ctx, @MainInvoker Invoker invoker, PeerManager peermgr)
{
super(ctx, invoker, "");
_peermgr = peermgr;
}
@Override // from ConfigRegistry
protected ObjectRecord createObjectRecord (String path, DObject object)
{
return new PeerDatabaseObjectRecord(path, object);
}
/** Stores settings in a database and broadcasts changes to peers. */
protected class PeerDatabaseObjectRecord extends DatabaseObjectRecord
implements PeerManager.StaleCacheObserver
{
public PeerDatabaseObjectRecord (String path, DObject object)
{
super(path, object);
_peermgr.addStaleCacheObserver(PEER_CACHE_PREFIX + _path, this);
}
// from interface PeerManager.StaleCacheObserver
public void changedCacheData (Streamable data)
{
@SuppressWarnings("unchecked") StreamableTuple<String, Object> change =
(StreamableTuple<String, Object>)data;
// note that we should ignore the attribute change event we're about to generate
// because it is not a real configuration change but rather a sync
try {
object.changeAttribute(change.left, change.right);
_pendingSyncs.add(change.left);
} catch (Exception e) {
log.warning("Config attribute sync failed " + change + ".", e);
}
}
@Override // from ObjectRecord
public void attributeChanged (AttributeChangedEvent event)
{
// if this was a pending sync event, don't pass it to our parent as it is not a real
// configuration change event
if (!_pendingSyncs.remove(event.getName())) {
super.attributeChanged(event);
}
}
@Override // from ObjectRecord
protected void updateValue (String name, Object value)
{
super.updateValue(name, value);
fieldUpdated(name, value);
}
@Override // from ObjectRecord
protected void serialize (String name, String key, Object value)
{
super.serialize(name, key, value);
fieldUpdated(name, value);
}
protected void fieldUpdated (String field, Object value)
{
// broadcast to the other nodes that this value has changed
_peermgr.broadcastStaleCacheData(
PEER_CACHE_PREFIX + _path, new StreamableTuple<String, Object>(field, value));
}
protected ArrayList<String> _pendingSyncs = Lists.newArrayList();
}
protected PeerManager _peermgr;
/** Prefixed to our cache invalidation notifications. */
protected static final String PEER_CACHE_PREFIX = "PeerConfigCache:";
}
@@ -0,0 +1,145 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.server;
import com.google.inject.Singleton;
import com.samskivert.util.Config;
import com.samskivert.util.PrefsConfig;
import com.threerings.presents.dobj.DObject;
/**
* Implements the {@link ConfigRegistry} using the Java preferences system as a persistent store
* for the configuration information (see {@link Config} for more information on how that works).
*/
@Singleton
public class PrefsConfigRegistry extends ConfigRegistry
{
@Override // from ConfigRegistry
protected ObjectRecord createObjectRecord (String path, DObject object)
{
return new PrefsObjectRecord(path, object);
}
/** Stores preferences using the Java preferences system. */
protected class PrefsObjectRecord extends ObjectRecord
{
public PrefsConfig config;
public PrefsObjectRecord (String path, DObject object)
{
super(object);
this.config = new PrefsConfig(path);
}
@Override
protected boolean getValue (String field, boolean defval) {
return config.getValue(field, defval);
}
@Override
protected byte getValue (String field, byte defval) {
return (byte)config.getValue(field, defval);
}
@Override
protected short getValue (String field, short defval) {
return (short)config.getValue(field, defval);
}
@Override
protected int getValue (String field, int defval) {
return config.getValue(field, defval);
}
@Override
protected long getValue (String field, long defval) {
return config.getValue(field, defval);
}
@Override
protected float getValue (String field, float defval) {
return config.getValue(field, defval);
}
@Override
protected String getValue (String field, String defval) {
return config.getValue(field, defval);
}
@Override
protected int[] getValue (String field, int[] defval) {
return config.getValue(field, defval);
}
@Override
protected float[] getValue (String field, float[] defval) {
return config.getValue(field, defval);
}
@Override
protected long[] getValue (String field, long[] defval) {
return config.getValue(field, defval);
}
@Override
protected String[] getValue (String field, String[] defval) {
return config.getValue(field, defval);
}
@Override
protected void setValue (String field, boolean value) {
config.setValue(field, value);
}
@Override
protected void setValue (String field, byte value) {
config.setValue(field, value);
}
@Override
protected void setValue (String field, short value) {
config.setValue(field, value);
}
@Override
protected void setValue (String field, int value) {
config.setValue(field, value);
}
@Override
protected void setValue (String field, long value) {
config.setValue(field, value);
}
@Override
protected void setValue (String field, float value) {
config.setValue(field, value);
}
@Override
protected void setValue (String field, String value) {
config.setValue(field, value);
}
@Override
protected void setValue (String field, int[] value) {
config.setValue(field, value);
}
@Override
protected void setValue (String field, float[] value) {
config.setValue(field, value);
}
@Override
protected void setValue (String field, long[] value) {
config.setValue(field, value);
}
@Override
protected void setValue (String field, String[] value) {
config.setValue(field, value);
}
}
}
@@ -0,0 +1,38 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.server.persist;
/**
* Contains a single datum of configuration information.
*/
public class ConfigDatum
{
public String node;
public String object;
public String field;
public String value;
@Override
public String toString () {
return node + "." + object + "." + field + "=" + value + "]";
}
}
@@ -0,0 +1,92 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.server.persist;
import com.samskivert.depot.Key;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.annotation.Column;
import com.samskivert.depot.annotation.Entity;
import com.samskivert.depot.annotation.Id;
import com.samskivert.depot.expression.ColumnExp;
/**
* Stores information about a configuration entry in the database.
*/
@Entity(name="CONFIG")
public class ConfigRecord extends PersistentRecord
{
// AUTO-GENERATED: FIELDS START
public static final Class<ConfigRecord> _R = ConfigRecord.class;
public static final ColumnExp NODE = colexp(_R, "node");
public static final ColumnExp OBJECT = colexp(_R, "object");
public static final ColumnExp FIELD = colexp(_R, "field");
public static final ColumnExp VALUE = colexp(_R, "value");
// AUTO-GENERATED: FIELDS END
public static final int SCHEMA_VERSION = 2;
@Id
@Column(name="NODE", length=64)
public String node;
@Id
@Column(name="OBJECT", length=128)
public String object;
@Id
@Column(name="FIELD", length=64)
public String field;
@Column(name="VALUE", length=65535)
public String value;
/**
* An empty constructor for unmarshalling.
*/
public ConfigRecord ()
{
super();
}
public ConfigRecord (String node, String object, String field, String value)
{
super();
this.node = node;
this.object = object;
this.field = field;
this.value = value;
}
// AUTO-GENERATED: METHODS START
/**
* Create and return a primary {@link Key} to identify a {@link ConfigRecord}
* with the supplied key values.
*/
public static Key<ConfigRecord> getKey (String node, String object, String field)
{
return new Key<ConfigRecord>(
ConfigRecord.class,
new ColumnExp[] { NODE, OBJECT, FIELD },
new Comparable[] { node, object, field });
}
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,75 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.server.persist;
import java.util.HashMap;
import java.util.Set;
import com.google.common.collect.Maps;
import com.samskivert.depot.DepotRepository;
import com.samskivert.depot.PersistenceContext;
import com.samskivert.depot.PersistentRecord;
import com.samskivert.depot.clause.Where;
/**
* Stores configuration information in a database table.
*/
public class ConfigRepository extends DepotRepository
{
/**
* Constructs a new config repository with the specified persistence context.
*/
public ConfigRepository (PersistenceContext ctx)
{
super(ctx);
}
/**
* Loads up the configuration data for the specified object.
*
* @return a map containing field/value pairs for all stored configuration data.
*/
public HashMap<String, String> loadConfig (String node, String object)
{
HashMap<String, String> data = Maps.newHashMap();
Where where = new Where(ConfigRecord.OBJECT, object, ConfigRecord.NODE, node);
for (ConfigRecord record : findAll(ConfigRecord.class, where)) {
data.put(record.field, record.value);
}
return data;
}
/**
* Updates the specified configuration datum.
*/
public void updateConfig (String node, String object, String field, String value)
{
store(new ConfigRecord(node, object, field, value));
}
@Override // from DepotRepository
protected void getManagedRecords (Set<Class<? extends PersistentRecord>> classes)
{
classes.add(ConfigRecord.class);
}
}
@@ -0,0 +1,6 @@
<module>
<inherits name="com.threerings.web.Base"/>
<source path="client"/>
<source path="gwt"/>
</module>
@@ -0,0 +1,90 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.web.client;
import java.util.Map;
import java.util.Map.Entry;
import com.google.common.collect.Maps;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.ui.TabPanel;
import com.threerings.admin.web.client.ConfigEditorTab.ConfigAccessor;
import com.threerings.admin.web.gwt.ConfigField;
import com.threerings.admin.web.gwt.ConfigService;
import com.threerings.admin.web.gwt.ConfigService.ConfigurationRecord;
import com.threerings.admin.web.gwt.ConfigService.ConfigurationResult;
import com.threerings.admin.web.gwt.ConfigServiceAsync;
import com.threerings.gwt.util.PopupCallback;
/**
* The main panel of the configuration editor. All service calls are routed through here.
* Subclass this class in your project.
*/
public abstract class ConfigEditorPanel extends TabPanel
implements ConfigAccessor
{
public ConfigEditorPanel ()
{
addStyleName("configEditorPanel");
((ServiceDefTarget)_configsvc).setServiceEntryPoint(getServiceEntryPoint());
_configsvc.getConfiguration(new PopupCallback<ConfigurationResult>() {
public void onSuccess (ConfigurationResult result) {
gotData(result);
}
});
}
public void submitChanges (String key, ConfigField[] modified,
AsyncCallback<ConfigurationRecord> callback)
{
_configsvc.updateConfiguration(key, modified, callback);
}
protected void gotData (ConfigurationResult result)
{
clear();
if (result.records.isEmpty()) {
return;
}
for (Entry<String, ConfigurationRecord> tab : result.records.entrySet()) {
String tabKey = tab.getKey();
ConfigEditorTab widget = new ConfigEditorTab(this, tabKey, tab.getValue());
_tabs.put(tabKey, widget);
add(widget, tabKey);
}
selectTab(0);
}
/** Should return the absolute path of the servlet that implements {@link ConfigService}. */
protected abstract String getServiceEntryPoint();
protected Map<String, ConfigEditorTab> _tabs = Maps.newHashMap();
protected static final ConfigServiceAsync _configsvc = GWT.create(ConfigService.class);
}
@@ -0,0 +1,124 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.web.client;
import java.util.List;
import com.google.common.collect.Lists;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.threerings.admin.web.gwt.ConfigField;
import com.threerings.gwt.ui.InfoPopup;
import com.threerings.gwt.ui.SmartTable;
import com.threerings.admin.web.gwt.ConfigService.ConfigurationRecord;
import com.threerings.gwt.util.ClickCallback;
/**
*
*/
public class ConfigEditorTab extends SmartTable
{
public interface ConfigAccessor
{
void submitChanges (String key, ConfigField[] modified,
AsyncCallback<ConfigurationRecord> callback);
}
public ConfigEditorTab (ConfigAccessor parent, String key, ConfigurationRecord record)
{
super("configEditorTab", 5, 5);
_parent = parent;
_key = key;
_submit = new Button("Submit Changes");
// wire up saving the code on click
new ClickCallback<ConfigurationRecord>(_submit) {
protected boolean callService () {
List<ConfigField> modified = Lists.newArrayList();
for (ConfigFieldEditor editor : _editors) {
ConfigField field = editor.getModifiedField();
if (field != null) {
modified.add(field);
}
}
_parent.submitChanges(
_key, modified.toArray(new ConfigField[modified.size()]), this);
return true;
}
protected boolean gotResult (ConfigurationRecord result) {
new InfoPopup("Updated " + result.updates + " fields.").show();
updateTable(result);
return false;
}
};
cell(1, 1).alignRight().widget(_submit);
updateTable(record);
}
protected void updateTable (ConfigurationRecord record)
{
SmartTable table = new SmartTable(5, 5);
table.setStyleName("configEditorTable");
int row = 0;
for (ConfigField field : record.fields) {
ConfigFieldEditor editor = ConfigFieldEditor.getEditorFor(field, UPDATE_BUTTON);
_editors.add(editor);
table.cell(row, 0).alignRight().widget(editor.getNameWidget());
table.cell(row, 1).alignLeft().widget(editor.getValueWidget());
table.cell(row, 2).alignLeft().widget(editor.getResetWidget());
row ++;
}
cell(0, 0).colSpan(2).widget(table);
UPDATE_BUTTON.execute();
}
protected List<ConfigFieldEditor> _editors = Lists.newArrayList();
protected ConfigAccessor _parent;
protected String _key;
protected Button _submit;
protected Command UPDATE_BUTTON = new Command () {
public void execute () {
// search for any modified field; if found, enable submissions & exit
for (ConfigFieldEditor editor : _editors) {
if (editor.getModifiedField() != null) {
_submit.setEnabled(true);
return;
}
}
_submit.setEnabled(false);
}
};
}
@@ -0,0 +1,207 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.web.client;
import com.google.gwt.user.client.ui.Label;
import com.threerings.admin.web.gwt.ConfigField;
import com.threerings.admin.web.gwt.ConfigField.FieldType;
import com.google.gwt.dom.client.Style;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
/**
* A class responsible for constructing and exporting the widgets involved in manipulating
* one specific configuration field.
*/
public abstract class ConfigFieldEditor
{
public static ConfigFieldEditor getEditorFor (ConfigField field, Command onChange)
{
if (field.type == FieldType.BOOLEAN) {
return new CheckboxFieldEditor(field, onChange);
}
return new StringFieldEditor(field, onChange);
}
/**
* This editor represents values as strings.
*/
protected static class StringFieldEditor extends ConfigFieldEditor
{
public StringFieldEditor (ConfigField field, Command onChange) {
super(field, onChange);
}
@Override
protected Widget buildWidget (ConfigField field) {
_box = new TextBox();
_box.setStyleName("configStringEditor");
_box.setVisibleLength(40);
resetField();
_box.addChangeHandler(new ChangeHandler() {
public void onChange (ChangeEvent changeEvent) {
// if the string fails conversion, just reset to the old value
if (_field.type.toValue(_box.getText().trim()) == null) {
_box.setText(_field.valStr);
}
updateModificationState();
}
});
return _box;
}
@Override
public ConfigField getModifiedField () {
Object newValue = _field.type.toValue(_box.getText().trim());
if (newValue == null) {
return null;
}
String newValStr = newValue.toString();
if ((newValStr.length() == 0 && _field.valStr == null)
|| newValStr.equals(_field.valStr)) {
return null;
}
return new ConfigField(_field.name, _field.type, newValStr);
}
@Override
protected void resetField () {
_box.setText(_field.valStr);
}
protected TextBox _box;
}
/**
* This editor represents boolean values as checkboxes.
*/
protected static class CheckboxFieldEditor extends ConfigFieldEditor
{
public CheckboxFieldEditor (ConfigField field, Command onChange) {
super(field, onChange);
}
@Override
protected Widget buildWidget (ConfigField field) {
_box = new CheckBox();
_box.setStyleName("configCheckBoxEditor");
resetField();
_box.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
public void onValueChange (ValueChangeEvent<Boolean> changeEvent) {
updateModificationState();
}
});
return _box;
}
@Override
public ConfigField getModifiedField () {
String newValStr = Boolean.toString(_box.getValue());
if (newValStr.equals(_field.valStr)) {
return null;
}
return new ConfigField(_field.name, _field.type, newValStr);
}
@Override
protected void resetField () {
_box.setValue(Boolean.valueOf(_field.valStr));
}
protected CheckBox _box;
}
public ConfigFieldEditor (ConfigField field, Command onChange)
{
_field = field;
_onChange = onChange;
_value = buildWidget(field);
_name = new Label(field.name);
_name.setStyleName("fieldName");
_reset = new Label("X");
_reset.setStyleName("resetButton");
_reset.addClickHandler(new ClickHandler() {
public void onClick (ClickEvent event) {
resetField();
updateModificationState();
}
});
_reset.setVisible(false);
}
protected void updateModificationState ()
{
Style style = _value.getElement().getStyle();
if (getModifiedField() != null) {
style.setBackgroundColor("red");
_reset.setVisible(true);
} else {
style.clearBackgroundColor();
_reset.setVisible(false);
}
_onChange.execute();
}
public Widget getNameWidget ()
{
return _name;
}
public Widget getValueWidget ()
{
return _value;
}
public Widget getResetWidget ()
{
return _reset;
}
public abstract ConfigField getModifiedField ();
protected abstract Widget buildWidget (ConfigField field);
protected abstract void resetField ();
protected ConfigField _field;
protected Command _onChange;
protected Label _name, _reset;
protected Widget _value;
}
@@ -0,0 +1,118 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.web.gwt;
import com.google.common.collect.ComparisonChain;
import com.google.gwt.user.client.rpc.IsSerializable;
/**
* A GWT-friendly representation of a configuration tuple, consisting of the name of the entry,
* a type enum, and the toString() of the value.
*/
public class ConfigField
implements IsSerializable, Comparable<ConfigField>
{
public enum FieldType
implements IsSerializable
{
INTEGER,
SHORT,
BYTE,
LONG,
FLOAT,
BOOLEAN,
DOUBLE,
STRING;
/**
* Create a string representation of the given value, which should be of the type
* reflected in this enum.
*/
public String toString (Object value)
{
return (value != null) ? value.toString() : null;
}
/**
* Convert the given string, which should have been created by {@link #toString(Object)},
* back into its raw value form.
*/
public Object toValue (String text)
{
switch(this) {
case INTEGER:
return new Integer(text);
case SHORT:
return new Short(text);
case BYTE:
return new Byte(text);
case LONG:
return new Long(text);
case FLOAT:
return new Float(text);
case DOUBLE:
return new Double(text);
case BOOLEAN:
return new Boolean(text);
case STRING:
return text;
}
return null;
}
}
public String name;
public FieldType type;
public String valStr;
/** Deserialization constructor. */
public ConfigField ()
{
}
/** Construct a new ConfigField with the given values. */
public ConfigField (String name, FieldType type, String valStr)
{
this.name = name;
this.type = type;
this.valStr = valStr;
}
// from Comparable<ConfigField>
public int compareTo (ConfigField o)
{
return ComparisonChain.start().compare(name, o.name).result();
}
@Override // from Object
public boolean equals (Object o)
{
return compareTo((ConfigField) o) == 0;
}
@Override // from Object
public int hashCode ()
{
return name.hashCode();
}
}
@@ -0,0 +1,69 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.web.gwt;
import java.util.Map;
import com.google.gwt.user.client.rpc.IsSerializable;
import com.google.gwt.user.client.rpc.RemoteService;
import com.threerings.web.gwt.ServiceException;
/**
* Defines remote services available to admins.
*/
public interface ConfigService extends RemoteService
{
/**
* The current runtime configuration of a server, a collection of {@link ConfigurationRecord}
* objects indexed by key.
*/
public static class ConfigurationResult
implements IsSerializable
{
public Map<String, ConfigurationRecord> records;
}
/**
* The runtime configuration of a single {@link com.threerings.admin.data.ConfigObject}.
*/
public static class ConfigurationRecord
implements IsSerializable
{
public ConfigField[] fields;
public int updates;
}
/**
* Retrieve all the runtime configuration held by the server and return it in a format
* that is digestible by GWT.
*/
public ConfigurationResult getConfiguration () throws ServiceException;
/**
* Submit a collection of updated fields to the server for application to its runtime
* configuration. A new snapshot of the configuration state is returned for sanity checking
* purposes.
*/
public ConfigurationRecord updateConfiguration (String key, ConfigField[] updates)
throws ServiceException;
}
@@ -0,0 +1,44 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.web.gwt;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.threerings.admin.web.gwt.ConfigService.ConfigurationRecord;
import com.threerings.admin.web.gwt.ConfigService.ConfigurationResult;
/**
* Provides the asynchronous version of {@link com.threerings.admin.web.gwt.AdminService}.
*/
public interface ConfigServiceAsync
{
/**
* The async version of {@link com.threerings.admin.web.gwt.ConfigService#getConfig}.
*/
public void getConfiguration (AsyncCallback<ConfigurationResult> callback);
/**
* The async version of {@link com.threerings.admin.web.gwt.ConfigService#updateConfiguration}.
*/
public void updateConfiguration (
String key, ConfigField[] updates, AsyncCallback<ConfigurationRecord> callback);
}
@@ -0,0 +1,180 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.admin.web.server;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;
import com.google.inject.Inject;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.threerings.presents.data.InvocationCodes;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.ObjectAccessException;
import com.threerings.presents.dobj.RootDObjectManager;
import com.threerings.web.gwt.ServiceException;
import com.threerings.web.server.ServletWaiter;
import com.threerings.admin.server.ConfigRegistry;
import com.threerings.admin.web.gwt.ConfigField;
import com.threerings.admin.web.gwt.ConfigField.FieldType;
import com.threerings.admin.web.gwt.ConfigService;
import static com.threerings.admin.Log.log;
/**
* Provides the server implementation of {@link ConfigService}.
*/
public abstract class ConfigServlet extends RemoteServiceServlet
implements ConfigService
{
// from interface ConfigService
public ConfigurationResult getConfiguration ()
throws ServiceException
{
requireAdminUser();
final ServletWaiter<ConfigurationResult> waiter =
new ServletWaiter<ConfigurationResult>("getConfiguration");
_omgr.postRunnable(new Runnable() {
public void run () {
Map<String, ConfigurationRecord> tabs = Maps.newHashMap();
for (String key : _confReg.getKeys()) {
ConfigurationRecord record = buildRecord(key);
if (record == null) {
waiter.requestFailed(
new ServiceException(InvocationCodes.E_INTERNAL_ERROR));
return;
}
tabs.put(key, record);
}
ConfigurationResult result = new ConfigurationResult();
result.records = tabs;
waiter.requestCompleted(result);
}
});
return waiter.waitForResult();
}
// from interface ConfigService
public ConfigurationRecord updateConfiguration (final String key, final ConfigField[] updates)
throws ServiceException
{
requireAdminUser();
final ServletWaiter<ConfigurationRecord> waiter =
new ServletWaiter<ConfigurationRecord>("updateConfiguration");
_omgr.postRunnable(new Runnable() {
public void run () {
DObject object = _confReg.getObject(key);
object.startTransaction();
int updateCount = 0;
for (ConfigField update : updates) {
try {
object.changeAttribute(update.name, update.type.toValue(update.valStr));
updateCount ++;
} catch (ObjectAccessException oae) {
log.warning("Failed to update field", "field", update.name, oae);
}
}
object.commitTransaction();
ConfigurationRecord record = buildRecord(key);
record.updates = updateCount;
waiter.requestCompleted(record);
}
});
return waiter.waitForResult();
}
protected ConfigurationRecord buildRecord (String key)
{
DObject object = _confReg.getObject(key);
List<ConfigField> configFields = Lists.newArrayList();
Field[] fields = object.getClass().getFields();
for (Field field : fields) {
if (field.getModifiers() != Modifier.PUBLIC) {
continue;
}
FieldType type = TYPES.get(field.getType());
if (type == null) {
log.warning("Unknown field type", "field", field.getName(),
"type", field.getType());
return null;
}
try {
Object value = field.get(object);
String valStr = type.toString(value);
configFields.add(new ConfigField(field.getName(), type, valStr));
} catch (IllegalAccessException e) {
log.warning("Failure reflecting on configuration object", "key", key,
"object", object, "field", field, e);
return null;
}
}
ConfigurationRecord record = new ConfigurationRecord();
record.fields = Iterables.toArray(configFields, ConfigField.class);
return record;
}
/**
* Implemented on a project by project basis to provide a security fence for configuration
* editing powers.
*/
protected abstract void requireAdminUser ()
throws ServiceException;
@Inject protected ConfigRegistry _confReg;
@Inject protected RootDObjectManager _omgr;
protected static Map<Class<?>, FieldType> TYPES = ImmutableMap.<Class<?>, FieldType>builder()
.put(Integer.class, FieldType.INTEGER)
.put(Integer.TYPE, FieldType.INTEGER)
.put(Short.class, FieldType.SHORT)
.put(Short.TYPE, FieldType.SHORT)
.put(Long.class, FieldType.LONG)
.put(Long.TYPE, FieldType.LONG)
.put(Float.class, FieldType.FLOAT)
.put(Float.TYPE, FieldType.FLOAT)
.put(Double.class, FieldType.DOUBLE)
.put(Double.TYPE, FieldType.DOUBLE)
.put(Boolean.class, FieldType.BOOLEAN)
.put(Boolean.TYPE, FieldType.BOOLEAN)
.put(String.class, FieldType.STRING)
.build();
}
@@ -0,0 +1,33 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau;
import com.samskivert.util.Logger;
/**
* Contains a reference to the log object used by the Bureau services.
*/
public class Log
{
/** We dispatch our log messages through this logger. */
public static Logger log = Logger.getLogger("com.threerings.bureau");
}
@@ -0,0 +1,53 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.client;
import com.threerings.bureau.data.AgentObject;
/**
* Represents an agent running within a bureau client.
*/
public abstract class Agent
{
/**
* Initializes the Agent with the distributed agent object.
*/
public void init (AgentObject agentObj)
{
_agentObj = agentObj;
}
/**
* Starts the code running in the agent.
*/
public abstract void start ();
/**
* Stops the code running in the agent.
*/
public abstract void stop ();
/**
* The shared agent object.
*/
protected AgentObject _agentObj;
}
@@ -0,0 +1,78 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.client;
import com.samskivert.util.Config;
import com.samskivert.util.RunQueue;
import com.threerings.presents.client.Client;
import com.threerings.presents.dobj.DObjectManager;
import com.threerings.bureau.data.BureauCredentials;
import com.threerings.bureau.util.BureauContext;
/**
* Represents a client embedded in a bureau.
*/
public abstract class BureauClient extends Client
{
/**
* Creates a new client.
* @param runQueue the place to post tasks required by clients
*/
public BureauClient (String bureauId, String sharedSecret, RunQueue runQueue)
{
super(null, runQueue);
_bureauId = bureauId;
_creds = new BureauCredentials(_bureauId, sharedSecret);
_ctx = createContext();
_director = createDirector();
}
protected abstract BureauDirector createDirector ();
protected BureauContext createContext ()
{
return new BureauContext() {
public BureauDirector getBureauDirector () {
return _director;
}
public DObjectManager getDObjectManager () {
return _omgr;
}
public Client getClient () {
return BureauClient.this;
}
public Config getConfig () {
return _config;
}
public String getBureauId () {
return _bureauId;
}
};
}
protected BureauContext _ctx;
protected String _bureauId;
protected BureauDirector _director;
protected Config _config = new Config("bureau");
}
@@ -0,0 +1,78 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.client;
import com.threerings.presents.client.InvocationDecoder;
/**
* Dispatches calls to a {@link BureauReceiver} instance.
*/
public class BureauDecoder extends InvocationDecoder
{
/** The generated hash code used to identify this receiver class. */
public static final String RECEIVER_CODE = "3e98f7a30deb5a8e25e05c71c6081bf4";
/** The method id used to dispatch {@link BureauReceiver#createAgent}
* notifications. */
public static final int CREATE_AGENT = 1;
/** The method id used to dispatch {@link BureauReceiver#destroyAgent}
* notifications. */
public static final int DESTROY_AGENT = 2;
/**
* Creates a decoder that may be registered to dispatch invocation
* service notifications to the specified receiver.
*/
public BureauDecoder (BureauReceiver receiver)
{
this.receiver = receiver;
}
@Override
public String getReceiverCode ()
{
return RECEIVER_CODE;
}
@Override
public void dispatchNotification (int methodId, Object[] args)
{
switch (methodId) {
case CREATE_AGENT:
((BureauReceiver)receiver).createAgent(
((Integer)args[0]).intValue()
);
return;
case DESTROY_AGENT:
((BureauReceiver)receiver).destroyAgent(
((Integer)args[0]).intValue()
);
return;
default:
super.dispatchNotification(methodId, args);
return;
}
}
}
@@ -0,0 +1,187 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.client;
import com.samskivert.util.IntMap;
import com.samskivert.util.IntMaps;
import com.threerings.presents.client.BasicDirector;
import com.threerings.presents.client.Client;
import com.threerings.presents.dobj.ObjectAccessException;
import com.threerings.presents.dobj.Subscriber;
import com.threerings.presents.util.SafeSubscriber;
import com.threerings.bureau.data.AgentObject;
import com.threerings.bureau.data.BureauCodes;
import com.threerings.bureau.server.BureauRegistry;
import com.threerings.bureau.util.BureauContext;
import static com.threerings.bureau.Log.log;
/**
* Allows the server to create and destroy agents on a client.
* @see BureauRegistry
*/
public abstract class BureauDirector extends BasicDirector
{
/**
* Creates a new BureauDirector.
*/
public BureauDirector (BureauContext ctx)
{
super(ctx);
_ctx = ctx;
}
@Override // from BasicDirector
public void clientDidLogon (Client client)
{
super.clientDidLogon(client);
_bureauService.bureauInitialized(_ctx.getClient(), _ctx.getBureauId());
}
/**
* Creates a new agent when the server requests it.
*/
protected synchronized void createAgent (int agentId)
{
Subscriber<AgentObject> delegator = new Subscriber<AgentObject>() {
public void objectAvailable (AgentObject agentObject) {
BureauDirector.this.objectAvailable(agentObject);
}
public void requestFailed (int oid, ObjectAccessException cause) {
BureauDirector.this.requestFailed(oid, cause);
}
};
log.info("Subscribing to object " + agentId);
SafeSubscriber<AgentObject> subscriber =
new SafeSubscriber<AgentObject>(agentId, delegator);
_subscribers.put(agentId, subscriber);
subscriber.subscribe(_ctx.getDObjectManager());
}
/**
* Destroys an agent at the server's request.
*/
protected synchronized void destroyAgent (int agentId)
{
Agent agent = null;
agent = _agents.remove(agentId);
if (agent == null) {
log.warning("Lost an agent, id " + agentId);
} else {
try {
agent.stop();
} catch (Throwable t) {
log.warning("Stopping an agent caused an exception", t);
}
SafeSubscriber<AgentObject> subscriber = _subscribers.remove(agentId);
if (subscriber == null) {
log.warning("Lost a subscriber for agent " + agent);
} else {
subscriber.unsubscribe(_ctx.getDObjectManager());
}
_bureauService.agentDestroyed(_ctx.getClient(), agentId);
}
}
/**
* Callback for when the a request to subscribe to an object finishes and the object is
* available.
*/
protected synchronized void objectAvailable (AgentObject agentObject)
{
int oid = agentObject.getOid();
log.info("Object " + oid + " now available");
Agent agent;
try {
agent = createAgent(agentObject);
agent.init(agentObject);
agent.start();
} catch (Throwable t) {
log.warning("Could not create agent", "obj", agentObject, t);
_bureauService.agentCreationFailed(_ctx.getClient(), oid);
return;
}
_agents.put(oid, agent);
_bureauService.agentCreated(_ctx.getClient(), oid);
}
/**
* Callback for when the a request to subscribe to an object fails.
*/
protected synchronized void requestFailed (int oid, ObjectAccessException cause)
{
log.warning("Could not subscribe to agent", "oid", oid, cause);
}
@Override // from BasicDirector
protected void registerServices (Client client)
{
super.registerServices(client);
// Require the bureau services
client.addServiceGroup(BureauCodes.BUREAU_GROUP);
// Set up our decoder so we can receive method calls from the server
BureauReceiver receiver = new BureauReceiver() {
public void createAgent (int agentId) {
BureauDirector.this.createAgent(agentId);
}
public void destroyAgent (int agentId) {
BureauDirector.this.destroyAgent(agentId);
}
};
client.getInvocationDirector().
registerReceiver(new BureauDecoder(receiver));
}
@Override // from BasicDirector
protected void fetchServices (Client client)
{
super.fetchServices(client);
_bureauService = client.getService(BureauService.class);
}
/**
* Called when it is time to create an Agent. Subclasses should read the
* <code>agentObject</code>'s type and/or properties to determine what kind of Agent to
* create.
* @param agentObj the distributed and object
* @return a new Agent that will govern the distributed object
*/
protected abstract Agent createAgent (AgentObject agentObj);
protected BureauContext _ctx;
protected BureauService _bureauService;
protected IntMap<Agent> _agents = IntMaps.newHashIntMap();
protected IntMap<SafeSubscriber<AgentObject>> _subscribers =
IntMaps.newHashIntMap();
}
@@ -0,0 +1,49 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.client;
import com.threerings.presents.client.InvocationReceiver;
import com.threerings.bureau.data.AgentObject;
/**
* Hooks for controlling a previously launched bureau client.
*/
public interface BureauReceiver extends InvocationReceiver
{
/**
* Creates a new agent. Implementors should create a new {@link Agent} and give it access to
* the {@link AgentObject} referred to by the <code>agentId</code> parameter and must notify
* the service that the agent has been created using {@link BureauService#agentCreated}.
* @param agentId the id of the <code>AgentObject</code> that needs an <code>Agent</code>
*/
void createAgent (int agentId);
/**
* Destroys a previously created agent. Implementors should destroy the agent that was created
* by the call to <code>createAgent</code> with the same agent id and must notify
* the service that the agent has been created using {@link BureauService#agentDestroyed}.
* @param agentId the id of the <code>AgentObject</code> whose <code>Agent</code>
* should be destroyed
*/
void destroyAgent (int agentId);
}
@@ -0,0 +1,64 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.client;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
/**
* Interface for the bureau to communicate with the server.
*/
public interface BureauService extends InvocationService
{
/**
* Notifies the server that the bureau is up and running and ready to receive
* requests via the <code>BureauReceiver</code>.
* @see BureauReceiver
*/
void bureauInitialized (Client client, String bureauId);
/**
* Notifies the server that this bureau has encountered a critical error and needs to be shut
* down.
*/
void bureauError (Client client, String message);
/**
* Notify the server that a previosuly requested agent is now created and ready to use.
* @see BureauReceiver#createAgent
*/
void agentCreated (Client client, int agentId);
/**
* Notify the server that a previosuly requested agent could not be created.
* @see BureauReceiver#createAgent
*/
void agentCreationFailed (Client client, int agentId);
/**
* Notify the server that an agent is no longer running. Normally called in response
* to a call to <code>destroyAgent</code>
* @see BureauReceiver#destroyAgent
*/
void agentDestroyed (Client client, int agentId);
}
@@ -0,0 +1,171 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.data;
import javax.annotation.Generated;
import com.threerings.presents.dobj.DObject;
/**
* Contains information for configuring and communicating with an agent.
*/
public class AgentObject extends DObject
{
// AUTO-GENERATED: FIELDS START
/** The field name of the <code>bureauId</code> field. */
@Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
public static final String BUREAU_ID = "bureauId";
/** The field name of the <code>bureauType</code> field. */
@Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
public static final String BUREAU_TYPE = "bureauType";
/** The field name of the <code>code</code> field. */
@Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
public static final String CODE = "code";
/** The field name of the <code>className</code> field. */
@Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
public static final String CLASS_NAME = "className";
/** The field name of the <code>clientOid</code> field. */
@Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
public static final String CLIENT_OID = "clientOid";
// AUTO-GENERATED: FIELDS END
/** The id of the bureau the agent is running in. This is normally a unique id corresponding
* to the game or item that requires some server-side processing. */
public String bureauId;
/** The type of bureau that the agent is running in. This is normally derived from the kind
* of media that the game or item has specified for its code and determines the method of
* launching the bureau when the first agent is requested. */
public String bureauType;
/** The location of the code for the agent. This could be a URL to an action script file or
* some other description that the bureau can use to load and execute the agent's code. */
public String code;
/** The main class within the code to use when launching an agent. Whether this value is
* used depends on the type of bureau and will be resolve in the bureau client. */
public String className;
/** The id of the client running this agent (only set after the agent is assigned to a
* bureau and run). */
public int clientOid;
/**
* Returns a brief string that identifies this agent. Use this instead of
* {@link Object#toString} when you wish to report an agent object in a log message.
*/
@Override
public String which ()
{
return "[bid=" + bureauId + ", type=" + bureauType + "]";
}
// AUTO-GENERATED: METHODS START
/**
* Requests that the <code>bureauId</code> field be set to the
* specified value. The local value will be updated immediately and an
* event will be propagated through the system to notify all listeners
* that the attribute did change. Proxied copies of this object (on
* clients) will apply the value change when they received the
* attribute changed notification.
*/
@Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
public void setBureauId (String value)
{
String ovalue = this.bureauId;
requestAttributeChange(
BUREAU_ID, value, ovalue);
this.bureauId = value;
}
/**
* Requests that the <code>bureauType</code> field be set to the
* specified value. The local value will be updated immediately and an
* event will be propagated through the system to notify all listeners
* that the attribute did change. Proxied copies of this object (on
* clients) will apply the value change when they received the
* attribute changed notification.
*/
@Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
public void setBureauType (String value)
{
String ovalue = this.bureauType;
requestAttributeChange(
BUREAU_TYPE, value, ovalue);
this.bureauType = value;
}
/**
* Requests that the <code>code</code> field be set to the
* specified value. The local value will be updated immediately and an
* event will be propagated through the system to notify all listeners
* that the attribute did change. Proxied copies of this object (on
* clients) will apply the value change when they received the
* attribute changed notification.
*/
@Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
public void setCode (String value)
{
String ovalue = this.code;
requestAttributeChange(
CODE, value, ovalue);
this.code = value;
}
/**
* Requests that the <code>className</code> field be set to the
* specified value. The local value will be updated immediately and an
* event will be propagated through the system to notify all listeners
* that the attribute did change. Proxied copies of this object (on
* clients) will apply the value change when they received the
* attribute changed notification.
*/
@Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
public void setClassName (String value)
{
String ovalue = this.className;
requestAttributeChange(
CLASS_NAME, value, ovalue);
this.className = value;
}
/**
* Requests that the <code>clientOid</code> field be set to the
* specified value. The local value will be updated immediately and an
* event will be propagated through the system to notify all listeners
* that the attribute did change. Proxied copies of this object (on
* clients) will apply the value change when they received the
* attribute changed notification.
*/
@Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
public void setClientOid (int value)
{
int ovalue = this.clientOid;
requestAttributeChange(
CLIENT_OID, Integer.valueOf(value), Integer.valueOf(ovalue));
this.clientOid = value;
}
// AUTO-GENERATED: METHODS END
}
@@ -0,0 +1,40 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.data;
import com.threerings.util.Name;
/**
* Represents an authenticated bureau client.
*/
public class BureauAuthName extends Name
{
public BureauAuthName (String bureauId)
{
super(bureauId);
}
// used when unserializing
public BureauAuthName ()
{
}
}
@@ -0,0 +1,36 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.data;
import com.threerings.presents.data.ClientObject;
/**
* An object representing a Bureau connection. This is currently just a marker class.
*/
public class BureauClientObject extends ClientObject
{
@Override
public String toString ()
{
return "BUREAU_CLIENT_OBJECT(" + super.toString() + ")";
}
}
@@ -0,0 +1,33 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.data;
import com.threerings.presents.data.InvocationCodes;
/**
* Codes and constants global to the Bureau services.
*/
public interface BureauCodes extends InvocationCodes
{
/** Defines our invocation services group. */
public static final String BUREAU_GROUP = "bureau";
}
@@ -0,0 +1,45 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.data;
import com.threerings.presents.net.ServiceCreds;
/**
* Extends the basic credentials to provide bureau-specific fields.
*/
public class BureauCredentials extends ServiceCreds
{
/**
* Creates new credentials for a specific bureau.
*/
public BureauCredentials (String bureauId, String sharedSecret)
{
super(bureauId, sharedSecret);
}
/**
* Creates an empty credentials for streaming. Should not be used directly.
*/
public BureauCredentials ()
{
}
}
@@ -0,0 +1,96 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.data;
import javax.annotation.Generated;
import com.threerings.bureau.client.BureauService;
import com.threerings.presents.client.Client;
import com.threerings.presents.data.InvocationMarshaller;
/**
* Provides the implementation of the {@link BureauService} interface
* that marshalls the arguments and delivers the request to the provider
* on the server. Also provides an implementation of the response listener
* interfaces that marshall the response arguments and deliver them back
* to the requesting client.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from BureauService.java.")
public class BureauMarshaller extends InvocationMarshaller
implements BureauService
{
/** The method id used to dispatch {@link #agentCreated} requests. */
public static final int AGENT_CREATED = 1;
// from interface BureauService
public void agentCreated (Client arg1, int arg2)
{
sendRequest(arg1, AGENT_CREATED, new Object[] {
Integer.valueOf(arg2)
});
}
/** The method id used to dispatch {@link #agentCreationFailed} requests. */
public static final int AGENT_CREATION_FAILED = 2;
// from interface BureauService
public void agentCreationFailed (Client arg1, int arg2)
{
sendRequest(arg1, AGENT_CREATION_FAILED, new Object[] {
Integer.valueOf(arg2)
});
}
/** The method id used to dispatch {@link #agentDestroyed} requests. */
public static final int AGENT_DESTROYED = 3;
// from interface BureauService
public void agentDestroyed (Client arg1, int arg2)
{
sendRequest(arg1, AGENT_DESTROYED, new Object[] {
Integer.valueOf(arg2)
});
}
/** The method id used to dispatch {@link #bureauError} requests. */
public static final int BUREAU_ERROR = 4;
// from interface BureauService
public void bureauError (Client arg1, String arg2)
{
sendRequest(arg1, BUREAU_ERROR, new Object[] {
arg2
});
}
/** The method id used to dispatch {@link #bureauInitialized} requests. */
public static final int BUREAU_INITIALIZED = 5;
// from interface BureauService
public void bureauInitialized (Client arg1, String arg2)
{
sendRequest(arg1, BUREAU_INITIALIZED, new Object[] {
arg2
});
}
}
@@ -0,0 +1,39 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.server;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.ClientResolver;
import com.threerings.bureau.data.BureauClientObject;
/**
* Used to configure crowd-specific client object data.
*/
public class BureauClientResolver extends ClientResolver
{
@Override // from ClientResolver
public ClientObject createClientObject ()
{
return new BureauClientObject();
}
}
@@ -0,0 +1,94 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.server;
import javax.annotation.Generated;
import com.threerings.bureau.data.BureauMarshaller;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationDispatcher;
import com.threerings.presents.server.InvocationException;
/**
* Dispatches requests to the {@link BureauProvider}.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from BureauService.java.")
public class BureauDispatcher extends InvocationDispatcher<BureauMarshaller>
{
/**
* Creates a dispatcher that may be registered to dispatch invocation
* service requests for the specified provider.
*/
public BureauDispatcher (BureauProvider provider)
{
this.provider = provider;
}
@Override
public BureauMarshaller createMarshaller ()
{
return new BureauMarshaller();
}
@Override
public void dispatchRequest (
ClientObject source, int methodId, Object[] args)
throws InvocationException
{
switch (methodId) {
case BureauMarshaller.AGENT_CREATED:
((BureauProvider)provider).agentCreated(
source, ((Integer)args[0]).intValue()
);
return;
case BureauMarshaller.AGENT_CREATION_FAILED:
((BureauProvider)provider).agentCreationFailed(
source, ((Integer)args[0]).intValue()
);
return;
case BureauMarshaller.AGENT_DESTROYED:
((BureauProvider)provider).agentDestroyed(
source, ((Integer)args[0]).intValue()
);
return;
case BureauMarshaller.BUREAU_ERROR:
((BureauProvider)provider).bureauError(
source, (String)args[0]
);
return;
case BureauMarshaller.BUREAU_INITIALIZED:
((BureauProvider)provider).bureauInitialized(
source, (String)args[0]
);
return;
default:
super.dispatchRequest(source, methodId, args);
return;
}
}
}
@@ -0,0 +1,61 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.server;
import javax.annotation.Generated;
import com.threerings.bureau.client.BureauService;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationProvider;
/**
* Defines the server-side of the {@link BureauService}.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from BureauService.java.")
public interface BureauProvider extends InvocationProvider
{
/**
* Handles a {@link BureauService#agentCreated} request.
*/
void agentCreated (ClientObject caller, int arg1);
/**
* Handles a {@link BureauService#agentCreationFailed} request.
*/
void agentCreationFailed (ClientObject caller, int arg1);
/**
* Handles a {@link BureauService#agentDestroyed} request.
*/
void agentDestroyed (ClientObject caller, int arg1);
/**
* Handles a {@link BureauService#bureauError} request.
*/
void bureauError (ClientObject caller, String arg1);
/**
* Handles a {@link BureauService#bureauInitialized} request.
*/
void bureauInitialized (ClientObject caller, String arg1);
}
@@ -0,0 +1,804 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.server;
import java.util.Map;
import java.util.Set;
import java.io.IOException;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.util.Interval;
import com.samskivert.util.Invoker;
import com.samskivert.util.RunQueue;
import com.samskivert.util.StringUtil;
import com.threerings.presents.annotation.MainInvoker;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.dobj.RootDObjectManager;
import com.threerings.presents.server.ClientManager;
import com.threerings.presents.server.InvocationManager;
import com.threerings.presents.server.PresentsSession;
import com.threerings.presents.server.ServiceAuthenticator;
import com.threerings.presents.server.SessionFactory;
import com.threerings.presents.server.net.ConnectionManager;
import com.threerings.bureau.data.AgentObject;
import com.threerings.bureau.data.BureauAuthName;
import com.threerings.bureau.data.BureauCodes;
import com.threerings.bureau.data.BureauCredentials;
import com.threerings.bureau.util.BureauLogRedirector;
import static com.threerings.bureau.Log.log;
/**
* Abstracts the launching and termination of external processes (bureaus) that host instances of
* server-side code (agents).
*/
@Singleton
public class BureauRegistry
{
/**
* Defines how a bureau is launched. Instances are associated to bureau types by the server on
* startup. The instances are used whenever the registry needs to launch a bureau for an agent
* with the associated bureau type.
*/
public static interface Launcher
{
/**
* Kicks off a new bureau. This method will always be called on the unit invocation
* thread since it may do extensive I/O.
* @param bureauId the id of the bureau being launched
* @param token the secret string for the bureau to use in its credentials
*/
void launchBureau (String bureauId, String token)
throws IOException;
}
/**
* Defines how to generate a command to launch a bureau in a local process.
* @see #setCommandGenerator(String,CommandGenerator,int)
* @see Launcher
*/
public static interface CommandGenerator
{
/**
* Creates the command line to launch a new bureau using the given information.
* Called by the registry when a new bureau is needed whose type was registered
* with <code>setCommandGenerator</code>.
* @param bureauId the id of the bureau being launched
* @param token the token string to use for the credentials when logging in
* @return command line arguments, including executable name
*/
String[] createCommand (String bureauId, String token);
}
/**
* Creates an uninitialized registry.
*/
@Inject public BureauRegistry (
InvocationManager invmgr, ConnectionManager conmgr, ClientManager clmgr)
{
invmgr.registerDispatcher(new BureauDispatcher(new BureauProvider() {
public void bureauInitialized (ClientObject client, String bureauId) {
BureauRegistry.this.bureauInitialized(client, bureauId);
}
public void bureauError (ClientObject caller, String message) {
BureauRegistry.this.bureauError(caller, message);
}
public void agentCreated (ClientObject client, int agentId) {
BureauRegistry.this.agentCreated(client, agentId);
}
public void agentCreationFailed (ClientObject client, int agentId) {
BureauRegistry.this.agentCreationFailed(client, agentId);
}
public void agentDestroyed (ClientObject client, int agentId) {
BureauRegistry.this.agentDestroyed(client, agentId);
}
}), BureauCodes.BUREAU_GROUP);
conmgr.addChainedAuthenticator(new ServiceAuthenticator<BureauCredentials>(
BureauCredentials.class, BureauAuthName.class) {
@Override protected boolean areValid (BureauCredentials creds) {
return checkToken(creds) == null;
}
});
clmgr.addSessionFactory(
SessionFactory.newSessionFactory(BureauCredentials.class, getSessionClass(),
BureauAuthName.class, getClientResolverClass()));
clmgr.addClientObserver(new ClientManager.ClientObserver() {
public void clientSessionDidStart (PresentsSession client) {
if (client.getCredentials() instanceof BureauCredentials) {
sessionDidStart(client, ((BureauCredentials)client.getCredentials()).clientId);
}
}
public void clientSessionDidEnd (PresentsSession client) {
if (client.getCredentials() instanceof BureauCredentials) {
sessionDidEnd(client, ((BureauCredentials)client.getCredentials()).clientId);
}
}
});
}
/**
* Check the credentials to make sure this is one of our bureaus.
* @return null if all's well, otherwise a string describing the authentication failure
*/
public String checkToken (BureauCredentials creds)
{
Bureau bureau = _bureaus.get(creds.clientId);
if (bureau == null) {
return "Bureau " + creds.clientId + " not found";
}
if (bureau.clientObj != null) {
return "Bureau " + creds.clientId + " already logged in";
}
if (!creds.areValid(bureau.token)) {
return "Bureau " + creds.clientId + " does not match credentials token";
}
return null;
}
/**
* Registers a command generator for a given type. When an agent is started and no bureaus are
* running, the <code>bureauType</code> is used to determine the <code>CommandGenerator</code>
* instance to call. The registry will wait indefinitely for the bureau to connect back.
* @param bureauType the type of bureau that will be launched
* @param cmdGenerator the generator to be used for bureaus of <code>bureauType</code>
*/
public void setCommandGenerator (String bureauType, final CommandGenerator cmdGenerator)
{
setCommandGenerator(bureauType, cmdGenerator, 0);
}
/**
* Registers a command generator for a given type. When an agent is started and no bureaus are
* running, the <code>bureauType</code> is used to determine the <code>CommandGenerator</code>
* instance to call. If the launched bureau does not connect within the given number of
* milliseconds, it will be logged as an error and future attempts to launch the bureau
* will try launching the command again.
* @param bureauType the type of bureau that will be launched
* @param cmdGenerator the generator to be used for bureaus of <code>bureauType</code>
* @param timeout milliseconds to wait for the bureau or 0 to wait forever
*/
public void setCommandGenerator (
String bureauType, final CommandGenerator cmdGenerator, int timeout)
{
setLauncher(bureauType, new Launcher() {
public void launchBureau (String bureauId, String token)
throws IOException {
ProcessBuilder builder = new ProcessBuilder(
cmdGenerator.createCommand(bureauId, token));
builder.redirectErrorStream(true);
Process process = builder.start();
// log the output of the process and prefix with bureau id
new BureauLogRedirector(bureauId, process.getInputStream());
}
@Override
public String toString () {
return "DefaultLauncher for " + cmdGenerator;
}
}, timeout);
}
/**
* Registers a launcher for a given type. When an agent is started and no bureaus are
* running, the <code>bureauType</code> is used to determine the <code>Launcher</code>
* instance to call. The registry will wait indefinitely for the launched bureau
* to connect back.
* @param bureauType the type of bureau that will be launched
* @param launcher the launcher to be used for bureaus of <code>bureauType</code>
*/
public void setLauncher (String bureauType, Launcher launcher)
{
setLauncher(bureauType, launcher, 0);
}
/**
* Registers a launcher for a given type. When an agent is started and no bureaus are
* running, the <code>bureauType</code> is used to determine the <code>Launcher</code>
* instance to call. If the launched bureau does not connect within the given number of
* milliseconds, it will be logged as an error and future attempts to launch the bureau
* will invoke the <code>launch</code> method again.
* @param bureauType the type of bureau that will be launched
* @param launcher the launcher to be used for bureaus of <code>bureauType</code>
* @param timeout milliseconds to wait for the bureau or 0 to wait forever
*/
public void setLauncher (String bureauType, Launcher launcher, int timeout)
{
if (_launchers.get(bureauType) != null) {
log.warning("Launcher for type already exists", "type", bureauType);
return;
}
_launchers.put(bureauType, new LauncherEntry(launcher, timeout));
}
/**
* Starts a new agent using the data in the given object, creating a new bureau if necessary.
*/
public void startAgent (AgentObject agent)
{
agent.setLocal(AgentData.class, new AgentData());
Bureau bureau = _bureaus.get(agent.bureauId);
if (bureau != null && bureau.ready()) {
_omgr.registerObject(agent);
log.info("Bureau ready, sending createAgent", "agent", agent.which());
BureauSender.createAgent(bureau.clientObj, agent.getOid());
bureau.agentStates.put(agent, AgentState.STARTED);
bureau.summarize();
return;
}
if (bureau == null) {
LauncherEntry launcherEntry = _launchers.get(agent.bureauType);
if (launcherEntry == null) {
log.warning("Launcher not found", "agent", agent.which());
return;
}
log.info("Creating new bureau", "bureauId", agent.bureauId, "launcher", launcherEntry);
bureau = new Bureau();
bureau.bureauId = agent.bureauId;
bureau.token = generateToken(bureau.bureauId);
bureau.launcherEntry = launcherEntry;
_invoker.postUnit(new LauncherUnit(bureau, _omgr));
_bureaus.put(agent.bureauId, bureau);
}
_omgr.registerObject(agent);
bureau.agentStates.put(agent, AgentState.PENDING);
log.info("Bureau not ready, pending agent", "agent", agent.which());
bureau.summarize();
}
/**
* Destroys a previously started agent using the data in the given object.
*/
public void destroyAgent (AgentObject agent)
{
FoundAgent found = resolve(null, agent.getOid(), "destroyAgent");
if (found == null) {
return;
}
log.info("Destroying agent", "agent", agent.which());
// transition the agent to a new state and perform the effect of the transition
if (found.state == AgentState.PENDING) {
found.bureau.agentStates.remove(found.agent);
_omgr.destroyObject(found.agent.getOid());
} else if (found.state == AgentState.STARTED) {
found.bureau.agentStates.put(found.agent, AgentState.STILL_BORN);
} else if (found.state == AgentState.RUNNING) {
// TODO: have a timeout for this in case the client is misbehaving or hung
BureauSender.destroyAgent(found.bureau.clientObj, agent.getOid());
found.bureau.agentStates.put(found.agent, AgentState.DESTROYED);
} else if (found.state == AgentState.DESTROYED ||
found.state == AgentState.STILL_BORN) {
log.warning("Ignoring request to destroy agent in unexpected state",
"state", found.state, "agent", found.agent.which());
}
found.bureau.summarize();
}
/**
* Returns the active session for a bureau of the given id.
*/
public PresentsSession lookupClient (String bureauId)
{
Bureau bureau = _bureaus.get(bureauId);
if (bureau == null) {
return null;
}
return bureau.client;
}
/**
* If this agent's bureau encountered an error on launch, return it.
*/
public Exception getLaunchError (AgentObject agentObj)
{
AgentData data = agentObj.getLocal(AgentData.class);
if (data == null) {
return null;
}
return data.launchError;
}
protected void sessionDidStart (PresentsSession client, String id)
{
Bureau bureau = _bureaus.get(id);
if (bureau == null) {
log.warning("Starting session for unknown bureau", "id", id, "client", client);
return;
}
if (bureau.client != null) {
log.warning("Multiple sessions for the same bureau", "id", id, "client", client,
"bureau", bureau);
}
bureau.client = client;
}
protected void sessionDidEnd (PresentsSession client, String id)
{
Bureau bureau = _bureaus.get(id);
if (bureau == null) {
log.warning("Ending session for unknown bureau", "id", id, "client", client);
return;
}
if (bureau.client == null) {
log.warning("Multiple logouts from the same bureau", "id", id, "client", client,
"bureau", bureau);
}
bureau.client = null;
clientDestroyed(bureau);
}
/**
* Callback for when the bureau client acknowledges starting up. Starts all pending agents and
* causes subsequent agent start requests to be sent directly to the bureau.
*/
protected void bureauInitialized (ClientObject client, String bureauId)
{
final Bureau bureau = _bureaus.get(bureauId);
if (bureau == null) {
log.warning("Initialization of non-existent bureau", "bureauId", bureauId);
return;
}
bureau.clientObj = client;
log.info("Bureau created, launching pending agents", "bureau", bureau);
// find all pending agents
Set<AgentObject> pending = Sets.newHashSet();
for (Map.Entry<AgentObject, AgentState> entry :
bureau.agentStates.entrySet()) {
if (entry.getValue() == AgentState.PENDING) {
pending.add(entry.getKey());
}
}
// create them
for (AgentObject agent : pending) {
log.info("Creating agent", "agent", agent.which());
BureauSender.createAgent(bureau.clientObj, agent.getOid());
bureau.agentStates.put(agent, AgentState.STARTED);
}
bureau.summarize();
}
protected void bureauError (ClientObject caller, String message)
{
for (Bureau bureau : _bureaus.values()) {
if (bureau.clientObj == caller) {
log.info(
"Bureau error occurred", "caller", caller.who(), "message", message,
"bureau", bureau.bureauId);
bureau.client.endSession();
return;
}
}
log.warning(
"Bureau error occurred in unregistered bureau", "caller", caller.who(),
"message", message);
}
/**
* Callback for when the bureau client acknowledges the creation of an agent.
*/
protected void agentCreated (ClientObject client, int agentId)
{
FoundAgent found = resolve(client, agentId, "agentCreated");
if (found == null) {
return;
}
log.info("Agent creation confirmed", "agent", found.agent.which());
if (found.state == AgentState.STARTED) {
found.bureau.agentStates.put(found.agent, AgentState.RUNNING);
found.agent.setClientOid(client.getOid());
} else if (found.state == AgentState.STILL_BORN) {
// TODO: have a timeout for this in case the client is misbehaving or hung
BureauSender.destroyAgent(found.bureau.clientObj, agentId);
found.bureau.agentStates.put(found.agent, AgentState.DESTROYED);
} else if (found.state == AgentState.PENDING ||
found.state == AgentState.RUNNING ||
found.state == AgentState.DESTROYED) {
log.warning("Ignoring confirmation of creation of an agent in an unexpected state",
"state", found.state, "agent", found.agent.which());
}
found.bureau.summarize();
}
/**
* Callback for when the bureau client acknowledges the failure to create an agent.
*/
protected void agentCreationFailed (ClientObject client, int agentId)
{
FoundAgent found = resolve(client, agentId, "agentCreationFailed");
if (found == null) {
return;
}
log.info("Agent creation failed", "agent", found.agent.which());
if (found.state == AgentState.STARTED ||
found.state == AgentState.STILL_BORN) {
found.bureau.agentStates.remove(found.agent);
_omgr.destroyObject(found.agent.getOid());
} else if (found.state == AgentState.PENDING ||
found.state == AgentState.RUNNING ||
found.state == AgentState.DESTROYED) {
log.warning("Ignoring failure of creation of an agent in an unexpected state",
"state", found.state, "agent", found.agent.which());
}
found.bureau.summarize();
}
/**
* Callback for when the bureau client acknowledges the destruction of an agent.
*/
protected void agentDestroyed (ClientObject client, int agentId)
{
FoundAgent found = resolve(client, agentId, "agentDestroyed");
if (found == null) {
return;
}
log.info("Agent destruction confirmed", "agent", found.agent.which());
if (found.state == AgentState.DESTROYED) {
found.bureau.agentStates.remove(found.agent);
_omgr.destroyObject(found.agent.getOid());
} else if (found.state == AgentState.PENDING ||
found.state == AgentState.STARTED ||
found.state == AgentState.RUNNING ||
found.state == AgentState.STILL_BORN) {
log.warning("Ignoring confirmation of destruction of agent in unexpected state",
"state", found.state, "agent", found.agent.which());
}
found.bureau.summarize();
}
/**
* Callback for when a client is destroyed.
*/
protected void clientDestroyed (Bureau bureau)
{
log.info("Client destroyed, destroying all agents", "bureau", bureau);
// clean up any agents attached to this bureau
for (AgentObject agent : bureau.agentStates.keySet()) {
_omgr.destroyObject(agent.getOid());
}
bureau.agentStates.clear();
if (_bureaus.remove(bureau.bureauId) == null) {
log.info("Bureau not found to remove", "bureau", bureau);
}
}
/**
* Does lots of null checks and lookups and resolves the given information into FoundAgent.
*/
protected FoundAgent resolve (ClientObject client, int agentId, String resolver)
{
com.threerings.presents.dobj.DObject dobj = _omgr.getObject(agentId);
if (dobj == null) {
log.warning("Non-existent agent", "function", resolver, "agentId", agentId);
return null;
}
if (!(dobj instanceof AgentObject)) {
log.warning("Object not an agent", "function", resolver, "obj", dobj.getClass());
return null;
}
AgentObject agent = (AgentObject)dobj;
Bureau bureau = _bureaus.get(agent.bureauId);
if (bureau == null) {
log.warning("Bureau not found for agent", "function", resolver, "agent", agent.which());
return null;
}
if (!bureau.agentStates.containsKey(agent)) {
log.warning("Bureau does not have agent", "function", resolver, "agent", agent.which());
return null;
}
if (client != null && bureau.clientObj != client) {
log.warning("Masquerading request", "function", resolver, "agent", agent.which(),
"client", bureau.clientObj, "client", client);
return null;
}
return new FoundAgent(bureau, agent, bureau.agentStates.get(agent));
}
/**
* Create a hard-to-guess token that the bureau can use to authenticate itself when it tries
* to log in.
*/
protected String generateToken (String bureauId)
{
String tokenSource = bureauId + "@" + System.currentTimeMillis() + "r" + Math.random();
return StringUtil.md5hex(tokenSource);
}
/**
* Called by the launcher unit timeout time after launching.
* @param bureau bureau whose launch occurred
*/
protected void launchTimeoutExpired (Bureau bureau)
{
if (bureau.clientObj != null) {
return; // all's well, ignore
}
if (!_bureaus.containsKey(bureau.bureauId)) {
// bureau has already managed to get destroyed before the launch timeout, ignore
return;
}
handleLaunchError(bureau, null, "timeout");
}
/**
* Called when something goes wrong with launching a bureau.
*/
protected void handleLaunchError (Bureau bureau, Exception error, String cause)
{
if (cause == null && error != null) {
cause = error.getMessage();
}
log.info("Bureau failed to launch", "bureau", bureau, "cause", cause);
// clean up any agents attached to this bureau
for (AgentObject agent : bureau.agentStates.keySet()) {
agent.getLocal(AgentData.class).launchError = error;
_omgr.destroyObject(agent.getOid());
}
bureau.agentStates.clear();
_bureaus.remove(bureau.bureauId);
}
/**
* Returns the class used to handle bureau sessions.
*/
protected Class<? extends BureauSession> getSessionClass ()
{
return BureauSession.class;
}
/**
* Returns the class used to resolve bureau client data.
*/
protected Class<? extends BureauClientResolver> getClientResolverClass ()
{
return BureauClientResolver.class;
}
/**
* Invoker unit to launch a bureau's process, then assign the result on the main thread.
*/
protected class LauncherUnit extends Invoker.Unit
{
LauncherUnit (Bureau bureau, RunQueue runQueue) {
super("LauncherUnit for " + bureau + ": " + StringUtil.toString(bureau.launcherEntry));
_bureau = bureau;
_runQueue = runQueue;
}
@Override public boolean invoke () {
try {
_bureau.launch();
} catch (Exception e) {
_error = e;
}
return true;
}
@Override
public void handleResult () {
if (_error == null) {
// bureau launched ok, but it may still not connect. wait for timeout
int timeout = _bureau.launcherEntry.timeout;
if (timeout != 0) {
new Interval(_runQueue) {
@Override public void expired () {
launchTimeoutExpired(_bureau);
}
}.schedule(timeout);
}
_bureau.launched = true;
_bureau.launcherEntry = null;
log.info("Bureau launch requested", "bureau", _bureau);
} else {
handleLaunchError(_bureau, _error, null);
}
}
protected Bureau _bureau;
protected Exception _error;
protected RunQueue _runQueue;
}
protected static class LauncherEntry
{
public Launcher launcher;
public int timeout;
public LauncherEntry (Launcher launcher, int timeout) {
this.launcher = launcher;
this.timeout = timeout;
}
@Override
public String toString () {
return StringUtil.fieldsToString(this);
}
}
protected enum AgentState
{
// Not yet stated, waiting for bureau to ack
PENDING,
// Bureau acked, agent told to start
STARTED,
// Agent ack'ed, now live and hosting, ready to tell other clients
RUNNING,
// Agent destruction requested, waiting for acknowledge (after which the agent is removed
// from the Bureau, so has no state)
DESTROYED,
// Edge case: destroy request prior to RUNNING
STILL_BORN
}
/** Models the results of searching for an agent. */
protected static class FoundAgent
{
FoundAgent (Bureau bureau, AgentObject agent, AgentState state) {
this.bureau = bureau;
this.agent = agent;
this.state = state;
}
// Bureau containing the agent
Bureau bureau;
// The object
AgentObject agent;
// The state of the agent
AgentState state;
}
/** Models a bureau, including the process handle, all running agents and their states. */
protected static class Bureau
{
// non-null once the bureau is scheduled but not yet kicked off
LauncherEntry launcherEntry;
// non-null once the bureau is kicked off
boolean launched;
// The token given to this bureau for authentication
String token;
// The bureau's key in the map of bureaus. All requests for this bureau
// with this id should be associated with one instance
String bureauId;
// The client object of the bureau that has opened a dobj connection to
// the registry
ClientObject clientObj;
// The client session
PresentsSession client;
// The states of the various agents allocated to this bureau
Map<AgentObject, AgentState> agentStates = Maps.newHashMap();
@Override
public String toString () {
StringBuilder builder = new StringBuilder();
builder.append("[Bureau id=").append(bureauId).append(", client=");
if (clientObj == null) {
builder.append("null");
} else {
builder.append(clientObj.getOid());
}
builder.append(", launcherEntry=").append(launcherEntry);
builder.append(", launched=").append(launched);
builder.append(", totalAgents=").append(agentStates.size());
agentSummary(builder.append(", ")).append("]");
return builder.toString();
}
boolean ready () {
return clientObj != null;
}
StringBuilder agentSummary (StringBuilder str) {
int[] counts = new int[AgentState.values().length];
for (Map.Entry<AgentObject, AgentState> me : agentStates.entrySet()) {
counts[me.getValue().ordinal()]++;
}
for (AgentState state : AgentState.values()) {
if (state.ordinal() > 0) {
str.append(", ");
}
str.append(counts[state.ordinal()]).append(" ").append(state.name());
}
return str;
}
void summarize () {
StringBuilder str = new StringBuilder();
str.append("Bureau ").append(bureauId).append(" [");
agentSummary(str).append("]");
log.info(str.toString());
}
void launch () throws IOException {
launcherEntry.launcher.launchBureau(bureauId, token);
}
}
protected static class AgentData
{
Exception launchError;
}
protected Map<String, LauncherEntry> _launchers = Maps.newHashMap();
protected Map<String, Bureau> _bureaus = Maps.newHashMap();
@Inject protected RootDObjectManager _omgr;
@Inject protected @MainInvoker Invoker _invoker;
}
@@ -0,0 +1,60 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.server;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationSender;
import com.threerings.bureau.client.BureauDecoder;
import com.threerings.bureau.client.BureauReceiver;
/**
* Used to issue notifications to a {@link BureauReceiver} instance on a
* client.
*/
public class BureauSender extends InvocationSender
{
/**
* Issues a notification that will result in a call to {@link
* BureauReceiver#createAgent} on a client.
*/
public static void createAgent (
ClientObject target, int arg1)
{
sendNotification(
target, BureauDecoder.RECEIVER_CODE, BureauDecoder.CREATE_AGENT,
new Object[] { Integer.valueOf(arg1) });
}
/**
* Issues a notification that will result in a call to {@link
* BureauReceiver#destroyAgent} on a client.
*/
public static void destroyAgent (
ClientObject target, int arg1)
{
sendNotification(
target, BureauDecoder.RECEIVER_CODE, BureauDecoder.DESTROY_AGENT,
new Object[] { Integer.valueOf(arg1) });
}
}
@@ -0,0 +1,36 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.server;
import com.threerings.presents.server.PresentsSession;
public class BureauSession extends PresentsSession
{
@Override // from PresentsSession
protected void sessionConnectionClosed ()
{
super.sessionConnectionClosed();
// end our session when the connection is closed
endSession();
}
}
@@ -0,0 +1,43 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.util;
import com.threerings.presents.util.PresentsContext;
import com.threerings.bureau.client.BureauDirector;
/**
* Defines the objects held on a bureau client. This includes usual set of objects found on a
* standard presents client.
*/
public interface BureauContext extends PresentsContext
{
/**
* Access the director object.
*/
BureauDirector getBureauDirector ();
/**
* Access the bureau id.
*/
String getBureauId ();
}
@@ -0,0 +1,168 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.bureau.util;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.DateFormat;
import java.util.Date;
import com.samskivert.io.StreamUtil;
import com.samskivert.util.Logger;
import static com.threerings.bureau.Log.log;
/**
* Captures the output of a bureau and redirects it into a single logger instance using a thread
* name equal to the bureau id. The {@link Logger} instance is the one for this class. The intent
* is that log4j will be configured to use %t (thread name) to embed the bureau id.
*/
public class BureauLogRedirector
{
/**
* Creates a new redirector with no size limit.
* @param bureauId the id of the bureau being redirected - this will become the thread name
* @param input the stream that is the output of the bureau process
*/
public BureauLogRedirector (String bureauId, InputStream input)
{
this(bureauId, input, 0);
}
/**
* Creates a new redirector.
* @param bureauId the id of the bureau being redirected - this will become the thread name
* @param input the stream that is the output of the bureau process
* @param limit approximate limit for the total characters written to the logger
*/
public BureauLogRedirector (String bureauId, InputStream input, int limit)
{
_bureauId = bureauId;
_reader = new BufferedReader(new InputStreamReader(input));
_limit = limit;
Thread thread = new Thread(bureauId) {
@Override public void run () {
copyLoop();
}};
thread.setDaemon(true);
thread.start();
}
/**
* Gets the bureau id this was created with.
*/
public String getBureauId ()
{
return _bureauId;
}
/**
* Gets the total number of characters written to the log.
*/
public int getWritten ()
{
return _written;
}
/**
* Gets the character limit associated with the log.
*/
public int getLimit ()
{
return _limit;
}
/**
* Resets the redirector's truncation status and allows additional output up to the given
* character limit.
*/
public synchronized void reset (int limit)
{
_written = 0;
_truncated = false;
_limit = limit;
}
/**
* Tests if this redirector has stopped copying lines due to the size limit being exceeded.
*/
public boolean isTruncated ()
{
return _truncated;
}
/**
* Returns true if the redirector is still active. Normally this indicates that the launched
* process is still running.
*/
public boolean isRunning ()
{
return _reader != null;
}
protected void copyLoop ()
{
String line;
try {
while ((line = _reader.readLine()) != null) {
int length = line.length();
boolean showTrunc = false;
synchronized (this) {
if (_truncated) {
line = null;
} else if (_limit > 0 && _written + length > _limit) {
_truncated = true;
showTrunc = true;
line = null;
}
}
if (line != null) {
_target.info(line); // this should get prefixed by the thread name
_written += length;
} else if (showTrunc) {
DateFormat format =
DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL);
_target.info(
format.format(new Date()) +
": Size limit reached, suppressing further output");
}
}
} catch (Exception e) {
log.warning("Failed to read bureau output", "bureauId", _bureauId, e);
} finally {
StreamUtil.close(_reader);
_reader = null;
}
}
protected String _bureauId;
protected BufferedReader _reader;
protected int _limit;
protected int _written;
protected boolean _truncated;
protected static Logger _target = Logger.getLogger(BureauLogRedirector.class);
}
@@ -0,0 +1,33 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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;
import com.samskivert.util.Logger;
/**
* Contains a reference to the log object used by the Crowd services.
*/
public class Log
{
/** We dispatch our log messages through this logger. */
public static Logger log = Logger.getLogger("com.threerings.crowd");
}
@@ -0,0 +1,38 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.client;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
import com.threerings.crowd.chat.data.ChatChannel;
/**
* Provides a way for clients to speak on chat channels.
*/
public interface ChannelSpeakService extends InvocationService
{
/**
* Requests to speak the supplied message on the specified channel.
*/
public void speak (Client client, ChatChannel channel, String message, byte mode);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,47 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.client;
import com.threerings.crowd.chat.data.ChatMessage;
/**
* A chat display provides a means by which chat messages can be
* displayed. The chat display will be notified when chat messages of
* various sorts have been received by the client.
*/
public interface ChatDisplay
{
/**
* Called to clear the chat display.
*/
void clear ();
/**
* Called to display a chat message.
*
* @param alreadyDisplayed true if a previous chat display in the list has
* already displayed this message, false otherwise.
*
* @return true if the message was displayed, false if not.
*/
boolean displayMessage (ChatMessage msg, boolean alreadyDisplayed);
}
@@ -0,0 +1,41 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.client;
import com.threerings.util.Name;
/**
* Filters messages chat messages to or from the server.
*/
public interface ChatFilter
{
/**
* Filter a chat message.
* @param msg the message text to be filtered.
* @param otherUser an optional argument that represents the target or the speaker, depending
* on 'outgoing', and can be considered in filtering if it is provided.
* @param outgoing true if the message is going out to the server.
*
* @return the filtered message, or null to block it completely.
*/
String filter (String msg, Name otherUser, boolean outgoing);
}
@@ -0,0 +1,77 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.client;
import com.threerings.util.Name;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
/**
* The chat services provide a mechanism by which the client can broadcast chat messages to all
* clients that are subscribed to a particular place object or directly to a particular client.
* These services should not be used directly, but instead should be accessed via the
* {@link ChatDirector}.
*/
public interface ChatService extends InvocationService
{
/**
* Used to communicate the response to a {@link ChatService#tell} request.
*/
public static interface TellListener extends InvocationListener
{
/**
* Communicates the response to a {@link ChatService#tell} request.
*
* @param idleTime the number of ms the tellee has been idle or 0L if they are not idle.
* @param awayMessage the away message configured by the told player or null if they have
* no away message.
*/
void tellSucceeded (long idleTime, String awayMessage);
}
/**
* Requests that a tell message be delivered to the user with username equal to
* <code>target</code>.
*
* @param client a connected, operational client instance.
* @param target the username of the user to which the tell message should be delivered.
* @param message the contents of the message.
* @param listener the reference that will receive the tell response.
*/
void tell (Client client, Name target, String message, TellListener listener);
/**
* Requests that a message be broadcast to all users in the system.
*
* @param client a connected, operational client instance.
* @param message the contents of the message.
* @param listener the reference that will receive a failure response.
*/
void broadcast (Client client, String message, InvocationListener listener);
/**
* Sets this client's away message. If the message is null or the empty string, the away
* message will be cleared.
*/
void away (Client client, String message);
}
@@ -0,0 +1,253 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.client;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.samskivert.util.RandomUtil;
import com.threerings.util.Name;
import static com.threerings.crowd.Log.log;
/**
* A chat filter that can filter out curse words from user chat.
*/
public abstract class CurseFilter implements ChatFilter
{
/** Indicates how messages should be handled. */
public enum Mode { DROP, COMIC, VERNACULAR, UNFILTERED; }
/**
* Creates a curse filter. The curse words should be a string in the following format:
*
* <pre>
* *penis*=John_Thomas shit*=barnacle muff=britches
* </pre>
*
* The key/value pairs are separated by spaces, * matches word characters and the value after
* the = is the string into which to convert the text when converting to the vernacular.
* Underscores in the target string will be turned into spaces.
*
* <p> And stopWords should be in the following format:
*
* <pre>
* *faggot* rape rapes raped raping
* </pre>
*
* Words are separated by spaces and * matches any other word characters.
*/
public CurseFilter (String curseWords, String stopWords)
{
configureCurseWords(curseWords);
configureStopWords(stopWords);
}
/**
* The client will need to provide a way to look up our current chat filter mode.
*/
public abstract Mode getFilterMode ();
// from interface ChatFilter
public String filter (String msg, Name otherUser, boolean outgoing)
{
// first, check against the drop-always list
_stopMatcher.reset(msg);
if (_stopMatcher.find()) {
return null;
}
// then see what kind of curse filtering the user has configured
Mode level = getFilterMode();
if (level == Mode.UNFILTERED) {
return msg;
}
StringBuffer inbuf = new StringBuffer(msg);
StringBuffer outbuf = new StringBuffer(msg.length());
for (int ii=0, nn=_matchers.length; ii < nn; ii++) {
Matcher m = _matchers[ii];
m.reset(inbuf);
while (m.find()) {
switch (level) {
case DROP:
return null;
case COMIC:
m.appendReplacement(outbuf,
_replacements[ii].replace(" ", comicChars(_comicLength[ii])));
break;
case VERNACULAR:
String vernacular = _vernacular[ii];
if (Character.isUpperCase(m.group(2).codePointAt(0))) {
int firstCharLen = Character.charCount(vernacular.codePointAt(0));
vernacular = vernacular.substring(0, firstCharLen).toUpperCase() +
vernacular.substring(firstCharLen);
}
m.appendReplacement(outbuf, _replacements[ii].replace(" ", vernacular));
break;
case UNFILTERED:
// We returned the msg unadulterated above in this case, so it should be
// impossible to wind up here, but let's enumerate it so we can let the compiler
// scream about missing enum values in a switch
log.warning("Omg? We're trying to filter chat even though we're unfiltered?");
break;
}
}
if (outbuf.length() == 0) {
// optimization: if we didn't find a match, jump to the next
// pattern without doing any StringBuilder jimmying
continue;
}
m.appendTail(outbuf);
// swap the buffers around and clear the output
StringBuffer temp = inbuf;
inbuf = outbuf;
outbuf = temp;
outbuf.setLength(0);
}
return inbuf.toString();
}
/**
* Configure the curse word portion of our filtering.
*/
protected void configureCurseWords (String curseWords)
{
StringTokenizer st = new StringTokenizer(curseWords);
int numWords = st.countTokens();
_matchers = new Matcher[numWords];
_replacements = new String[numWords];
_vernacular = new String[numWords];
_comicLength = new int[numWords];
for (int ii=0; ii < numWords; ii++) {
String mapping = st.nextToken();
StringTokenizer st2 = new StringTokenizer(mapping, "=");
if (st2.countTokens() != 2) {
log.warning("Something looks wrong in the x.cursewords properties (" +
mapping + "), skipping.");
continue;
}
String curse = st2.nextToken();
String s = "";
String p = "";
if (curse.startsWith("*")) {
curse = curse.substring(1);
p += "([a-zA-Z]*)";
s += "$1";
} else {
p += "()";
}
s += " ";
p += " ";
if (curse.endsWith("*")) {
curse = curse.substring(0, curse.length() - 1);
p += "([a-zA-Z]*)";
s += "$3";
}
String pattern = "\\b" + p.replace(" ", "(" + curse + ")") + "\\b";
Pattern pat = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
_matchers[ii] = pat.matcher("");
_replacements[ii] = s;
_vernacular[ii] = st2.nextToken().replace('_', ' ');
_comicLength[ii] = curse.codePointCount(0, curse.length());
}
}
/**
* Configure the words that will stop.
*/
protected void configureStopWords (String stopWords)
{
StringTokenizer st = new StringTokenizer(stopWords);
String pattern = "";
while (st.hasMoreTokens()) {
if ("".equals(pattern)) {
pattern += "(";
} else {
pattern += "|";
}
pattern += getStopWordRegexp(st.nextToken());
}
pattern += ")";
setStopPattern(pattern);
}
/**
* Sets our stop word matcher to one for the given regular expression.
*/
protected void setStopPattern (String pattern)
{
_stopMatcher = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE).matcher("");
}
/**
* Turns a naughty word into a regular expression to catch it.
*/
protected String getStopWordRegexp (String word)
{
return "\\b" + word.replace("*", "[A-Za-z]*") + "\\b";
}
/**
* Return a comicy replacement of the specified length.
*/
protected String comicChars (int length)
{
StringBuilder buf = new StringBuilder();
for (int ii=0; ii < length; ii++) {
buf.append(RandomUtil.pickRandom(COMIC_CHARS));
}
return buf.toString();
}
/** A matcher that will always cause a message to be dropped if it matches. */
protected Matcher _stopMatcher;
/** Matchers for each curseword. */
protected Matcher[] _matchers;
/** Length of comic-y replacements for each curseword. */
protected int[] _comicLength;
/** Replacements. */
protected String[] _replacements;
/** Replacements for each curseword "in the vernacular". */
protected String[] _vernacular;
/** Comic replacement characters. */
protected static final String[] COMIC_CHARS = { "!", "@", "#", "%", "&", "*" };
}
@@ -0,0 +1,127 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.client;
import java.util.ArrayList;
import com.samskivert.util.ObserverList;
import com.threerings.crowd.chat.data.ChatMessage;
/**
* Stores chat history.
*/
public class HistoryList extends ArrayList<ChatMessage>
implements ChatDisplay
{
/** An interface for chat history observers. */
public interface Observer {
/** Called when messages have been added or removed from the chat history.
* @param adjustment the number of messages that have been added (+) or removed (-). */
void historyUpdated (int adjustment);
}
// documentation inherited from interface
public boolean displayMessage (ChatMessage msg, boolean alreadyDisplayed)
{
// see if we're full, and if so, clear out a bunch of old stuff
int adjusted;
if (size() == MAX_HISTORY) {
removeRange(0, PRUNE_HISTORY);
adjusted = PRUNE_HISTORY;
} else {
adjusted = 0;
}
// add the message to the history
add(msg);
// notify observers that something changed
notify(adjusted);
return true;
}
@Override
public void clear ()
{
// see how many entries we're clearing out..
int adjusted = size();
super.clear();
// and notify the chat displays of that fact
notify(adjusted);
}
/**
* Adds an {@link Observer} that wants to know about changes to the history.
*/
public void addObserver (Observer obs)
{
_obs.add(obs);
}
/**
* Removes a {@link Observer} from hearing about changes to the history.
*/
public void removeObserver (Observer obs)
{
_obs.remove(obs);
}
/**
* Notifies listening {@link Observer}s that there has been a change to this history.
*/
protected void notify (int adjustment)
{
_historyUpdatedOp.setAdjustment(adjustment);
_obs.apply(_historyUpdatedOp);
}
protected static class HistoryUpdatedOp
implements ObserverList.ObserverOp<Observer>
{
public void setAdjustment (int adjustment) {
_adjustment = adjustment;
}
public boolean apply (Observer obs) {
obs.historyUpdated(_adjustment);
return true;
}
protected int _adjustment;
}
/** A list of {@link Observer}s interested in history changes. */
protected ObserverList<Observer> _obs = ObserverList.newFastUnsafe();
/** An operation used to notify observers of history updates. */
protected HistoryList.HistoryUpdatedOp _historyUpdatedOp = new HistoryUpdatedOp();
/** The maximum number of history entries we'll keep. */
protected static final int MAX_HISTORY = 2000;
/** The number of history entries we'll prune when we hit the max. */
protected static final int PRUNE_HISTORY = 200;
}
@@ -0,0 +1,193 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.client;
import java.util.Collections;
import java.util.HashSet;
import com.google.common.collect.Sets;
import com.samskivert.util.ObserverList;
import com.threerings.util.MessageBundle;
import com.threerings.util.Name;
import com.threerings.presents.client.BasicDirector;
import com.threerings.crowd.util.CrowdContext;
/**
* Manages the mutelist.
*
* TODO: This class right now is pretty much just a placeholder.
*/
public class MuteDirector extends BasicDirector
implements ChatFilter
{
/**
* An interface that can be registered with the MuteDirector to
* receive notifications to the mutelist.
*/
public static interface MuteObserver
{
/**
* The specified player was added or removed from the mutelist.
*/
void muteChanged (Name playername, boolean nowMuted);
}
/**
* Should be instantiated after the ChatDirector.
*/
public MuteDirector (CrowdContext ctx)
{
super(ctx);
}
/**
* Set up the mute director with the specified list of initial mutees.
*/
public MuteDirector (CrowdContext ctx, Name[] list)
{
this(ctx);
Collections.addAll(_mutelist, list);
}
/**
* Called to shut down the mute director.
*/
public void shutdown ()
{
if (_chatdir != null) {
_chatdir.removeChatFilter(this);
_chatdir = null;
}
}
/**
* Set the required ChatDirector.
*/
public void setChatDirector (ChatDirector chatdir)
{
if (_chatdir == null) {
_chatdir = chatdir;
_chatdir.addChatFilter(this);
}
}
/**
* Add the specified mutelist observer.
*/
public void addMuteObserver (MuteObserver obs)
{
_observers.add(obs);
}
/**
* Remove the specified mutelist observer.
*/
public void removeMuteObserver (MuteObserver obs)
{
_observers.remove(obs);
}
/**
* Check to see if the specified user is muted.
*/
public boolean isMuted (Name username)
{
return _mutelist.contains(username);
}
/**
* Mute or unmute the specified user.
*/
public void setMuted (Name username, boolean mute)
{
boolean changed = mute ? _mutelist.add(username) : _mutelist.remove(username);
String feedback;
if (mute) {
feedback = "m.muted";
} else {
feedback = changed ? "m.unmuted" : "m.notmuted";
}
// always give some feedback to the user
_chatdir.displayFeedback(null, MessageBundle.tcompose(feedback, username));
// if the mutelist actually changed, notify observers
if (changed) {
notifyObservers(username, mute);
}
}
/**
* @return a list of the currently muted players.
*
* This list may be out of date immediately upon returning from this method.
*/
public Name[] getMuted ()
{
return _mutelist.toArray(new Name[_mutelist.size()]);
}
// documentation inherited from interface ChatFilter
public String filter (String msg, Name otherUser, boolean outgoing)
{
// we are only concerned with filtering things going to or coming
// from muted users
if ((otherUser != null) && isMuted(otherUser)) {
// if it was outgoing, explain the dropped message, otherwise
// silently drop
if (outgoing) {
_chatdir.displayFeedback(null, "m.no_tell_mute");
}
return null;
}
return msg;
}
/**
* Notify our observers of a change in the mutelist.
*/
protected void notifyObservers (final Name username, final boolean muted)
{
_observers.apply(new ObserverList.ObserverOp<MuteObserver>() {
public boolean apply (MuteObserver observer) {
observer.muteChanged(username, muted);
return true;
}
});
}
/** The chat director that we're working hard for. */
protected ChatDirector _chatdir;
/** The mutelist. */
protected HashSet<Name> _mutelist = Sets.newHashSet();
/** List of mutelist observers. */
protected ObserverList<MuteObserver> _observers =
new ObserverList<MuteObserver>(ObserverList.FAST_UNSAFE_NOTIFY);
}
@@ -0,0 +1,45 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.client;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
/**
* Provides a means by which "speaking" can be allowed among subscribers
* of a particular distributed object.
*/
public interface SpeakService extends InvocationService
{
/**
* Issues a request to speak "on" the distributed object via which
* this speak service was provided.
*
* @param message the message to be spoken.
* @param mode the "mode" of the message. This is an opaque value that
* will be passed back down via the {@link ChatDirector} to the {@link
* ChatDisplay} implementations which can interpret it in an
* application specific manner. It's useful for differentiating
* between regular speech, emotes, etc.
*/
void speak (Client client, String message, byte mode);
}
@@ -0,0 +1,52 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.data;
import javax.annotation.Generated;
import com.threerings.crowd.chat.client.ChannelSpeakService;
import com.threerings.presents.client.Client;
import com.threerings.presents.data.InvocationMarshaller;
/**
* Provides the implementation of the {@link ChannelSpeakService} interface
* that marshalls the arguments and delivers the request to the provider
* on the server. Also provides an implementation of the response listener
* interfaces that marshall the response arguments and deliver them back
* to the requesting client.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from ChannelSpeakService.java.")
public class ChannelSpeakMarshaller extends InvocationMarshaller
implements ChannelSpeakService
{
/** The method id used to dispatch {@link #speak} requests. */
public static final int SPEAK = 1;
// from interface ChannelSpeakService
public void speak (Client arg1, ChatChannel arg2, String arg3, byte arg4)
{
sendRequest(arg1, SPEAK, new Object[] {
arg2, arg3, Byte.valueOf(arg4)
});
}
}
@@ -0,0 +1,57 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.data;
import com.threerings.io.SimpleStreamableObject;
import com.threerings.presents.dobj.DSet;
/**
* Represents a chat channel.
*/
public abstract class ChatChannel extends SimpleStreamableObject
implements Comparable<ChatChannel>, DSet.Entry
{
// from interface Comparable<ChatChannel>
public abstract int compareTo (ChatChannel other);
/**
* Converts this channel into a unique name that can be used as the name of the distributed
* lock used when resolving the channel.
*/
public abstract String getLockName ();
// from interface DSet.Entry
public Comparable<?> getKey ()
{
return this;
}
@Override
public boolean equals (Object other)
{
return compareTo((ChatChannel)other) == 0;
}
@Override
public abstract int hashCode ();
}
@@ -0,0 +1,96 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.data;
import com.threerings.presents.data.InvocationCodes;
import com.threerings.presents.data.Permission;
import com.threerings.crowd.chat.client.ChatDirector;
import com.threerings.crowd.chat.client.SpeakService;
/**
* Contains codes used by the chat invocation services.
*/
public interface ChatCodes extends InvocationCodes
{
/** A return value used by the ChatDirector and possibly other entities to indicate successful
* processing of chat. */
public static final String SUCCESS = "success";
/** The message identifier for a chat notification message. */
public static final String CHAT_NOTIFICATION = "crowd.chat";
/** The message identifier for a chat channel notification message. */
public static final String CHAT_CHANNEL_NOTIFICATION = "crowd.chat.channel";
/** The access control identifier for normal chat privileges. */
public static final Permission CHAT_ACCESS = new Permission();
/** The access control identifier for broadcast chat privileges. */
public static final Permission BROADCAST_ACCESS = new Permission();
/** The configuration key for idle time. */
public static final String IDLE_TIME_KEY = "narya.chat.idle_time";
/** The default time after which a player is assumed idle. */
public static final long DEFAULT_IDLE_TIME = 3 * 60 * 1000L;
/** The chat localtype code for chat messages delivered on the place object currently occupied
* by the client. This is the only type of chat message that will be delivered unless the chat
* director is explicitly provided with other chat message sources via {@link
* ChatDirector#addAuxiliarySource}. */
public static final String PLACE_CHAT_TYPE = "placeChat";
/** The chat localtype for messages received on the user object. */
public static final String USER_CHAT_TYPE = "userChat";
/** The default mode used by {@link SpeakService#speak} requests. */
public static final byte DEFAULT_MODE = 0;
/** A {@link SpeakService#speak} mode to indicate that the user is thinking what they're
* saying, or is it that they're saying what they're thinking? */
public static final byte THINK_MODE = 1;
/** A {@link SpeakService#speak} mode to indicate that a speak is actually an emote. */
public static final byte EMOTE_MODE = 2;
/** A {@link SpeakService#speak} mode to indicate that a speak is actually a shout. */
public static final byte SHOUT_MODE = 3;
/** A {@link SpeakService#speak} mode to indicate that a speak is actually a server-wide
* broadcast. */
public static final byte BROADCAST_MODE = 4;
/** The last chat mode defined in the interface. */
public static final byte LAST_MODE = BROADCAST_MODE;
/** String translations for the various chat modes. */
public static final String[] XLATE_MODES = {
"default", "think", "emote", "shout", "broadcast"
};
/** An error code delivered when the user targeted for a tell notification is not online. */
public static final String USER_NOT_ONLINE = "m.user_not_online";
/** An error code delivered when the user targeted for a tell notification is disconnected. */
public static final String USER_DISCONNECTED = "m.user_disconnected";
}
@@ -0,0 +1,116 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.data;
import javax.annotation.Generated;
import com.threerings.crowd.chat.client.ChatService;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
import com.threerings.presents.data.InvocationMarshaller;
import com.threerings.presents.dobj.InvocationResponseEvent;
import com.threerings.util.Name;
/**
* Provides the implementation of the {@link ChatService} interface
* that marshalls the arguments and delivers the request to the provider
* on the server. Also provides an implementation of the response listener
* interfaces that marshall the response arguments and deliver them back
* to the requesting client.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from ChatService.java.")
public class ChatMarshaller extends InvocationMarshaller
implements ChatService
{
/**
* Marshalls results to implementations of {@link ChatService.TellListener}.
*/
public static class TellMarshaller extends ListenerMarshaller
implements TellListener
{
/** The method id used to dispatch {@link #tellSucceeded}
* responses. */
public static final int TELL_SUCCEEDED = 1;
// from interface TellMarshaller
public void tellSucceeded (long arg1, String arg2)
{
_invId = null;
omgr.postEvent(new InvocationResponseEvent(
callerOid, requestId, TELL_SUCCEEDED,
new Object[] { Long.valueOf(arg1), arg2 }, transport));
}
@Override // from InvocationMarshaller
public void dispatchResponse (int methodId, Object[] args)
{
switch (methodId) {
case TELL_SUCCEEDED:
((TellListener)listener).tellSucceeded(
((Long)args[0]).longValue(), (String)args[1]);
return;
default:
super.dispatchResponse(methodId, args);
return;
}
}
}
/** The method id used to dispatch {@link #away} requests. */
public static final int AWAY = 1;
// from interface ChatService
public void away (Client arg1, String arg2)
{
sendRequest(arg1, AWAY, new Object[] {
arg2
});
}
/** The method id used to dispatch {@link #broadcast} requests. */
public static final int BROADCAST = 2;
// from interface ChatService
public void broadcast (Client arg1, String arg2, InvocationService.InvocationListener arg3)
{
ListenerMarshaller listener3 = new ListenerMarshaller();
listener3.listener = arg3;
sendRequest(arg1, BROADCAST, new Object[] {
arg2, listener3
});
}
/** The method id used to dispatch {@link #tell} requests. */
public static final int TELL = 3;
// from interface ChatService
public void tell (Client arg1, Name arg2, String arg3, ChatService.TellListener arg4)
{
ChatMarshaller.TellMarshaller listener4 = new ChatMarshaller.TellMarshaller();
listener4.listener = arg4;
sendRequest(arg1, TELL, new Object[] {
arg2, arg3, listener4
});
}
}
@@ -0,0 +1,91 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.data;
import com.samskivert.util.StringUtil;
import com.threerings.io.Streamable;
import com.threerings.util.ActionScript;
/**
* The abstract base class of all the client-side ChatMessage objects.
*/
public abstract class ChatMessage
implements Streamable
{
/** The actual text of the message. */
public String message;
/** The bundle to use when translating this message. */
public String bundle;
/** The client side 'localtype' of this chat, set to the type registered with an auxiliary
* source in the ChatDirector. */
public transient String localtype;
/** The client time that this message was created. */
@ActionScript(type="int")
public transient long timestamp;
/**
* For all your unserialization needs.
*/
public ChatMessage ()
{
}
/**
* Construct a ChatMessage.
*/
public ChatMessage (String message, String bundle)
{
this.message = message;
this.bundle = bundle;
}
/**
* Once this message reaches the client, the information contained within is changed around a
* bit.
*/
public void setClientInfo (String msg, String ltype)
{
message = msg;
localtype = ltype;
bundle = null;
timestamp = System.currentTimeMillis();
}
/**
* Get the appropriate message format for this message.
*/
public String getFormat ()
{
return null;
}
@Override
public String toString ()
{
return StringUtil.shortClassName(this) + StringUtil.fieldsToString(this);
}
}
@@ -0,0 +1,33 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.data;
import com.threerings.crowd.chat.server.SpeakUtil;
import com.threerings.util.Name;
/**
* Marks a {@link Name} as disinterested in chat history such that {@link SpeakUtil} will keep no
* messages sent to it.
*/
public interface KeepNoHistory
{
}
@@ -0,0 +1,52 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.data;
import javax.annotation.Generated;
import com.threerings.crowd.chat.client.SpeakService;
import com.threerings.presents.client.Client;
import com.threerings.presents.data.InvocationMarshaller;
/**
* Provides the implementation of the {@link SpeakService} interface
* that marshalls the arguments and delivers the request to the provider
* on the server. Also provides an implementation of the response listener
* interfaces that marshall the response arguments and deliver them back
* to the requesting client.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from SpeakService.java.")
public class SpeakMarshaller extends InvocationMarshaller
implements SpeakService
{
/** The method id used to dispatch {@link #speak} requests. */
public static final int SPEAK = 1;
// from interface SpeakService
public void speak (Client arg1, String arg2, byte arg3)
{
sendRequest(arg1, SPEAK, new Object[] {
arg2, Byte.valueOf(arg3)
});
}
}
@@ -0,0 +1,47 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.data;
import com.threerings.util.Name;
/**
* Provides a mechanism by which the speak service can identify chat listeners so as to maintain a
* recent history of all chat traffic on the server.
*/
public interface SpeakObject
{
/** Used in conjunction with {@link SpeakObject#applyToListeners}. */
public static interface ListenerOp
{
/** Call this method if you only have access to body oids. */
void apply (int bodyOid);
/** Call this method if you can provide usernames directly. */
void apply (Name username);
}
/**
* The speak service will call this every time a chat message is delivered on this speak object
* to note the listeners that received the message.
*/
void applyToListeners (ListenerOp op);
}
@@ -0,0 +1,56 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.data;
/**
* A ChatMessage that represents a message that came from the server and did not result from direct
* user action.
*/
public class SystemMessage extends ChatMessage
{
/** Attention level constant to indicate that this message is merely providing the user with
* information. */
public static final byte INFO = 0;
/** Attention level constant to indicate that this message is the result of a user action. */
public static final byte FEEDBACK = 1;
/** Attention level constant to indicate that some action is required. */
public static final byte ATTENTION = 2;
/** The attention level of this message. */
public byte attentionLevel;
// documentation inherited
public SystemMessage ()
{
}
/**
* Construct a SystemMessage.
*/
public SystemMessage (String message, String bundle, byte attentionLevel)
{
super(message, bundle);
this.attentionLevel = attentionLevel;
}
}
@@ -0,0 +1,55 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.data;
import com.threerings.util.Name;
/**
* A feedback message to indicate that a tell succeeded.
*/
public class TellFeedbackMessage extends UserMessage
{
/**
* A tell feedback message is only composed on the client.
*/
public TellFeedbackMessage (Name target, String message, boolean failure)
{
super(target, null, message, ChatCodes.DEFAULT_MODE);
_failure = failure;
}
/**
* Returns true if this is a failure feedback, false if it is successful tell feedback.
*/
public boolean isFailure ()
{
return _failure;
}
@Override
public String getFormat ()
{
return _failure ? null : "m.told_format";
}
protected boolean _failure;
}
@@ -0,0 +1,90 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.data;
import com.threerings.util.Name;
/**
* A ChatMessage representing a message that came from another user.
*/
public class UserMessage extends ChatMessage
{
/** The user that the message came from. */
public Name speaker;
/** The mode of the message. @see ChatCodes.DEFAULT_MODE */
public byte mode;
/**
* For unserialization.
*/
public UserMessage ()
{
}
/**
* Construct a user message.
*/
public UserMessage (Name speaker, String bundle, String message, byte mode)
{
super(message, bundle);
this.speaker = speaker;
this.mode = mode;
}
/**
* Constructs a user message for a player originated tell (which has no bundle and is in the
* default mode).
*/
public UserMessage (Name speaker, String message)
{
super(message, null);
this.speaker = speaker;
this.mode = ChatCodes.DEFAULT_MODE;
}
/**
* Returns the name to display for the speaker. Some types of messages may wish to not use the
* canonical name for the speaker and should thus override this function.
*/
public Name getSpeakerDisplayName ()
{
return speaker;
}
@Override
public String getFormat ()
{
switch (mode) {
case ChatCodes.THINK_MODE: return "m.think_format";
case ChatCodes.EMOTE_MODE: return "m.emote_format";
case ChatCodes.SHOUT_MODE: return "m.shout_format";
case ChatCodes.BROADCAST_MODE: return "m.broadcast_format";
default: // fall through
}
if (ChatCodes.USER_CHAT_TYPE.equals(localtype)) {
return "m.tell_format";
}
return "m.speak_format";
}
}
@@ -0,0 +1,56 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.data;
import com.threerings.util.Name;
/**
* A system message triggered by the activity of another user. If the user is muted we can suppress
* this message, unlike a normal system message.
*/
public class UserSystemMessage extends SystemMessage
{
/** The "speaker" of this message, the user that triggered that this message be sent to us. */
public Name speaker;
/** Suitable for unserialization. */
public UserSystemMessage ()
{
}
/**
* Construct a INFO-level UserSystemMessage.
*/
public UserSystemMessage (Name sender, String message, String bundle)
{
this(sender, message, bundle, INFO);
}
/**
* Construct a UserSystemMessage.
*/
public UserSystemMessage (Name sender, String message, String bundle, byte attentionLevel)
{
super(message, bundle, attentionLevel);
this.speaker = sender;
}
}
@@ -0,0 +1,71 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.server;
import javax.annotation.Generated;
import com.threerings.crowd.chat.data.ChannelSpeakMarshaller;
import com.threerings.crowd.chat.data.ChatChannel;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationDispatcher;
import com.threerings.presents.server.InvocationException;
/**
* Dispatches requests to the {@link ChannelSpeakProvider}.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from ChannelSpeakService.java.")
public class ChannelSpeakDispatcher extends InvocationDispatcher<ChannelSpeakMarshaller>
{
/**
* Creates a dispatcher that may be registered to dispatch invocation
* service requests for the specified provider.
*/
public ChannelSpeakDispatcher (ChannelSpeakProvider provider)
{
this.provider = provider;
}
@Override
public ChannelSpeakMarshaller createMarshaller ()
{
return new ChannelSpeakMarshaller();
}
@Override
public void dispatchRequest (
ClientObject source, int methodId, Object[] args)
throws InvocationException
{
switch (methodId) {
case ChannelSpeakMarshaller.SPEAK:
((ChannelSpeakProvider)provider).speak(
source, (ChatChannel)args[0], (String)args[1], ((Byte)args[2]).byteValue()
);
return;
default:
super.dispatchRequest(source, methodId, args);
return;
}
}
}
@@ -0,0 +1,42 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.server;
import javax.annotation.Generated;
import com.threerings.crowd.chat.client.ChannelSpeakService;
import com.threerings.crowd.chat.data.ChatChannel;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationProvider;
/**
* Defines the server-side of the {@link ChannelSpeakService}.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from ChannelSpeakService.java.")
public interface ChannelSpeakProvider extends InvocationProvider
{
/**
* Handles a {@link ChannelSpeakService#speak} request.
*/
void speak (ClientObject caller, ChatChannel arg1, String arg2, byte arg3);
}
@@ -0,0 +1,503 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.server;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.primitives.Longs;
import com.google.inject.Inject;
import com.samskivert.util.ArrayIntSet;
import com.samskivert.util.ResultListener;
import com.threerings.util.Name;
import com.threerings.presents.annotation.AnyThread;
import com.threerings.presents.client.InvocationService;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationException;
import com.threerings.presents.server.InvocationManager;
import com.threerings.presents.server.PresentsDObjectMgr;
import com.threerings.presents.peer.data.ClientInfo;
import com.threerings.presents.peer.data.NodeObject;
import com.threerings.presents.peer.server.PeerManager;
import com.threerings.presents.peer.server.PeerManager.NodeRequest;
import com.threerings.presents.peer.server.NodeRequestsListener;
import com.threerings.crowd.chat.data.ChatChannel;
import com.threerings.crowd.chat.data.ChatCodes;
import com.threerings.crowd.chat.data.UserMessage;
import com.threerings.crowd.chat.server.SpeakUtil.ChatHistoryEntry;
import com.threerings.crowd.data.BodyObject;
import com.threerings.crowd.data.CrowdCodes;
import com.threerings.crowd.peer.data.CrowdClientInfo;
import com.threerings.crowd.peer.data.CrowdNodeObject;
import com.threerings.crowd.peer.server.CrowdPeerManager;
import static com.threerings.crowd.Log.log;
/**
* Handles chat channel services.
*/
public abstract class ChatChannelManager
implements ChannelSpeakProvider
{
/**
* Value asynchronously returned by {@link #collectChatHistory} after polling all peer nodes.
*/
public static class ChatHistoryResult
{
/** The set of nodes that either did not reply within the timeout, or had a failure. */
public Set<String> failedNodes;
/** The things in the user's chat history, aggregated from all nodes and sorted by
* timestamp. */
public List<ChatHistoryEntry> history;
}
/**
* When a body becomes a member of a channel, this method should be called so that any server
* that happens to be hosting that channel can be told that the body in question is now a
* participant.
*/
@AnyThread
public void bodyAddedToChannel (ChatChannel channel, int bodyId)
{
_peerMan.invokeNodeAction(new ParticipantChanged(channel, bodyId, true));
}
/**
* When a body loses channel membership, this method should be called so that any server that
* happens to be hosting that channel can be told that the body in question is now a
* participant.
*/
@AnyThread
public void bodyRemovedFromChannel (ChatChannel channel, int bodyId)
{
_peerMan.invokeNodeAction(new ParticipantChanged(channel, bodyId, false));
}
/**
* Collects all chat messages heard by the given user on all peers.
*/
@AnyThread
public void collectChatHistory (Name user, final ResultListener<ChatHistoryResult> lner)
{
NodeRequestsListener<List<ChatHistoryEntry>> listener =
new NodeRequestsListener<List<ChatHistoryEntry>>() {
public void requestsProcessed (NodeRequestsResult<List<ChatHistoryEntry>> rRes) {
ChatHistoryResult chRes = new ChatHistoryResult();
chRes.failedNodes = rRes.getNodeErrors().keySet();
chRes.history = Lists.newArrayList(
Iterables.concat(rRes.getNodeResults().values()));
Collections.sort(chRes.history, SORT_BY_TIMESTAMP);
lner.requestCompleted(chRes);
}
public void requestFailed (String cause) {
lner.requestFailed(new InvocationException(cause));
}
};
_peerMan.invokeNodeRequest(new ChatCollectionRequest(user), listener);
}
// from interface ChannelSpeakProvider
public void speak (ClientObject caller, final ChatChannel channel, String message, byte mode)
{
final UserMessage umsg = new UserMessage(
((BodyObject)caller).getVisibleName(), null, message, mode);
// if we're hosting this channel, dispatch it directly
if (_channels.containsKey(channel)) {
dispatchSpeak(channel, umsg);
return;
}
// if we're resolving this channel, queue up our message for momentary deliver
List<UserMessage> msgs = _resolving.get(channel);
if (msgs != null) {
msgs.add(umsg);
return;
}
// forward the speak request to the server that hosts the channel in question
_peerMan.invokeNodeAction(new ForwardChannelSpeak(channel, umsg), new Runnable() {
public void run () {
_resolving.put(channel, Lists.newArrayList(umsg));
resolveAndDispatch(channel);
}
});
}
/**
* Creates our singleton manager and registers our invocation service.
*/
@Inject protected ChatChannelManager (PresentsDObjectMgr omgr, InvocationManager invmgr)
{
invmgr.registerDispatcher(new ChannelSpeakDispatcher(this), CrowdCodes.CROWD_GROUP);
// create and start our idle channel closer; this will run as long as omgr is alive
omgr.newInterval(new Runnable() {
public void run () {
closeIdleChannels();
}
}).schedule(IDLE_CHANNEL_CHECK_PERIOD, true);
}
/**
* Resolves the channel specified in the supplied action and then dispatches it.
*/
protected void resolveAndDispatch (final ChatChannel channel)
{
NodeObject.Lock lock = new NodeObject.Lock("ChatChannel", channel.getLockName());
_peerMan.performWithLock(lock, new PeerManager.LockedOperation() {
public void run () {
((CrowdNodeObject)_peerMan.getNodeObject()).addToHostedChannels(channel);
finishResolveAndDispatch(channel);
}
public void fail (String peerName) {
List<UserMessage> msgs = _resolving.remove(channel);
if (peerName == null) {
log.warning("Failed to resolve chat channel due to lock failure",
"channel", channel);
} else {
// some other peer resolved this channel first, so forward any queued messages
// directly to that node
for (UserMessage msg : msgs) {
_peerMan.invokeNodeAction(peerName, new ForwardChannelSpeak(channel, msg));
}
}
}
});
}
/**
* Resolves the participant set for the specified chat channel and dispatches all pending
* messages to the channel. End users of the chat channel system should override this method
* and do what is necessary to resolve the channel's participant set and call {@link
* #resolutionComplete} or {@link #resolutionFailed}.
*/
protected void finishResolveAndDispatch (ChatChannel channel)
{
resolutionComplete(channel, new ArrayIntSet());
}
/**
* This should be called when a channel's participant set has been resolved.
*/
protected void resolutionComplete (ChatChannel channel, Set<Integer> parts)
{
// map the participants of our now resolved channel
ChannelInfo info = new ChannelInfo();
info.channel = channel;
info.participants = parts;
_channels.put(channel, info);
// dispatch any pending messages now that we know where they go
for (UserMessage msg : _resolving.remove(channel)) {
dispatchSpeak(channel, msg);
}
}
/**
* This should be called if channel resolution fails.
*/
protected void resolutionFailed (ChatChannel channel, Exception cause)
{
log.warning("Failed to resolve chat channel", "channel", channel, cause);
// alas, we just drop all pending messages because we're hosed
_resolving.remove(channel);
}
/**
* Requests that we dispatch the supplied message to all participants of the specified chat
* channel. The speaker will be validated prior to dispatching the message as the originating
* server does not have the information it needs to validate the speaker and must leave that to
* us, the channel hosting server.
*/
protected void dispatchSpeak (ChatChannel channel, UserMessage message)
{
final ChannelInfo info = _channels.get(channel);
if (info == null) {
// TODO: maybe we should just reresolve the channel...
log.warning("Requested to dispatch speak on unhosted channel", "channel", channel,
"msg", message);
return;
}
// validate the speaker
if (!info.participants.contains(getBodyId(message.speaker))) {
log.warning("Dropping channel chat message from non-speaker", "channel", channel,
"message", message);
return;
}
// note that we're dispatching a message on this channel
info.lastMessage = System.currentTimeMillis();
// generate a mapping from node name to an array of body ids for the participants that are
// currently on the node in question
final Map<String,int[]> partMap = Maps.newHashMap();
for (NodeObject nodeobj : _peerMan.getNodeObjects()) {
ArrayIntSet nodeBodyIds = new ArrayIntSet();
for (ClientInfo clinfo : nodeobj.clients) {
int bodyId = getBodyId(((CrowdClientInfo)clinfo).visibleName);
if (info.participants.contains(bodyId)) {
nodeBodyIds.add(bodyId);
}
}
partMap.put(nodeobj.nodeName, nodeBodyIds.toIntArray());
}
for (Map.Entry<String,int[]> entry : partMap.entrySet()) {
_peerMan.invokeNodeAction(
entry.getKey(), new DispatchChannelSpeak(channel, message, entry.getValue()));
}
}
/**
* Delivers the supplied chat channel message to the specified bodies.
*/
protected void deliverSpeak (ChatChannel channel, UserMessage message, int[] bodyIds)
{
channel = intern(channel);
for (int bodyId : bodyIds) {
BodyObject bobj = getBodyObject(bodyId);
if (bobj != null && shouldDeliverSpeak(channel, message, bobj)) {
SpeakUtil.recordToChatHistory(channel, message, bobj.getVisibleName());
bobj.postMessage(ChatCodes.CHAT_CHANNEL_NOTIFICATION, channel, message);
}
}
}
/**
* Called periodically to check for and close any channels that have been idle too long.
*/
protected void closeIdleChannels ()
{
long now = System.currentTimeMillis();
Iterator<Map.Entry<ChatChannel, ChannelInfo>> iter = _channels.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<ChatChannel, ChannelInfo> entry = iter.next();
if (now - entry.getValue().lastMessage > IDLE_CHANNEL_CLOSE_TIME) {
((CrowdNodeObject)_peerMan.getNodeObject()).removeFromHostedChannels(
entry.getKey());
iter.remove();
}
}
}
/**
* Ratifies the delivery of the supplied chat channel message to the specified body. Derived
* classes can override this method to implement channel disabling, mute lists or any other
* suppression they might need.
*/
protected boolean shouldDeliverSpeak (ChatChannel channel, UserMessage message, BodyObject body)
{
return true;
}
/**
* Returns a widely referenced instance equivalent to the given channel, if one is available.
* This reduces memory usage since clients send new channel instances with each message.
*/
protected ChatChannel intern (ChatChannel channel)
{
ChannelInfo chinfo = _channels.get(channel);
if (chinfo != null) {
return chinfo.channel;
}
return channel;
}
/**
* Converts a speaker's visible name into a unique integer id. This is not the oid for this
* speaker but rather a persistent integer identifier that can be passed between servers and
* used to look up the speaker on the target server via a call to {@link #getBodyObject}. We
* use this rather than names to avoid having to send (large) {@link Name} objects for every
* channel participant to each individual peer that will be forwarding messages.
*/
protected abstract int getBodyId (Name speaker);
/**
* Locates a body object from the given unique id. May return null.
*/
protected abstract BodyObject getBodyObject (int bodyId);
/** Forwards a channel speak request from the server hosting the message originator to the
* server that is hosting the channel. */
protected abstract static class ChannelAction extends PeerManager.NodeAction
{
public ChannelAction (ChatChannel channel) {
_channel = channel;
}
public ChannelAction () {
}
@Override public boolean isApplicable (NodeObject nodeobj) {
return ((CrowdNodeObject)nodeobj).hostedChannels.contains(_channel);
}
protected ChatChannel _channel;
@Inject protected transient ChatChannelManager _channelMan;
}
/** Informs the server hosting a channel that a body has been added to or removed from the
* channel's participants set. */
protected static class ParticipantChanged extends ChannelAction
{
public ParticipantChanged (ChatChannel channel, int bodyId, boolean added) {
super(channel);
_bodyId = bodyId;
_added = added;
}
public ParticipantChanged () {
}
@Override protected void execute () {
ChannelInfo info = _channelMan._channels.get(_channel);
if (info != null) {
if (_added) {
info.participants.add(_bodyId);
} else {
info.participants.remove(_bodyId);
}
} else if (_channelMan._resolving.containsKey(_channel)) {
log.warning("Oh for fuck's sake, distributed systems are complicated",
"channel", _channel);
}
}
protected int _bodyId;
protected boolean _added;
}
protected static class ChatCollectionRequest extends NodeRequest
{
public ChatCollectionRequest (Name user)
{
_user = user;
}
public ChatCollectionRequest ()
{
}
@Override public boolean isApplicable (NodeObject nodeobj)
{
// poll all nodes
return true;
}
@Override protected void execute (InvocationService.ResultListener listener)
{
// find all the UserMessages for the given user and send them back
listener.requestProcessed(Lists.newArrayList(Iterables.filter(
SpeakUtil.getChatHistory(_user), IS_USER_MESSAGE)));
}
protected Name _user;
}
protected static final Predicate<ChatHistoryEntry> IS_USER_MESSAGE =
new Predicate<ChatHistoryEntry>() {
public boolean apply (ChatHistoryEntry entry) {
return entry.message instanceof UserMessage;
}
};
protected static final Comparator<ChatHistoryEntry> SORT_BY_TIMESTAMP =
new Comparator<ChatHistoryEntry>() {
public int compare (ChatHistoryEntry e1, ChatHistoryEntry e2) {
return Longs.compare(e1.message.timestamp, e2.message.timestamp);
}
};
/** Forwards a channel speak request from the server hosting the message originator to the
* server that is hosting the channel. */
protected static class ForwardChannelSpeak extends ChannelAction
{
public ForwardChannelSpeak (ChatChannel channel, UserMessage message) {
super(channel);
_message = message;
}
public ForwardChannelSpeak () {
}
@Override protected void execute () {
_channelMan.dispatchSpeak(_channel, _message);
}
protected UserMessage _message;
}
/** Forwards a chat channel message to the server to which some subset of the channel
* participants are connected so that it can dispatch the message on their body objects. */
protected static class DispatchChannelSpeak extends ForwardChannelSpeak
{
public DispatchChannelSpeak (ChatChannel channel, UserMessage message, int[] bodyIds) {
super(channel, message);
_bodyIds = bodyIds;
}
public DispatchChannelSpeak () {
}
@Override public boolean isApplicable (NodeObject nodeobj) {
return true; // not used
}
@Override protected void execute () {
_channelMan.deliverSpeak(_channel, _message, _bodyIds);
}
protected int[] _bodyIds;
}
/** Contains metadata for a particular channel. */
protected static class ChannelInfo
{
/** The channel this info is for. */
public ChatChannel channel;
/** The body ids of the participants of this channel. */
public Set<Integer> participants;
/** The time at which a message was last dispatched on this channel. */
public long lastMessage;
}
/** Contains pending messages for all channels currently being resolved. */
protected Map<ChatChannel,List<UserMessage>> _resolving = Maps.newHashMap();
/** A map of resolved channels to metadata records. */
protected Map<ChatChannel,ChannelInfo> _channels = Maps.newHashMap();
/** Provides peer services. */
@Inject protected CrowdPeerManager _peerMan;
/** The period on which we check for idle channels. */
protected static final long IDLE_CHANNEL_CHECK_PERIOD = 5 * 1000L;
/** The amount of idle time (in milliseconds) after which we close a channel. */
protected static final long IDLE_CHANNEL_CLOSE_TIME = 5 * 60 * 1000L;
}
@@ -0,0 +1,85 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.server;
import javax.annotation.Generated;
import com.threerings.crowd.chat.client.ChatService;
import com.threerings.crowd.chat.data.ChatMarshaller;
import com.threerings.presents.client.InvocationService;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationDispatcher;
import com.threerings.presents.server.InvocationException;
import com.threerings.util.Name;
/**
* Dispatches requests to the {@link ChatProvider}.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from ChatService.java.")
public class ChatDispatcher extends InvocationDispatcher<ChatMarshaller>
{
/**
* Creates a dispatcher that may be registered to dispatch invocation
* service requests for the specified provider.
*/
public ChatDispatcher (ChatProvider provider)
{
this.provider = provider;
}
@Override
public ChatMarshaller createMarshaller ()
{
return new ChatMarshaller();
}
@Override
public void dispatchRequest (
ClientObject source, int methodId, Object[] args)
throws InvocationException
{
switch (methodId) {
case ChatMarshaller.AWAY:
((ChatProvider)provider).away(
source, (String)args[0]
);
return;
case ChatMarshaller.BROADCAST:
((ChatProvider)provider).broadcast(
source, (String)args[0], (InvocationService.InvocationListener)args[1]
);
return;
case ChatMarshaller.TELL:
((ChatProvider)provider).tell(
source, (Name)args[0], (String)args[1], (ChatService.TellListener)args[2]
);
return;
default:
super.dispatchRequest(source, methodId, args);
return;
}
}
}
@@ -0,0 +1,302 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.server;
import java.util.Iterator;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.samskivert.util.StringUtil;
import com.threerings.util.MessageBundle;
import com.threerings.util.Name;
import com.threerings.util.TimeUtil;
import com.threerings.presents.client.InvocationService.InvocationListener;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.server.InvocationException;
import com.threerings.presents.server.InvocationManager;
import com.threerings.presents.server.InvocationProvider;
import com.threerings.crowd.chat.client.ChatService.TellListener;
import com.threerings.crowd.chat.client.ChatService;
import com.threerings.crowd.chat.data.ChatCodes;
import com.threerings.crowd.chat.data.SystemMessage;
import com.threerings.crowd.chat.data.UserMessage;
import com.threerings.crowd.data.BodyObject;
import com.threerings.crowd.data.CrowdCodes;
import com.threerings.crowd.data.OccupantInfo;
import com.threerings.crowd.data.PlaceObject;
import com.threerings.crowd.server.BodyLocal;
import com.threerings.crowd.server.BodyLocator;
import com.threerings.crowd.server.PlaceRegistry;
/**
* The chat provider handles the server side of the chat-related invocation services.
*/
@Singleton
public class ChatProvider
implements InvocationProvider
{
/** Interface to allow an auto response to a tell message. */
public static interface TellAutoResponder
{
/**
* Called following the delivery of <code>message</code> from <code>teller</code> to
* <code>tellee</code>.
*/
void sentTell (BodyObject teller, BodyObject tellee, String message);
}
/** Used to forward certain types of chat messages between servers in a multi-server setup. */
public static interface ChatForwarder
{
/**
* Requests that the supplied tell message be delivered to the appropriate destination.
*
* @return true if the tell was delivered, false otherwise.
*/
boolean forwardTell (UserMessage message, Name target, TellListener listener);
/**
* Requests that the supplied broadcast message be delivered on other servers.
*/
void forwardBroadcast (Name from, byte levelOrMode, String bundle, String msg);
}
/**
* Creates and registers this chat provider.
*/
@Inject public ChatProvider (InvocationManager invmgr)
{
// register a chat provider with the invocation manager
invmgr.registerDispatcher(new ChatDispatcher(this), CrowdCodes.CROWD_GROUP);
}
/**
* Set an object to which all broadcasts should be sent, rather than iterating over the place
* objects and sending to each of them.
*
* @param object an object to send all broadcasts, or null to send to each place object
* instead.
*/
public void setAlternateBroadcastObject (DObject object)
{
_broadcastObject = object;
}
/**
* Set the auto tell responder for the chat provider. Only one auto responder is allowed.
* <em>Note:</em> this only works for same-server tells. If the tell is forwarded to another
* server, no auto-response opportunity is provided (because we never have both body objects in
* the same place).
*/
public void setTellAutoResponder (TellAutoResponder autoRespond)
{
_autoRespond = autoRespond;
}
/**
* Configures the chat forwarder. This is used by the Crowd peer services to forward messages
* between servers in a multi-server cluster.
*/
public void setChatForwarder (ChatForwarder forwarder)
{
_chatForwarder = forwarder;
}
/**
* Processes a {@link ChatService#tell} request.
*/
public void tell (ClientObject caller, Name target, String message, TellListener listener)
throws InvocationException
{
// ensure that the caller has normal chat privileges
InvocationException.requireAccess(caller, ChatCodes.CHAT_ACCESS);
// deliver the tell message to the target
BodyObject source = (BodyObject)caller;
deliverTell(createTellMessage(source, message), target, listener);
// inform the auto-responder if needed
BodyObject targobj;
if (_autoRespond != null && (targobj = _locator.lookupBody(target)) != null) {
_autoRespond.sentTell(source, targobj, message);
}
}
/**
* Processes a {@link ChatService#broadcast} request.
*/
public void broadcast (ClientObject caller, String message, InvocationListener listener)
throws InvocationException
{
// make sure the requesting user has broadcast privileges
InvocationException.requireAccess(caller, ChatCodes.BROADCAST_ACCESS);
BodyObject body = (BodyObject)caller;
broadcast(body.getVisibleName(), null, message, false, true);
}
/**
* Processes a {@link ChatService#away} request.
*/
public void away (ClientObject caller, String message)
{
BodyObject body = (BodyObject)caller;
// we modify this field via an invocation service request because a body object is not
// modifiable by the client
body.setAwayMessage(message);
}
/**
* Broadcasts the specified message to all place objects in the system.
*
* @param from the user the broadcast is from, or null to send the message as a system message.
* @param bundle the bundle, or null if the message needs no translation.
* @param msg the content of the message to broadcast.
* @param attention if true, the message is sent as ATTENTION level, otherwise as INFO. Ignored
* if from is non-null.
* @param forward if true, forward this broadcast on to any registered chat forwarder, if
* false, deliver it only locally on this server.
*/
public void broadcast (Name from, String bundle, String msg, boolean attention, boolean forward)
{
byte levelOrMode = (from != null) ? ChatCodes.BROADCAST_MODE
: (attention ? SystemMessage.ATTENTION : SystemMessage.INFO);
broadcast(from, levelOrMode, bundle, msg, forward);
}
/**
* Broadcast with support for a customizable level or mode.
* @param levelOrMode if from is null, it's an attentionLevel, else it's a mode code.
*/
public void broadcast (Name from, byte levelOrMode, String bundle, String msg, boolean forward)
{
if (_broadcastObject != null) {
broadcastTo(_broadcastObject, from, levelOrMode, bundle, msg);
} else {
for (Iterator<PlaceObject> iter = _plreg.enumeratePlaces(); iter.hasNext(); ) {
PlaceObject plobj = iter.next();
if (plobj.shouldBroadcast()) {
broadcastTo(plobj, from, levelOrMode, bundle, msg);
}
}
}
if (forward && _chatForwarder != null) {
_chatForwarder.forwardBroadcast(from, levelOrMode, bundle, msg);
}
}
/**
* Delivers a tell message to the specified target and notifies the supplied listener of the
* result. It is assumed that the teller has already been permissions checked.
*/
public void deliverTell (UserMessage message, Name target, TellListener listener)
throws InvocationException
{
// make sure the target user is online
BodyObject tobj = _locator.lookupBody(target);
if (tobj == null) {
// if we have a forwarder configured, try forwarding the tell
if (_chatForwarder != null && _chatForwarder.forwardTell(message, target, listener)) {
return;
}
throw new InvocationException(ChatCodes.USER_NOT_ONLINE);
}
if (tobj.status == OccupantInfo.DISCONNECTED) {
String errmsg = MessageBundle.compose(
ChatCodes.USER_DISCONNECTED, TimeUtil.getTimeOrderString(
System.currentTimeMillis() - tobj.getLocal(BodyLocal.class).statusTime,
TimeUtil.SECOND));
throw new InvocationException(errmsg);
}
// deliver a tell notification to the target player
deliverTell(tobj, message);
// let the teller know it went ok
long idle = 0L;
if (tobj.status == OccupantInfo.IDLE) {
idle = System.currentTimeMillis() - tobj.getLocal(BodyLocal.class).statusTime;
}
String awayMessage = null;
if (!StringUtil.isBlank(tobj.awayMessage)) {
awayMessage = tobj.awayMessage;
}
listener.tellSucceeded(idle, awayMessage);
}
/**
* Delivers a tell notification to the specified target player. It is assumed that the message
* is coming from some server entity and need not be permissions checked or notified of the
* result.
*/
public void deliverTell (BodyObject target, UserMessage message)
{
SpeakUtil.sendMessage(target, message);
// note that the teller "heard" what they said
SpeakUtil.noteMessage(message.speaker, message);
}
/**
* Used to create a {@link UserMessage} for the supplied sender.
*/
protected UserMessage createTellMessage (BodyObject source, String message)
{
return new UserMessage(source.getVisibleName(), message);
}
/**
* Direct a broadcast to the specified object.
*/
protected void broadcastTo (
DObject object, Name from, byte levelOrMode, String bundle, String msg)
{
if (from == null) {
SpeakUtil.sendSystem(object, bundle, msg, levelOrMode /* level */);
} else {
SpeakUtil.sendSpeak(object, from, bundle, msg, levelOrMode /* mode */);
}
}
/** Provides access to place managers. */
@Inject protected PlaceRegistry _plreg;
/** Used to look up body objects by name. */
@Inject protected BodyLocator _locator;
/** Generates auto-responses to tells. May be null. */
protected TellAutoResponder _autoRespond;
/** Forwards chat between servers. May be null. */
protected ChatForwarder _chatForwarder;
/** An alternative object to which broadcasts should be sent. */
protected DObject _broadcastObject;
}
@@ -0,0 +1,70 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.server;
import javax.annotation.Generated;
import com.threerings.crowd.chat.data.SpeakMarshaller;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationDispatcher;
import com.threerings.presents.server.InvocationException;
/**
* Dispatches requests to the {@link SpeakProvider}.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from SpeakService.java.")
public class SpeakDispatcher extends InvocationDispatcher<SpeakMarshaller>
{
/**
* Creates a dispatcher that may be registered to dispatch invocation
* service requests for the specified provider.
*/
public SpeakDispatcher (SpeakProvider provider)
{
this.provider = provider;
}
@Override
public SpeakMarshaller createMarshaller ()
{
return new SpeakMarshaller();
}
@Override
public void dispatchRequest (
ClientObject source, int methodId, Object[] args)
throws InvocationException
{
switch (methodId) {
case SpeakMarshaller.SPEAK:
((SpeakProvider)provider).speak(
source, (String)args[0], ((Byte)args[1]).byteValue()
);
return;
default:
super.dispatchRequest(source, methodId, args);
return;
}
}
}
@@ -0,0 +1,104 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.server;
import com.threerings.util.MessageManager;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.server.InvocationManager;
import com.threerings.crowd.chat.client.SpeakService;
import com.threerings.crowd.chat.data.ChatCodes;
import com.threerings.crowd.data.BodyObject;
import static com.threerings.crowd.Log.log;
/**
* Wires up the {@link SpeakService} to a particular distributed object. A server entity can make
* "speech" available among the subscribers of a particular distributed object by constructing a
* speak handler and registering it with the {@link InvocationManager}, then placing the resulting
* marshaller into the distributed object in question so that subscribers to that object can use it
* to generate "speak" requests on that object.
*/
public class SpeakHandler
implements SpeakProvider
{
/**
* Used to prevent abitrary users from issuing speak requests.
*/
public static interface SpeakerValidator
{
/**
* Should return true if the supplied speaker is allowed to speak via the speak provider
* with which this validator was registered.
*/
boolean isValidSpeaker (DObject speakObj, ClientObject speaker, byte mode);
}
/**
* Creates a handler that will provide speech on the supplied distributed object.
*
* @param speakObj the object for which speech requests will be processed.
* @param validator an optional validator that can be used to prevent arbitrary users from
* using the speech services on this object.
*/
public SpeakHandler (DObject speakObj, SpeakerValidator validator)
{
_speakObj = speakObj;
_validator = validator;
}
// from interface SpeakProvider
public void speak (ClientObject caller, String message, byte mode)
{
// ensure that the caller has normal chat privileges
BodyObject source = (BodyObject)caller;
String errmsg = source.checkAccess(ChatCodes.CHAT_ACCESS, null);
if (errmsg != null) {
// we normally don't listen for responses to speak messages so we can't just throw an
// InvocationException we have to specifically communicate the error to the user
SpeakUtil.sendFeedback(source, MessageManager.GLOBAL_BUNDLE, errmsg);
return;
}
// TODO: broadcast should be handled more like a system message rather than as a mode for a
// user message so that we don't have to do this validation here. Or not.
// ensure that the speaker is valid
if ((mode == ChatCodes.BROADCAST_MODE) ||
(_validator != null && !_validator.isValidSpeaker(_speakObj, caller, mode))) {
log.warning("Refusing invalid speak request", "caller", caller.who(),
"speakObj", _speakObj.which(), "message", message, "mode", mode);
} else {
// issue the speak message on our speak object
SpeakUtil.sendSpeak(_speakObj, source.getVisibleName(), null, message, mode);
}
}
/** Our speech object. */
protected DObject _speakObj;
/** The entity that will validate our speakers. */
protected SpeakerValidator _validator;
}
@@ -0,0 +1,41 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.server;
import javax.annotation.Generated;
import com.threerings.crowd.chat.client.SpeakService;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationProvider;
/**
* Defines the server-side of the {@link SpeakService}.
*/
@Generated(value={"com.threerings.presents.tools.GenServiceTask"},
comments="Derived from SpeakService.java.")
public interface SpeakProvider extends InvocationProvider
{
/**
* Handles a {@link SpeakService#speak} request.
*/
void speak (ClientObject caller, String arg1, byte arg2);
}
@@ -0,0 +1,392 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.chat.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.ObserverList;
import com.threerings.util.Name;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.RootDObjectManager;
import com.threerings.crowd.chat.data.ChatChannel;
import com.threerings.crowd.chat.data.ChatCodes;
import com.threerings.crowd.chat.data.ChatMessage;
import com.threerings.crowd.chat.data.KeepNoHistory;
import com.threerings.crowd.chat.data.SpeakObject;
import com.threerings.crowd.chat.data.SystemMessage;
import com.threerings.crowd.chat.data.UserMessage;
import com.threerings.crowd.data.BodyObject;
import com.threerings.io.Streamable;
import static com.threerings.crowd.Log.log;
/**
* Provides the back-end of the chat speaking facilities.
*/
public class SpeakUtil
{
/**
* An interface used to notify external systems whenever a chat message is spoken by one user
* and heard by another.
*/
public static interface MessageObserver
{
/**
* Called for each player that hears a particular chat message.
*/
void messageDelivered (Name hearer, UserMessage message);
}
/**
* Recorded parcel of chat for historical purposes, maintained by
* {@link #recordToChatHistory(ChatChannel, UserMessage, Name...)},
* {@link #getChatHistory(Name)}, and {@link #clearHistory(Name)}.
*/
public static class ChatHistoryEntry
implements Streamable
{
/** The channel on which the message was sent, of null if the channel manager was not
* used. */
public ChatChannel channel;
/** The message sent. */
public ChatMessage message;
/** For deserialization. */
public ChatHistoryEntry ()
{
}
/**
* Creates a new history entry.
*/
public ChatHistoryEntry (ChatChannel channel, ChatMessage message)
{
this.channel = channel;
this.message = message;
}
}
/**
* Registers a {@link MessageObserver} to be notified whenever a user-originated chat message
* is heard by another user.
*/
public static void registerMessageObserver (MessageObserver obs)
{
_messageObs.add(obs);
}
/**
* Removes a registration made previously with {@link #registerMessageObserver}.
*/
public static void removeMessageObserver (MessageObserver obs)
{
_messageObs.remove(obs);
}
/**
* Sends a speak notification to the specified place object originating with the specified
* speaker (the speaker optionally being a server entity that wishes to fake a "speak" message)
* and with the supplied message content.
*
* @param speakObj the object on which to generate the speak message.
* @param speaker the username of the user that generated the message (or some special speaker
* name for server messages).
* @param bundle null when the message originates from a real human, the bundle identifier that
* will be used by the client to translate the message text when the message originates from a
* server entity "faking" a chat message.
* @param message the text of the speak message.
*/
public static void sendSpeak (DObject speakObj, Name speaker, String bundle, String message)
{
sendSpeak(speakObj, speaker, bundle, message, ChatCodes.DEFAULT_MODE);
}
/**
* Sends a speak notification to the specified place object originating with the specified
* speaker (the speaker optionally being a server entity that wishes to fake a "speak" message)
* and with the supplied message content.
*
* @param speakObj the object on which to generate the speak message.
* @param speaker the username of the user that generated the message (or some special speaker
* name for server messages).
* @param bundle null when the message originates from a real human, the bundle identifier that
* will be used by the client to translate the message text when the message originates from a
* server entity "faking" a chat message.
* @param message the text of the speak message.
* @param mode the mode of the message, see {@link ChatCodes#DEFAULT_MODE}.
*/
public static void sendSpeak (DObject speakObj, Name speaker, String bundle, String message,
byte mode)
{
sendMessage(speakObj, new UserMessage(speaker, bundle, message, mode));
}
/**
* Sends a system INFO message notification to the specified object with the supplied message
* content. A system message is one that will be rendered where the speak messages are
* rendered, but in a way that makes it clear that it is a message from the server.
*
* Info messages are sent when something happens that was neither directly triggered by the
* user, nor requires direct action.
*
* @param speakObj the object on which to deliver the message.
* @param bundle the name of the localization bundle that should be used to translate this
* system message prior to displaying it to the client.
* @param message the text of the message.
*/
public static void sendInfo (DObject speakObj, String bundle, String message)
{
sendSystem(speakObj, bundle, message, SystemMessage.INFO);
}
/**
* Sends a system FEEDBACK message notification to the specified object with the supplied
* message content. A system message is one that will be rendered where the speak messages are
* rendered, but in a way that makes it clear that it is a message from the server.
*
* Feedback messages are sent in direct response to a user action, usually to indicate success
* or failure of the user's action.
*
* @param speakObj the object on which to deliver the message.
* @param bundle the name of the localization bundle that should be used to translate this
* system message prior to displaying it to the client.
* @param message the text of the message.
*/
public static void sendFeedback (DObject speakObj, String bundle, String message)
{
sendSystem(speakObj, bundle, message, SystemMessage.FEEDBACK);
}
/**
* Sends a system ATTENTION message notification to the specified object with the supplied
* message content. A system message is one that will be rendered where the speak messages are
* rendered, but in a way that makes it clear that it is a message from the server.
*
* Attention messages are sent when something requires user action that did not result from
* direct action by the user.
*
* @param speakObj the object on which to deliver the message.
* @param bundle the name of the localization bundle that should be used to translate this
* system message prior to displaying it to the client.
* @param message the text of the message.
*/
public static void sendAttention (DObject speakObj, String bundle, String message)
{
sendSystem(speakObj, bundle, message, SystemMessage.ATTENTION);
}
/**
* Send the specified message on the specified object.
*/
public static void sendMessage (DObject speakObj, ChatMessage msg)
{
if (speakObj == null) {
log.warning("Dropping speak message, no speak obj '" + msg + "'.", new Exception());
return;
}
// post the message to the relevant object
speakObj.postMessage(ChatCodes.CHAT_NOTIFICATION, new Object[] { msg });
// if this is a user message; add it to the heard history of all users that can "hear" it
if (!(msg instanceof UserMessage)) {
return;
} else if (speakObj instanceof SpeakObject) {
_messageMapper.omgr = (RootDObjectManager)speakObj.getManager();
_messageMapper.message = (UserMessage)msg;
((SpeakObject)speakObj).applyToListeners(_messageMapper);
_messageMapper.omgr = null;
_messageMapper.message = null;
} else {
log.info("Unable to note listeners", "dclass", speakObj.getClass(), "msg", msg);
}
}
/**
* Returns a list of {@link ChatMessage} objects to which this user has been privy in the
* recent past. If the given name implements {@link KeepNoHistory}, null is returned.
*/
public static List<ChatHistoryEntry> getChatHistory (Name username)
{
List<ChatHistoryEntry> history = getHistoryList(username);
if (history != null) {
pruneHistory(System.currentTimeMillis(), history);
}
return history;
}
/**
* Called to clear the chat history for the specified user.
*/
public static void clearHistory (Name username)
{
// Log.info("Clearing history for " + username + ".");
_histories.remove(username);
}
/**
* Records the specified channel and message to the specified users' chat histories. If {@link
* ChatMessage#timestamp} is not already filled in, it will be.
*/
public static void recordToChatHistory (
ChatChannel channel, UserMessage msg, Name... usernames)
{
// fill in the message's time stamp if necessary
if (msg.timestamp == 0L) {
msg.timestamp = System.currentTimeMillis();
}
for (Name username : usernames) {
// add the message to this user's chat history
List<ChatHistoryEntry> history = getHistoryList(username);
if (history == null) {
continue;
}
history.add(new ChatHistoryEntry(channel, msg));
// if the history is big enough, potentially prune it (we always prune when asked for
// the history, so this is just to balance memory usage with CPU expense)
if (history.size() > 15) {
pruneHistory(msg.timestamp, history);
}
}
}
/**
* Notes that the specified user was privy to the specified message. If {@link
* ChatMessage#timestamp} is not already filled in, it will be.
*/
protected static void noteMessage (Name username, UserMessage msg)
{
// fill in the message's time stamp if necessary
if (msg.timestamp == 0L) {
msg.timestamp = System.currentTimeMillis();
}
recordToChatHistory(null, msg, username);
// Log.info("Noted that " + username + " heard " + msg + ".");
// notify any message observers
_messageOp.init(username, msg);
_messageObs.apply(_messageOp);
}
/**
* Send the specified system message on the specified dobj.
*/
protected static void sendSystem (DObject speakObj, String bundle, String message, byte level)
{
sendMessage(speakObj, new SystemMessage(message, bundle, level));
}
/**
* Returns this user's chat history, creating one if necessary. If the given name implements
* {@link KeepNoHistory}, null is returned.
*/
protected static List<ChatHistoryEntry> getHistoryList (Name username)
{
if (username instanceof KeepNoHistory) {
return null;
}
List<ChatHistoryEntry> history = _histories.get(username);
if (history == null) {
_histories.put(username, history = Lists.newArrayList());
}
return history;
}
/**
* Prunes all messages from this history which are expired.
*/
protected static void pruneHistory (long now, List<ChatHistoryEntry> history)
{
int prunepos = 0;
for (int ll = history.size(); prunepos < ll; prunepos++) {
ChatHistoryEntry entry = history.get(prunepos);
if (now - entry.message.timestamp < HISTORY_EXPIRATION) {
break; // stop when we get to the first valid message
}
}
history.subList(0, prunepos).clear();
}
/** Used to note the recipients of a chat message. */
protected static class MessageMapper implements SpeakObject.ListenerOp
{
public RootDObjectManager omgr;
public UserMessage message;
public void apply (int bodyOid) {
DObject dobj = omgr.getObject(bodyOid);
if (dobj != null && dobj instanceof BodyObject) {
noteMessage(((BodyObject)dobj).getVisibleName(), message);
}
}
public void apply (Name username) {
noteMessage(username, message);
}
}
/** Used to notify our {@link MessageObserver}s. */
protected static class MessageObserverOp
implements ObserverList.ObserverOp<MessageObserver>
{
public void init (Name hearer, UserMessage message) {
_hearer = hearer;
_message = message;
}
public boolean apply (MessageObserver observer) {
observer.messageDelivered(_hearer, _message);
return true;
}
protected Name _hearer;
protected UserMessage _message;
}
/** Recent chat history for the server. */
protected static Map<Name, List<ChatHistoryEntry>> _histories = Maps.newHashMap();
/** Used to note the recipients of a chat message. */
protected static MessageMapper _messageMapper = new MessageMapper();
/** A list of {@link MessageObserver}s. */
protected static ObserverList<MessageObserver> _messageObs = ObserverList.newFastUnsafe();
/** Used to notify our {@link MessageObserver}s. */
protected static MessageObserverOp _messageOp = new MessageObserverOp();
/** The amount of time before chat history becomes... history. */
protected static final long HISTORY_EXPIRATION = 5L * 60L * 1000L;
}
@@ -0,0 +1,37 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
/**
* The client side of the body-related invocation services.
*/
public interface BodyService extends InvocationService
{
/**
* Requests to set the idle state of the client to the specified
* value.
*/
void setIdle (Client client, boolean idle);
}
@@ -0,0 +1,53 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import com.threerings.crowd.data.PlaceObject;
/**
* The location adapter makes life easier for a class that really only
* cares about one or two of the location observer callbacks and doesn't
* want to provide empty implementations of the others. One can either
* extend location adapter, or create an anonymous instance that overrides
* the desired callback(s). Note that the location adapter defaults to
* ratifying any location change.
*
* @see LocationObserver
*/
public class LocationAdapter implements LocationObserver
{
// documentation inherited
public boolean locationMayChange (int placeId)
{
return true;
}
// documentation inherited
public void locationDidChange (PlaceObject place)
{
}
// documentation inherited
public void locationChangeFailed (int placeId, String reason)
{
}
}
@@ -0,0 +1,68 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import com.threerings.presents.client.InvocationDecoder;
/**
* Dispatches calls to a {@link LocationReceiver} instance.
*/
public class LocationDecoder extends InvocationDecoder
{
/** The generated hash code used to identify this receiver class. */
public static final String RECEIVER_CODE = "58f2830e027f4f3377e100ef12332497";
/** The method id used to dispatch {@link LocationReceiver#forcedMove}
* notifications. */
public static final int FORCED_MOVE = 1;
/**
* Creates a decoder that may be registered to dispatch invocation
* service notifications to the specified receiver.
*/
public LocationDecoder (LocationReceiver receiver)
{
this.receiver = receiver;
}
@Override
public String getReceiverCode ()
{
return RECEIVER_CODE;
}
@Override
public void dispatchNotification (int methodId, Object[] args)
{
switch (methodId) {
case FORCED_MOVE:
((LocationReceiver)receiver).forcedMove(
((Integer)args[0]).intValue()
);
return;
default:
super.dispatchNotification(methodId, args);
return;
}
}
}
@@ -0,0 +1,628 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import java.util.ArrayList;
import com.google.common.collect.Lists;
import com.samskivert.util.ObserverList;
import com.samskivert.util.ResultListener;
import com.samskivert.util.ObserverList.ObserverOp;
import com.threerings.presents.client.BasicDirector;
import com.threerings.presents.client.Client;
import com.threerings.presents.dobj.ObjectAccessException;
import com.threerings.presents.dobj.Subscriber;
import com.threerings.presents.util.SafeSubscriber;
import com.threerings.crowd.data.BodyObject;
import com.threerings.crowd.data.CrowdCodes;
import com.threerings.crowd.data.LocationCodes;
import com.threerings.crowd.data.PlaceConfig;
import com.threerings.crowd.data.PlaceObject;
import com.threerings.crowd.util.CrowdContext;
import static com.threerings.crowd.Log.log;
/**
* The location director provides a means by which entities on the client can request to move from
* place to place and can be notified if other entities have caused the client to move to a new
* place. It also provides a mechanism for ratifying a request to move to a new place before
* actually issuing the request.
*/
public class LocationDirector extends BasicDirector
implements LocationCodes, LocationReceiver
{
/**
* Used to recover from a moveTo request that was accepted but resulted in a failed attempt to
* fetch the place object to which we were moving.
*/
public static interface FailureHandler
{
/**
* Should instruct the client to move to the last known working location (as well as clean
* up after the failed moveTo request).
*/
void recoverFailedMove (int placeId);
}
/**
* Constructs a location director which will configure itself for operation using the supplied
* context.
*/
public LocationDirector (CrowdContext ctx)
{
super(ctx);
// keep this around for later
_ctx = ctx;
// register for location notifications
_ctx.getClient().getInvocationDirector().registerReceiver(new LocationDecoder(this));
}
/**
* Adds a location observer to the list. This observer will subsequently be notified of
* potential, effected and failed location changes.
*/
public void addLocationObserver (LocationObserver observer)
{
_observers.add(observer);
}
/**
* Removes a location observer from the list.
*/
public void removeLocationObserver (LocationObserver observer)
{
_observers.remove(observer);
}
/**
* Returns the place object for the location we currently occupy or null if we're not currently
* occupying any location.
*/
public PlaceObject getPlaceObject ()
{
return _plobj;
}
/**
* Returns true if there is a pending move request.
*/
public boolean movePending ()
{
return (_pendingPlaceId > 0);
}
/**
* Requests that this client be moved to the specified place. A request will be made and when
* the response is received, the location observers will be notified of success or failure.
*
* @return true if the move to request was issued, false if it was rejected by a location
* observer or because we have another request outstanding.
*/
public boolean moveTo (int placeId)
{
// make sure the placeId is valid
if (placeId < 0) {
log.warning("Refusing moveTo(): invalid placeId " + placeId + ".");
return false;
}
// first check to see if our observers are happy with this move request
if (!mayMoveTo(placeId, null)) {
return false;
}
// we need to call this both to mark that we're issuing a move request and to check to see
// if the last issued request should be considered stale
boolean refuse = checkRepeatMove();
// complain if we're over-writing a pending request
if (_pendingPlaceId != -1) {
// if the pending request has been outstanding more than a minute, go ahead and let
// this new one through in an attempt to recover from dropped moveTo requests
if (refuse) {
log.warning("Refusing moveTo; We have a request outstanding",
"ppid", _pendingPlaceId, "npid", placeId);
return false;
} else {
log.warning("Overriding stale moveTo request", "ppid", _pendingPlaceId,
"npid", placeId);
}
}
// make a note of our pending place id
_pendingPlaceId = placeId;
// issue a moveTo request
log.info("Issuing moveTo(" + placeId + ").");
_lservice.moveTo(_ctx.getClient(), placeId, new LocationService.MoveListener() {
public void moveSucceeded (PlaceConfig config) {
// handle the successful move
didMoveTo(_pendingPlaceId, config);
// and clear out the tracked pending oid
_pendingPlaceId = -1;
handlePendingForcedMove();
}
public void requestFailed (String reason) {
// clear out our pending request oid
int placeId = _pendingPlaceId;
_pendingPlaceId = -1;
log.info("moveTo failed", "pid", placeId, "reason", reason);
// let our observers know that something has gone horribly awry
handleFailure(placeId, reason);
handlePendingForcedMove();
}
});
return true;
}
/**
* Requests to move to the room that we last occupied, if such a room exists.
*
* @return true if we had a previous room and we requested to move to it, false if we had no
* previous room.
*/
public boolean moveBack ()
{
if (_previousPlaceId == -1) {
return false;
} else {
moveTo(_previousPlaceId);
return true;
}
}
/**
* Issues a request to leave our current location.
*
* @return true if we were able to leave, false if we are in the middle of moving somewhere and
* can't yet leave.
*/
public boolean leavePlace ()
{
if (_pendingPlaceId != -1) {
return false;
}
_lservice.leavePlace(_ctx.getClient());
didLeavePlace();
// let our observers know that we're no longer in a location
_observers.apply(_didChangeOp);
return true;
}
/**
* This can be called by cooperating directors that need to coopt the moving process to extend
* it in some way or other. In such situations, they should call this method before moving to a
* new location to check to be sure that all of the registered location observers are amenable
* to a location change.
*
* @param placeId the place oid of our tentative new location.
*
* @return true if everyone is happy with the move, false if it was vetoed by one of the
* location observers.
*/
public boolean mayMoveTo (final int placeId, ResultListener<PlaceConfig> rl)
{
final boolean[] vetoed = new boolean[1];
_observers.apply(new ObserverOp<LocationObserver>() {
public boolean apply (LocationObserver obs) {
vetoed[0] = (vetoed[0] || !obs.locationMayChange(placeId));
return true;
}
});
// if we're actually going somewhere, let the controller know that we might be leaving
mayLeavePlace();
// if we have a result listener, let it know if we failed or keep it for later if we're
// still going
if (rl != null) {
if (vetoed[0]) {
rl.requestFailed(new MoveVetoedException());
} else {
_moveListener = rl;
}
}
// and return the result
return !vetoed[0];
}
/**
* Called to inform our controller that we may be leaving the current place.
*/
protected void mayLeavePlace ()
{
if (_controller != null) {
try {
_controller.mayLeavePlace(_plobj);
} catch (Exception e) {
log.warning("Place controller choked in mayLeavePlace", "plobj", _plobj, e);
}
}
}
/**
* This can be called by cooperating directors that need to coopt the moving process to extend
* it in some way or other. In such situations, they will be responsible for receiving the
* successful move response and they should let the location director know that the move has
* been effected.
*
* @param placeId the place oid of our new location.
* @param config the configuration information for the new place.
*/
public void didMoveTo (int placeId, PlaceConfig config)
{
if (_moveListener != null) {
_moveListener.requestCompleted(config);
_moveListener = null;
}
// keep track of our previous place id
_previousPlaceId = _placeId;
// clear out our last request time
_lastRequestTime = 0;
// do some cleaning up in case we were previously in a place
didLeavePlace();
// make a note that we're now mostly in the new location
_placeId = placeId;
// start up a new place controller to manage the new place
try {
_controller = createController(config);
if (_controller == null) {
log.warning("Place config returned null controller", "config", config);
return;
}
_controller.init(_ctx, config);
// subscribe to our new place object to complete the move
_subber = new SafeSubscriber<PlaceObject>(_placeId, new Subscriber<PlaceObject>() {
public void objectAvailable (PlaceObject object) {
gotPlaceObject(object);
}
public void requestFailed (int oid, ObjectAccessException cause) {
// aiya! we were unable to fetch our new place object; something is badly wrong
log.warning("Aiya! Unable to fetch place object for new location", "plid", oid,
"reason", cause);
// clear out our half initialized place info
int placeId = _placeId;
_placeId = -1;
// let the kids know shit be fucked
handleFailure(placeId, "m.unable_to_fetch_place_object");
}
});
_subber.subscribe(_ctx.getDObjectManager());
} catch (Exception e) {
log.warning("Failed to create place controller", "config", config, e);
handleFailure(_placeId, LocationCodes.E_INTERNAL_ERROR);
}
}
/**
* Called when we're leaving our current location. Informs the location's controller that we're
* departing, unsubscribes from the location's place object, and clears out our internal place
* information.
*/
public void didLeavePlace ()
{
// unsubscribe from our old place object
if (_subber != null) {
_subber.unsubscribe(_ctx.getDObjectManager());
_subber = null;
}
// let the old controller know that things are going away
if (_plobj != null && _controller != null) {
try {
_controller.didLeavePlace(_plobj);
} catch (Exception e) {
log.warning("Place controller choked in didLeavePlace", "plobj", _plobj, e);
}
}
// and clear out other bits
_plobj = null;
_controller = null;
_placeId = -1;
}
/**
* This can be called by cooperating directors that need to coopt the moving process to extend
* it in some way or other. If the coopted move request fails, this failure can be propagated
* to the location observers if appropriate.
*
* @param placeId the place oid to which we failed to move.
* @param reason the reason code given for failure.
*/
public void failedToMoveTo (int placeId, String reason)
{
if (_moveListener != null) {
_moveListener.requestFailed(new MoveFailedException(reason));
_moveListener = null;
}
// clear out our last request time
_lastRequestTime = 0;
// let our observers know what's up
handleFailure(placeId, reason);
}
/**
* Called to test and set a time stamp that we use to determine if a pending moveTo request is
* stale.
*/
public boolean checkRepeatMove ()
{
long now = System.currentTimeMillis();
if (now - _lastRequestTime < STALE_REQUEST_DURATION) {
return true;
} else {
_lastRequestTime = now;
return false;
}
}
@Override
public void clientDidLogon (Client client)
{
super.clientDidLogon(client);
// subscribe to our body object
Subscriber<BodyObject> sub = new Subscriber<BodyObject>() {
public void objectAvailable (BodyObject object) {
gotBodyObject(object);
}
public void requestFailed (int oid, ObjectAccessException cause) {
log.warning("Location director unable to fetch body object; all has gone " +
"horribly wrong", "cause", cause);
}
};
int cloid = client.getClientOid();
client.getDObjectManager().subscribeToObject(cloid, sub);
}
@Override
public void clientDidLogoff (Client client)
{
super.clientDidLogoff(client);
// clear ourselves out and inform observers of our departure
mayLeavePlace();
didLeavePlace();
// let our observers know that we're no longer in a location
_observers.apply(_didChangeOp);
// clear out everything else (it's possible that we were logged off in the middle of a
// change location request)
_pendingPlaceId = -1;
_pendingForcedMoves.clear();
_previousPlaceId = -1;
_lastRequestTime = 0L;
_lservice = null;
}
@Override
protected void registerServices (Client client)
{
client.addServiceGroup(CrowdCodes.CROWD_GROUP);
}
@Override
protected void fetchServices (Client client)
{
// obtain our service handle
_lservice = client.requireService(LocationService.class);
}
protected void gotPlaceObject (PlaceObject object)
{
// yay, we have our new place object
_plobj = object;
// fill in our manager caller
_plobj.initManagerCaller(_ctx.getClient().getDObjectManager());
// let the place controller know that we're ready to roll
if (_controller != null) {
try {
_controller.willEnterPlace(_plobj);
} catch (Exception e) {
log.warning("Controller choked in willEnterPlace", "place", _plobj, e);
}
}
// let our observers know that all is well on the western front
_observers.apply(_didChangeOp);
}
protected void gotBodyObject (BodyObject clobj)
{
// TODO? check to see if we are already in a location, in which case we'll want to be going
// there straight away
}
// documentation inherited from interface
public void forcedMove (final int placeId)
{
// if we're in the middle of a move, we can't abort it or we will screw everything up, so
// just finish up what we're doing and assume that the repeated move request was the
// spurious one as it would be in the case of lag causing rapid-fire repeat requests
if (movePending()) {
if (_pendingPlaceId == placeId) {
log.info("Dropping forced move because we have a move pending",
"pendId", _pendingPlaceId, "reqId", placeId);
} else {
log.info("Delaying forced move because we have a move pending",
"pendId", _pendingPlaceId, "reqId", placeId);
addPendingForcedMove(new Runnable() {
public void run () {
forcedMove(placeId);
}
});
}
return;
}
log.info("Moving at request of server", "placeId", placeId);
// clear out our old place information
mayLeavePlace();
didLeavePlace();
// move to the new place
moveTo(placeId);
}
/**
* Sets the failure handler which will recover from place object fetching failures. In the
* event that we are unable to fetch our place object after making a successful moveTo request,
* we attempt to rectify the failure by moving back to the last known working location. Because
* entites that cooperate with the location director may need to become involved in this
* failure recovery, we provide this interface whereby they can interject themseves into the
* failure recovery process and do their own failure recovery.
*/
public void setFailureHandler (FailureHandler handler)
{
if (_failureHandler != null) {
log.warning("Requested to set failure handler, but we've already got one. The " +
"conflicting entities will likely need to perform more sophisticated " +
"coordination to deal with failures.",
"old", _failureHandler, "new", handler);
} else {
_failureHandler = handler;
}
}
protected void handleFailure (final int placeId, final String reason)
{
_observers.apply(new ObserverOp<LocationObserver>() {
public boolean apply (LocationObserver obs) {
obs.locationChangeFailed(placeId, reason);
return true;
}
});
// try to return to our previous location
if (_failureHandler != null) {
_failureHandler.recoverFailedMove(placeId);
} else if (_placeId <= 0) {
// if we were previously somewhere (and that somewhere isn't where we just tried to
// go), try going back to that happy place
if (_previousPlaceId != -1 && _previousPlaceId != placeId) {
moveTo(_previousPlaceId);
}
} // else we're currently somewhere, so just stay there
}
/**
* Called to create our place controller using the supplied place configuration. This lives in
* a separate method so that derived instances can do funny class loader business if necessary
* to load the place controller using a sandboxed class loader.
*/
protected PlaceController createController (PlaceConfig config)
{
return config.createController();
}
public void addPendingForcedMove (Runnable move)
{
_pendingForcedMoves.add(move);
}
protected void handlePendingForcedMove ()
{
if (!_pendingForcedMoves.isEmpty()) {
_ctx.getClient().getRunQueue().postRunnable(_pendingForcedMoves.remove(0));
}
}
/** The context through which we access needed services. */
protected CrowdContext _ctx;
/** Provides access to location services. */
protected LocationService _lservice;
/** Our location observer list. */
protected ObserverList<LocationObserver> _observers = new ObserverList<LocationObserver>(
ObserverList.SAFE_IN_ORDER_NOTIFY);
/** Used to subscribe to our place object. */
protected SafeSubscriber<PlaceObject> _subber;
/** The oid of the place we currently occupy. */
protected int _placeId = -1;
/** The place object that we currently occupy. */
protected PlaceObject _plobj;
/** The place controller in effect for our current place. */
protected PlaceController _controller;
/** The oid of the place for which we have an outstanding moveTo request, or -1 if we have no
* outstanding request. */
protected int _pendingPlaceId = -1;
/** The oid of the place we previously occupied. */
protected int _previousPlaceId = -1;
/** The last time we requested a move to. */
protected long _lastRequestTime;
/** The entity that deals when we fail to subscribe to a place object. */
protected FailureHandler _failureHandler;
/** A listener that wants to know if we succeeded or how we failed to move. */
protected ResultListener<PlaceConfig> _moveListener;
/** Forced move actions we should take once we complete the move we're in the middle of. */
protected ArrayList<Runnable> _pendingForcedMoves = Lists.newArrayList();
/** The operation used to inform observers that the location changed. */
protected ObserverOp<LocationObserver> _didChangeOp = new ObserverOp<LocationObserver>() {
public boolean apply (LocationObserver obs) {
obs.locationDidChange(_plobj);
return true;
}
};
/** We require that a moveTo request be outstanding for one minute before it is declared to be
* stale. */
protected static final long STALE_REQUEST_DURATION = 60L * 1000L;
}
@@ -0,0 +1,67 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import com.threerings.crowd.data.PlaceObject;
/**
* The location observer interface makes it possible for entities to be
* notified when the client moves to a new location. It also provides a
* means for an entity to participate in the ratification process of a new
* location. Observers may opt to reject a request to change to a new
* location, probably because something is going on in the previous
* location that should not be abandoned.
*
* <p> Note that these location callbacks occur on the main thread and
* should execute quickly and not block under any circumstance.
*/
public interface LocationObserver
{
/**
* Called when someone has requested that we switch to a new location.
* An observer may choose to veto the location change request for some
* reason or other.
*
* @return true if it's OK for the location to change, false if the
* change request should be aborted.
*/
boolean locationMayChange (int placeId);
/**
* Called when we have switched to a new location.
*
* @param place the place object that represents the new location or
* null if we have switched to no location.
*/
void locationDidChange (PlaceObject place);
/**
* This is called on all location observers when a location change
* request is rejected by the server or fails for some other reason.
*
* @param placeId the place id to which we attempted to relocate, but
* failed.
* @param reason the reason code that explains why the location change
* request was rejected or otherwise failed.
*/
void locationChangeFailed (int placeId, String reason);
}
@@ -0,0 +1,39 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import com.threerings.presents.client.InvocationReceiver;
/**
* Defines, for the location services, a set of notifications delivered
* asynchronously by the server to the client.
*/
public interface LocationReceiver extends InvocationReceiver
{
/**
* Used to communicate a required move notification to the client. The
* server will have removed the client from their existing location
* and the client is then responsible for generating a {@link
* LocationService#moveTo} request to move to the new location.
*/
void forcedMove (int placeId);
}
@@ -0,0 +1,61 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
import com.threerings.crowd.data.PlaceConfig;
/**
* The location services provide a mechanism by which the client can request to move from place to
* place in the server. These services should not be used directly, but instead should be accessed
* via the {@link LocationDirector}.
*/
public interface LocationService extends InvocationService
{
/**
* Used to communicate responses to {@link LocationService#moveTo} requests.
*/
public static interface MoveListener extends InvocationListener
{
/**
* Called in response to a successful {@link LocationService#moveTo} request.
*/
void moveSucceeded (PlaceConfig config);
}
/**
* Requests that this client's body be moved to the specified location.
*
* @param client a reference to the client object that defines the context in which this
* invocation service should be executed.
* @param placeId the object id of the place object to which the body should be moved.
* @param listener the listener that will be informed of success or failure.
*/
void moveTo (Client client, int placeId, MoveListener listener);
/**
* Requests that we leave our current place and move to nowhere land.
*/
void leavePlace (Client client);
}
@@ -0,0 +1,33 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
/**
* An exception that indicates that the server did not allow us to move.
*/
public class MoveFailedException extends Exception
{
public MoveFailedException (String message)
{
super(message);
}
}
@@ -0,0 +1,29 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
/**
* An exception that indicates that a LocationObserver vetoed our move request.
*/
public class MoveVetoedException extends Exception
{
}
@@ -0,0 +1,49 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import com.threerings.crowd.data.OccupantInfo;
/**
* The occupant adapter makes life easier for occupant observer classes
* that only care about one or two of the occupant observer
* callbacks. They can either extend occupant adapter or create an
* anonymous class that extends it and overrides just the callbacks they
* care about.
*/
public class OccupantAdapter implements OccupantObserver
{
// documentation inherited from interface
public void occupantEntered (OccupantInfo info)
{
}
// documentation inherited from interface
public void occupantLeft (OccupantInfo info)
{
}
// documentation inherited from interface
public void occupantUpdated (OccupantInfo oinfo, OccupantInfo info)
{
}
}
@@ -0,0 +1,215 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import com.samskivert.util.ObserverList;
import com.threerings.util.Name;
import com.threerings.presents.client.BasicDirector;
import com.threerings.presents.client.Client;
import com.threerings.presents.dobj.EntryAddedEvent;
import com.threerings.presents.dobj.EntryRemovedEvent;
import com.threerings.presents.dobj.EntryUpdatedEvent;
import com.threerings.presents.dobj.SetListener;
import com.threerings.crowd.data.OccupantInfo;
import com.threerings.crowd.data.PlaceObject;
import com.threerings.crowd.util.CrowdContext;
/**
* The occupant director listens for occupants of places to enter and
* exit, and dispatches notices to interested parties about these events.
*
* <p> It will eventually provide a framework for keeping track of
* occupant information in a network efficient manner. The idea being that
* we want to store as little information about occupants as possible in
* the place object (probably just body oid and username), but upon
* entering a place, this will be all we know about the occupants. We then
* dispatch a request to get information about all of the occupants in the
* room (things like avatar information for a graphical display or perhaps
* their ratings in the game that is associated with a place for a gaming
* site) which we then pass on to the occupant observers when it becomes
* available.
*
* <p> This information would be cached and we could return cached
* information for occupants for which we have cached info. We will
* probably want to still make a request for the occupant info so that we
* can update non-static occupant data rather than permanently using
* what's in the cache.
*/
public class OccupantDirector extends BasicDirector
implements LocationObserver, SetListener<OccupantInfo>
{
/**
* Constructs a new occupant director with the supplied context.
*/
public OccupantDirector (CrowdContext ctx)
{
super(ctx);
// register ourselves as a location observer
ctx.getLocationDirector().addLocationObserver(this);
}
/**
* Adds the specified occupant observer to the list.
*/
public void addOccupantObserver (OccupantObserver obs)
{
_observers.add(obs);
}
/**
* Removes the specified occupant observer from the list.
*/
public void removeOccupantObserver (OccupantObserver obs)
{
_observers.remove(obs);
}
/**
* Returns the occupant info for the user in question if it exists in
* the currently occupied place. Returns null if no occupant info
* exists for the specified body.
*/
public OccupantInfo getOccupantInfo (int bodyOid)
{
// make sure we're somewhere
return (_place == null) ? null : _place.occupantInfo.get(Integer.valueOf(bodyOid));
}
/**
* Returns the occupant info for the user in question if it exists in
* the currently occupied place. Returns null if no occupant info
* exists with the specified username.
*/
public OccupantInfo getOccupantInfo (Name username)
{
return (_place == null) ? null : _place.getOccupantInfo(username);
}
@Override
public void clientDidLogoff (Client client)
{
// clear things out
if (_place != null) {
_place.removeListener(this);
_place = null;
}
}
// inherit documentation
public boolean locationMayChange (int placeId)
{
// we've got no opinion
return true;
}
// inherit documentation
public void locationDidChange (PlaceObject place)
{
// unlisten to the old place object if there was one
if (_place != null) {
_place.removeListener(this);
}
// listen to the new one
_place = place;
if (_place != null) {
_place.addListener(this);
}
}
// inherit documentation
public void locationChangeFailed (int placeId, String reason)
{
// nothing to do here either
}
/**
* Deals with all of the processing when an occupant shows up.
*/
public void entryAdded (EntryAddedEvent<OccupantInfo> event)
{
// bail if this isn't for the OCCUPANT_INFO field
if (!event.getName().equals(PlaceObject.OCCUPANT_INFO)) {
return;
}
// now let the occupant observers know what's up
final OccupantInfo info = event.getEntry();
_observers.apply(new ObserverList.ObserverOp<OccupantObserver>() {
public boolean apply (OccupantObserver observer) {
observer.occupantEntered(info);
return true;
}
});
}
/**
* Deals with all of the processing when an occupant is updated.
*/
public void entryUpdated (EntryUpdatedEvent<OccupantInfo> event)
{
// bail if this isn't for the OCCUPANT_INFO field
if (!event.getName().equals(PlaceObject.OCCUPANT_INFO)) {
return;
}
// now let the occupant observers know what's up
final OccupantInfo info = event.getEntry();
final OccupantInfo oinfo = event.getOldEntry();
_observers.apply(new ObserverList.ObserverOp<OccupantObserver>() {
public boolean apply (OccupantObserver observer) {
observer.occupantUpdated(oinfo, info);
return true;
}
});
}
/**
* Deals with all of the processing when an occupant leaves.
*/
public void entryRemoved (EntryRemovedEvent<OccupantInfo> event)
{
// bail if this isn't for the OCCUPANT_INFO field
if (!event.getName().equals(PlaceObject.OCCUPANT_INFO)) {
return;
}
// let the occupant observers know what's up
final OccupantInfo oinfo = event.getOldEntry();
_observers.apply(new ObserverList.ObserverOp<OccupantObserver>() {
public boolean apply (OccupantObserver observer) {
observer.occupantLeft(oinfo);
return true;
}
});
}
/** The occupant observers to keep abreast of occupant antics. */
protected ObserverList<OccupantObserver> _observers = ObserverList.newSafeInOrder();
/** The user's current location. */
protected PlaceObject _place;
}
@@ -0,0 +1,50 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import com.threerings.crowd.data.OccupantInfo;
/**
* An entity that is interested in hearing about bodies that enter and leave a location (as well
* as disconnect and reconnect) can implement this interface and register itself with the
* {@link OccupantDirector}.
*/
public interface OccupantObserver
{
/**
* Called when a body enters the place.
*/
void occupantEntered (OccupantInfo info);
/**
* Called when a body leaves the place.
*/
void occupantLeft (OccupantInfo info);
/**
* Called when an occupant is updated.
*
* @param oldinfo the occupant info prior to the update.
* @param newinfo the newly update info record.
*/
void occupantUpdated (OccupantInfo oldinfo, OccupantInfo newinfo);
}
@@ -0,0 +1,276 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import java.util.ArrayList;
import java.awt.event.ActionEvent;
import com.google.common.collect.Lists;
import com.samskivert.swing.Controller;
import com.threerings.crowd.data.PlaceConfig;
import com.threerings.crowd.data.PlaceObject;
import com.threerings.crowd.util.CrowdContext;
/**
* Controls the user interface that is used to display a place. When the client moves to a new
* place, the appropriate place controller is constructed and requested to create and display the
* user interface for that place.
*/
public abstract class PlaceController extends Controller
{
/**
* Used to call methods in delegates.
*/
public static abstract class DelegateOp
{
public DelegateOp (Class<? extends PlaceControllerDelegate> delegateClass) {
_delegateClass = delegateClass;
}
/** Applies an operation to the supplied delegate. */
public abstract void apply (PlaceControllerDelegate delegate);
public boolean shouldApply (PlaceControllerDelegate delegate) {
return _delegateClass.isInstance(delegate);
}
protected Class<? extends PlaceControllerDelegate> _delegateClass;
}
/**
* Initializes this place controller with a reference to the context that they can use to
* access client services and to the configuration record for this place. The controller
* should create as much of its user interface that it can without having access to the place
* object because this will be invoked in parallel with the fetching of the place object. When
* the place object is obtained, the controller will be notified and it can then finish the
* user interface configuration and put the user interface into operation.
*
* @param ctx the client context.
* @param config the place configuration for this place.
*/
public void init (CrowdContext ctx, PlaceConfig config)
{
// keep these around
_ctx = ctx;
_config = config;
// create our user interface
_view = createPlaceView(_ctx);
// initialize our delegates
applyToDelegates(new DelegateOp(PlaceControllerDelegate.class) {
@Override
public void apply (PlaceControllerDelegate delegate) {
delegate.init(_ctx, _config);
}
});
// let the derived classes do any initialization stuff
didInit();
}
/**
* Derived classes can override this and perform any post-initialization processing they might
* need. They should of course be sure to call <code>super.didInit()</code>.
*/
protected void didInit ()
{
}
/**
* Returns a reference to the place view associated with this controller. This is only valid
* after a call has been made to {@link #init}.
*/
public PlaceView getPlaceView ()
{
return _view;
}
/**
* Returns the {@link PlaceConfig} associated with this place.
*/
public PlaceConfig getPlaceConfig ()
{
return _config;
}
/**
* Creates the user interface that will be used to display this place. The view instance
* returned will later be configured with the place object, once it becomes available.
*
* @param ctx a reference to the {@link CrowdContext} associated with this controller.
*/
protected PlaceView createPlaceView (CrowdContext ctx)
{
return createPlaceView();
}
/**
* Obsolete but retained for runtime compatibility with the old and busted.
*
* @deprecated Use {@link #createPlaceView(CrowdContext)}.
*/
@Deprecated
protected PlaceView createPlaceView ()
{
return null;
}
/**
* This is called by the location director once the place object has been fetched. The place
* controller will dispatch the place object to the user interface hierarchy via
* {@link PlaceViewUtil#dispatchWillEnterPlace}. Derived classes can override this and perform
* any other starting up that they need to do
*/
public void willEnterPlace (final PlaceObject plobj)
{
// keep a handle on our place object
_plobj = plobj;
if (_view != null) {
// let the UI hierarchy know that we've got our place
PlaceViewUtil.dispatchWillEnterPlace(_view, plobj);
// and display the user interface
_ctx.setPlaceView(_view);
}
// let our delegates know what's up
applyToDelegates(new DelegateOp(PlaceControllerDelegate.class) {
@Override
public void apply (PlaceControllerDelegate delegate) {
delegate.willEnterPlace(plobj);
}
});
}
/**
* Called before a request is submitted to the server to leave the current place. As such,
* this method may be called multiple times before {@link #didLeavePlace} is finally called.
* The request to leave may be rejected, but if a place controller needs to flush any
* information to the place manager before it leaves, it should so do here. This is the only
* place in which the controller is guaranteed to be able to communicate to the place manager,
* as by the time {@link #didLeavePlace} is called, the place manager may have already been
* destroyed.
*/
public void mayLeavePlace (final PlaceObject plobj)
{
// let our delegates know what's up
applyToDelegates(new DelegateOp(PlaceControllerDelegate.class) {
@Override
public void apply (PlaceControllerDelegate delegate) {
delegate.mayLeavePlace(plobj);
}
});
}
/**
* This is called by the location director when we are leaving this place and need to clean up
* after ourselves and shutdown. Derived classes should override this method (being sure to
* call <code>super.didLeavePlace</code>) and perform any necessary cleanup.
*/
public void didLeavePlace (final PlaceObject plobj)
{
// let our delegates know what's up
applyToDelegates(new DelegateOp(PlaceControllerDelegate.class) {
@Override
public void apply (PlaceControllerDelegate delegate) {
delegate.didLeavePlace(plobj);
}
});
// let the UI hierarchy know that we're outta here
if (_view != null) {
PlaceViewUtil.dispatchDidLeavePlace(_view, plobj);
_ctx.clearPlaceView(_view);
_view = null;
}
_plobj = null;
}
/**
* Handles basic place controller action events. Derived classes should be sure to call
* <code>super.handleAction</code> for events they don't specifically handle.
*/
@Override
public boolean handleAction (final ActionEvent action)
{
final boolean[] handled = new boolean[1];
// let our delegates have a crack at the action
applyToDelegates(new DelegateOp(PlaceControllerDelegate.class) {
@Override
public void apply (PlaceControllerDelegate delegate) {
// we take advantage of short-circuiting here
handled[0] = handled[0] || delegate.handleAction(action);
}
});
// if they didn't handle it, pass it off to the super class
return handled[0] || super.handleAction(action);
}
/**
* Adds the supplied delegate to the list for this controller.
*/
protected void addDelegate (PlaceControllerDelegate delegate)
{
if (_delegates == null) {
_delegates = Lists.newArrayList();
}
_delegates.add(delegate);
}
/**
* Applies the supplied operation to the registered delegates.
*/
protected void applyToDelegates (DelegateOp op)
{
if (_delegates != null) {
for (int ii = 0, ll = _delegates.size(); ii < ll; ii++) {
PlaceControllerDelegate delegate = _delegates.get(ii);
if (op.shouldApply(delegate)) {
op.apply(delegate);
}
}
}
}
/** A reference to the active client context. */
protected CrowdContext _ctx;
/** A reference to our place configuration. */
protected PlaceConfig _config;
/** A reference to the place object for which we're controlling a user
* interface. */
protected PlaceObject _plobj;
/** A reference to the root user interface component. */
protected PlaceView _view;
/** A list of the delegates in use by this controller. */
protected ArrayList<PlaceControllerDelegate> _delegates;
}
@@ -0,0 +1,98 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import java.awt.event.ActionEvent;
import com.threerings.crowd.data.PlaceConfig;
import com.threerings.crowd.data.PlaceObject;
import com.threerings.crowd.util.CrowdContext;
/**
* Provides an extensible mechanism for encapsulating delegated
* functionality that works with the place services.
*
* <p> Thanks to Java's lack of multiple inheritance, it will likely
* become necessary to factor certain services that might be used by a
* variety of {@link PlaceController} derived classes into delegate
* classes because they do not fit into the single inheritance hierarchy
* that makes sense for a particular application. To facilitate this
* process, this delegate class is provided which the standard place
* controller can be made to call out to for all of the standard methods.
*/
public class PlaceControllerDelegate
{
/**
* Constructs the delegate with the controller for which it is
* delegating.
*/
public PlaceControllerDelegate (PlaceController controller)
{
_controller = controller;
}
/**
* Called to initialize the delegate.
*/
public void init (CrowdContext ctx, PlaceConfig config)
{
}
/**
* Called to let the delegate know that we're entering a place.
*/
public void willEnterPlace (PlaceObject plobj)
{
}
/**
* Called before a request is submitted to the server to leave the
* current place. The request to leave may be rejected, but if a place
* controller needs to make a final communication to the place manager
* before it leaves, it should so do here. This is the only place in
* which the controller is guaranteed to be able to communicate to the
* place manager, as by the time {@link #didLeavePlace} is called, the
* place manager may have already been destroyed.
*/
public void mayLeavePlace (final PlaceObject plobj)
{
}
/**
* Called to let the delegate know that we've left the place.
*/
public void didLeavePlace (PlaceObject plobj)
{
}
/**
* Called to give the delegate a chance to handle controller actions
* that weren't handled by the main controller.
*/
public boolean handleAction (ActionEvent action)
{
return false;
}
/** A reference to the controller for which we are delegating. */
protected PlaceController _controller;
}
@@ -0,0 +1,65 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 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.client;
import javax.swing.JPanel;
import com.samskivert.swing.Controller;
import com.samskivert.swing.ControllerProvider;
import com.threerings.crowd.data.PlaceObject;
/**
* A useful base class for client interfaces which wish to make use of a
* {@link JPanel} as their top-level {@link PlaceView}.
*/
public class PlacePanel extends JPanel
implements ControllerProvider, PlaceView
{
/**
* Constructs a place panel with the specified controller which will
* be made availabel via the {@link ControllerProvider} interface.
*/
public PlacePanel (PlaceController controller)
{
_controller = controller;
}
// documentation inherited from interface
public Controller getController ()
{
return _controller;
}
// documentation inherited from interface
public void willEnterPlace (PlaceObject plobj)
{
}
// documentation inherited from interface
public void didLeavePlace (PlaceObject plobj)
{
}
/** A reference to the controller with which we interoperate. */
protected PlaceController _controller;
}

Some files were not shown because too many files have changed in this diff Show More