9d2ca42eac
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
430 lines
17 KiB
Java
430 lines
17 KiB
Java
//
|
|
// $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;
|
|
}
|