GWT frontend and servlet for editing runtime configuration.
git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@6178 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
<module>
|
||||||
|
<inherits name="com.threerings.web.Base"/>
|
||||||
|
|
||||||
|
<source path="client"/>
|
||||||
|
<source path="gwt"/>
|
||||||
|
</module>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
//
|
||||||
|
// $Id: $
|
||||||
|
|
||||||
|
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,107 @@
|
|||||||
|
//
|
||||||
|
// $Id: $
|
||||||
|
|
||||||
|
|
||||||
|
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,196 @@
|
|||||||
|
//
|
||||||
|
// $Id: $
|
||||||
|
|
||||||
|
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.isEmpty() && _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 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,100 @@
|
|||||||
|
//
|
||||||
|
// $Id: $
|
||||||
|
|
||||||
|
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,51 @@
|
|||||||
|
//
|
||||||
|
// $Id: $
|
||||||
|
|
||||||
|
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,26 @@
|
|||||||
|
//
|
||||||
|
// $Id: $
|
||||||
|
|
||||||
|
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,162 @@
|
|||||||
|
//
|
||||||
|
// $Id: $
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user