From: Henri Sara Date: Tue, 31 Jan 2012 12:46:15 +0000 (+0200) Subject: Implement simple shared state as a Map (#8304) - work in progress X-Git-Tag: 7.0.0.alpha2~440^2~27 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=c763891bc1535f62e524b4d4ba5837ab53817dd9;p=vaadin-framework.git Implement simple shared state as a Map (#8304) - work in progress In this version, SharedState can return a map that will be transferred. On the client side, shared state is deserialized from the map but not used. Shared state is not sent for nested components that have not been painted yet - doing so requires refactoring of painting of components. --- diff --git a/src/com/vaadin/data/Buffered.java b/src/com/vaadin/data/Buffered.java index 22a4b6f9f7..f6398f585b 100644 --- a/src/com/vaadin/data/Buffered.java +++ b/src/com/vaadin/data/Buffered.java @@ -11,6 +11,7 @@ import com.vaadin.terminal.ErrorMessage; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; import com.vaadin.terminal.SystemError; +import com.vaadin.terminal.gwt.client.communication.SharedState; /** *

@@ -332,6 +333,11 @@ public interface Buffered extends Serializable { } + public SharedState getState() { + // TODO implement: move relevant parts from paint() to getState() + return null; + } + /* Documented in super interface */ public void addListener(RepaintRequestListener listener) { } diff --git a/src/com/vaadin/data/Validator.java b/src/com/vaadin/data/Validator.java index 36820c1caa..bc64594edb 100644 --- a/src/com/vaadin/data/Validator.java +++ b/src/com/vaadin/data/Validator.java @@ -9,6 +9,7 @@ import java.io.Serializable; import com.vaadin.terminal.ErrorMessage; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.gwt.client.communication.SharedState; import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; /** @@ -167,6 +168,11 @@ public interface Validator extends Serializable { target.endTag("error"); } + public SharedState getState() { + // TODO implement: move relevant parts from paint() to getState() + return null; + } + /** * Returns the message of the error in HTML. * diff --git a/src/com/vaadin/terminal/CompositeErrorMessage.java b/src/com/vaadin/terminal/CompositeErrorMessage.java index 94f4e3a5d5..2ec1fc949e 100644 --- a/src/com/vaadin/terminal/CompositeErrorMessage.java +++ b/src/com/vaadin/terminal/CompositeErrorMessage.java @@ -10,6 +10,8 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import com.vaadin.terminal.gwt.client.communication.SharedState; + /** * Class for combining multiple error messages together. * @@ -131,6 +133,11 @@ public class CompositeErrorMessage implements ErrorMessage, Serializable { } } + public SharedState getState() { + // TODO implement: move relevant parts from paint() to getState() + return null; + } + /* Documented in super interface */ public void addListener(RepaintRequestListener listener) { } diff --git a/src/com/vaadin/terminal/Paintable.java b/src/com/vaadin/terminal/Paintable.java index d043cb2606..9026a45ca3 100644 --- a/src/com/vaadin/terminal/Paintable.java +++ b/src/com/vaadin/terminal/Paintable.java @@ -7,6 +7,8 @@ package com.vaadin.terminal; import java.io.Serializable; import java.util.EventObject; +import com.vaadin.terminal.gwt.client.communication.SharedState; + /** * Interface implemented by all classes that can be painted. Classes * implementing this interface know how to output themselves to a UIDL stream @@ -39,6 +41,19 @@ public interface Paintable extends java.util.EventListener, Serializable { */ public void paint(PaintTarget target) throws PaintException; + /** + * Returns the current shared state bean for the paintable. The state (or + * changes to it) is communicated from the server to the client when + * components are painted. + * + * Subclasses can use a more specific return type for this method. + * + * @return shared state instance or null + * + * @since 7.0 + */ + public SharedState getState(); + /** * Requests that the paintable should be repainted as soon as possible. */ diff --git a/src/com/vaadin/terminal/SystemError.java b/src/com/vaadin/terminal/SystemError.java index 7f2464c9b9..55e62abb0a 100644 --- a/src/com/vaadin/terminal/SystemError.java +++ b/src/com/vaadin/terminal/SystemError.java @@ -7,6 +7,7 @@ package com.vaadin.terminal; import java.io.PrintWriter; import java.io.StringWriter; +import com.vaadin.terminal.gwt.client.communication.SharedState; import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; /** @@ -90,6 +91,11 @@ public class SystemError extends RuntimeException implements ErrorMessage { } + public SharedState getState() { + // TODO implement: move relevant parts from paint() to getState() + return null; + } + /** * Returns the message of the error in HTML. * diff --git a/src/com/vaadin/terminal/UserError.java b/src/com/vaadin/terminal/UserError.java index 8ec45ac725..50cf32ab0c 100644 --- a/src/com/vaadin/terminal/UserError.java +++ b/src/com/vaadin/terminal/UserError.java @@ -4,6 +4,7 @@ package com.vaadin.terminal; +import com.vaadin.terminal.gwt.client.communication.SharedState; import com.vaadin.terminal.gwt.server.AbstractApplicationServlet; /** @@ -143,6 +144,11 @@ public class UserError implements ErrorMessage { target.endTag("error"); } + public SharedState getState() { + // TODO implement: move relevant parts from paint() to getState() + return null; + } + /* Documented in interface */ public void requestRepaintRequests() { } diff --git a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java index e790c1d3fc..68ec1b5434 100644 --- a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java +++ b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java @@ -40,8 +40,10 @@ import com.google.gwt.user.client.ui.Widget; import com.vaadin.terminal.gwt.client.ApplicationConfiguration.ErrorMessage; import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize; import com.vaadin.terminal.gwt.client.RenderInformation.Size; +import com.vaadin.terminal.gwt.client.communication.JsonDecoder; import com.vaadin.terminal.gwt.client.communication.JsonEncoder; import com.vaadin.terminal.gwt.client.communication.MethodInvocation; +import com.vaadin.terminal.gwt.client.communication.SharedState; import com.vaadin.terminal.gwt.client.ui.Field; import com.vaadin.terminal.gwt.client.ui.VAbstractPaintableWidget; import com.vaadin.terminal.gwt.client.ui.VContextMenu; @@ -996,6 +998,31 @@ public class ApplicationConnection { redirectTimer.schedule(1000 * sessionExpirationInterval); } + // TODO implement shared state handling + + // map from paintable id to its shared state instance + Map sharedStates = new HashMap(); + + // TODO Cache shared state (if any) - components might not exist + // TODO cleanup later: keep only used states + ValueMap states = json.getValueMap("state"); + JsArrayString keyArray = states.getKeyArray(); + for (int i = 0; i < keyArray.length(); i++) { + String paintableId = keyArray.get(i); + // TODO handle as a ValueMap or similar object and native + // JavaScript processing? + JavaScriptObject value = states + .getJavaScriptObject(paintableId); + // TODO implement with shared state subclasses + SharedState state = GWT.create(SharedState.class); + Map stateMap = (Map) JsonDecoder + .convertValue(new JSONArray(value), + getPaintableMap()); + state.setState(stateMap); + // TODO cache state to temporary stateMap + sharedStates.put(paintableId, state); + } + // Process changes JsArray changes = json.getJSValueMapArray("changes"); diff --git a/src/com/vaadin/terminal/gwt/client/ComponentState.java b/src/com/vaadin/terminal/gwt/client/ComponentState.java new file mode 100644 index 0000000000..955833e48f --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ComponentState.java @@ -0,0 +1,29 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import com.vaadin.terminal.gwt.client.communication.SharedState; + +/** + * Default shared state implementation for UI components. + * + * State classes of concrete components should extend this class. + * + * @since 7.0 + */ +public class ComponentState extends SharedState { + // TODO more javadoc + + // TODO constants for the state attributes for now + public static final String STATE_HEIGHT = "height"; + public static final String STATE_WIDTH = "width"; + public static final String STATE_STYLE = "style"; + public static final String STATE_READONLY = "readonly"; + public static final String STATE_IMMEDIATE = "immediate"; + public static final String STATE_DISABLED = "disabled"; + public static final String STATE_CAPTION = "caption"; + public static final String STATE_DESCRIPTION = "description"; + +} diff --git a/src/com/vaadin/terminal/gwt/client/VPaintableMap.java b/src/com/vaadin/terminal/gwt/client/VPaintableMap.java index f21d85558c..f9d05e592d 100644 --- a/src/com/vaadin/terminal/gwt/client/VPaintableMap.java +++ b/src/com/vaadin/terminal/gwt/client/VPaintableMap.java @@ -236,6 +236,7 @@ public class VPaintableMap { idToComponentDetail.remove(pid); idToPaintable.remove(pid); paintableToId.remove(paintable); + // TODO purge shared state for pid } /* * else NOP : same component has been reattached to another diff --git a/src/com/vaadin/terminal/gwt/client/ValueMap.java b/src/com/vaadin/terminal/gwt/client/ValueMap.java index 8d14ef57ce..5deb5feb55 100644 --- a/src/com/vaadin/terminal/gwt/client/ValueMap.java +++ b/src/com/vaadin/terminal/gwt/client/ValueMap.java @@ -101,4 +101,9 @@ public final class ValueMap extends JavaScriptObject { return '' + this[name]; }-*/; + native JavaScriptObject getJavaScriptObject(String name) + /*-{ + return this[name]; + }-*/; + } \ No newline at end of file diff --git a/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java b/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java index e7fea08bbe..7ed212af49 100644 --- a/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java +++ b/src/com/vaadin/terminal/gwt/client/communication/JsonDecoder.java @@ -37,13 +37,13 @@ public class JsonDecoder { * mapper between paintable ID and {@link VPaintable} objects * @return converted value (does not contain JSON types) */ - public static Object convertVariableValue(JSONArray value, - VPaintableMap idMapper) { - return convertVariableValue(((JSONString) value.get(0)).stringValue() - .charAt(0), value.get(1), idMapper); + public static Object convertValue(JSONArray value, VPaintableMap idMapper) { + return convertValue( + ((JSONString) value.get(0)).stringValue().charAt(0), + value.get(1), idMapper); } - private static Object convertVariableValue(char variableType, Object value, + private static Object convertValue(char variableType, Object value, VPaintableMap idMapper) { Object val = null; // TODO type checks etc. @@ -84,6 +84,10 @@ public class JsonDecoder { // TODO handle properly val = idMapper.getPaintable(String.valueOf(value)); break; + case JsonEncoder.VTYPE_SHAREDSTATE: + val = convertMap((JSONObject) value, idMapper); + // TODO convert to a SharedState instance + break; } return val; @@ -94,8 +98,7 @@ public class JsonDecoder { Iterator it = jsonMap.keySet().iterator(); while (it.hasNext()) { String key = it.next(); - map.put(key, - convertVariableValue((JSONArray) jsonMap.get(key), idMapper)); + map.put(key, convertValue((JSONArray) jsonMap.get(key), idMapper)); } return map; } @@ -115,7 +118,7 @@ public class JsonDecoder { for (int i = 0; i < jsonArray.size(); ++i) { // each entry always has two elements: type and value JSONArray entryArray = (JSONArray) jsonArray.get(i); - tokens.add(convertVariableValue(entryArray, idMapper)); + tokens.add(convertValue(entryArray, idMapper)); } return tokens.toArray(new Object[tokens.size()]); } diff --git a/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java b/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java index 4b26d5939f..61af9294d9 100644 --- a/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java +++ b/src/com/vaadin/terminal/gwt/client/communication/JsonEncoder.java @@ -38,6 +38,8 @@ public class JsonEncoder { public static final char VTYPE_ARRAY = 'a'; public static final char VTYPE_STRINGARRAY = 'c'; public static final char VTYPE_MAP = 'm'; + // TODO this will be replaced by the shared state class name + public static final char VTYPE_SHAREDSTATE = 't'; // TODO is this needed? public static final char VTYPE_UNDEFINED = 'u'; diff --git a/src/com/vaadin/terminal/gwt/client/communication/SharedState.java b/src/com/vaadin/terminal/gwt/client/communication/SharedState.java new file mode 100644 index 0000000000..38183ba50d --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/communication/SharedState.java @@ -0,0 +1,69 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.communication; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import com.google.gwt.core.client.GWT; +import com.vaadin.terminal.gwt.client.ui.VAbstractPaintableWidget; + +/** + * Interface to be implemented by all shared state classes used to communicate + * basic information about a paintable from server to client. These typically + * replace most of the semi-static information sent via the paintContent() and + * updateFromUIDL() mechanism in Vaadin 6 (component sizes, captions, tooltips, + * etc.). + * + * Shared state classes have to be declared in client side packages to be + * accessible both for server and client code. They can be static nested classes + * of the client side widget. + * + * Shared state objects are only sent from the server to the client, and any + * modifications from the client should be performed via an RPC call that + * modifies the authoritative state on the server. + * + * In current Vaadin versions, the whole shared state is sent every time the + * component is painted. Future versions may optimize this so that only the + * necessary (changed or missing on the client side) parts are re-sent to the + * client, but the client will have access to the whole state. + * + * TODO the rest of the javadoc corresponds to the design that is not yet + * implemented + * + * A shared state class should be a bean with getters and setters for each + * field, and should only contain simple data types, or arrays or maps of + * supported data types. + * + * On the client side, SharedState instances must be created using + * {@link GWT#create(Class)} to let a generator create custom deserialization + * support for them. For most widgets, + * {@link VAbstractPaintableWidget#createSharedState()} method should be + * overridden to create a shared state instance of the correct type using + * {@link GWT#create(Class)}. + * + * Subclasses of a paintable using shared state should also provide a subclass + * of the shared state class of the parent class to extend the state - a single + * paintable can only have one shared state object. + * + * Future versions of the shared state mechanism may also support custom data + * types as fields of a shared state class. + * + * @since 7.0 + */ +public class SharedState implements Serializable { + private Map state = new HashMap(); + + // TODO temporary until reflection based serialization is implemented + public Map getState() { + return state; + } + + // TODO temporary until generator based deserialization is implemented + public void setState(Map stateMap) { + state = stateMap; + } +} diff --git a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java b/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java index 0d25b48422..168d6543f9 100644 --- a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java @@ -63,6 +63,7 @@ import com.vaadin.terminal.WrappedRequest; import com.vaadin.terminal.WrappedResponse; import com.vaadin.terminal.gwt.client.ApplicationConnection; import com.vaadin.terminal.gwt.client.communication.MethodInvocation; +import com.vaadin.terminal.gwt.client.communication.SharedState; import com.vaadin.terminal.gwt.server.BootstrapHandler.BootstrapContext; import com.vaadin.terminal.gwt.server.ComponentSizeValidator.InvalidLayout; import com.vaadin.ui.AbstractComponent; @@ -808,6 +809,37 @@ public abstract class AbstractCommunicationManager implements }); } + if (paintables != null) { + // paint shared state before changes - for now, send the complete + // state of all modified and new components + + // TODO problem: some components will only be created and registered + // below in the paint phase + + JSONObject sharedStates = new JSONObject(); + for (final Iterator i = paintables.iterator(); i + .hasNext();) { + final Paintable p = i.next(); + String paintableId = getPaintableId(p); + SharedState state = p.getState(); + if (null != state) { + // encode and send shared state + try { + JSONArray stateJsonArray = JsonCodec + .encode(state, this); + sharedStates.put(paintableId, stateJsonArray); + } catch (JSONException e) { + throw new PaintException( + "Failed to serialize shared state for paintable " + + paintableId + ": " + e.getMessage()); + } + } + } + outWriter.print("\"state\":"); + outWriter.append(sharedStates.toString()); + outWriter.print(", "); // close changes + } + outWriter.print("\"changes\":["); List invalidComponentRelativeSizes = null; @@ -877,9 +909,9 @@ public abstract class AbstractCommunicationManager implements } paintTarget.close(); - outWriter.print("]"); // close changes + outWriter.print("], "); // close changes - outWriter.print(", \"meta\" : {"); + outWriter.print("\"meta\" : {"); boolean metaOpen = false; if (repaintAll) { diff --git a/src/com/vaadin/terminal/gwt/server/JsonCodec.java b/src/com/vaadin/terminal/gwt/server/JsonCodec.java index dfe4491400..4d7501e739 100644 --- a/src/com/vaadin/terminal/gwt/server/JsonCodec.java +++ b/src/com/vaadin/terminal/gwt/server/JsonCodec.java @@ -16,6 +16,7 @@ import com.vaadin.external.json.JSONException; import com.vaadin.external.json.JSONObject; import com.vaadin.terminal.Paintable; import com.vaadin.terminal.gwt.client.communication.JsonEncoder; +import com.vaadin.terminal.gwt.client.communication.SharedState; /** * Decoder for converting RPC parameters and other values from JSON in transfer @@ -140,6 +141,11 @@ public class JsonCodec implements Serializable { // TODO as undefined type? return combineTypeAndValue(JsonEncoder.VTYPE_UNDEFINED, JSONObject.NULL); + } else if (value instanceof SharedState) { + // TODO implement by encoding the bean + Map map = ((SharedState) value).getState(); + return combineTypeAndValue(JsonEncoder.VTYPE_SHAREDSTATE, + encodeMapContents(map, idMapper)); } else if (value instanceof String[]) { String[] array = (String[]) value; JSONArray jsonArray = new JSONArray(); @@ -153,20 +159,11 @@ public class JsonCodec implements Serializable { return combineTypeAndValue(JsonEncoder.VTYPE_BOOLEAN, value); } else if (value instanceof Object[]) { Object[] array = (Object[]) value; - JSONArray jsonArray = new JSONArray(); - for (int i = 0; i < array.length; ++i) { - // TODO handle object graph loops? - jsonArray.put(encode(array[i], idMapper)); - } + JSONArray jsonArray = encodeArrayContents(array, idMapper); return combineTypeAndValue(JsonEncoder.VTYPE_ARRAY, jsonArray); } else if (value instanceof Map) { Map map = (Map) value; - JSONObject jsonMap = new JSONObject(); - for (String mapKey : map.keySet()) { - // TODO handle object graph loops? - Object mapValue = map.get(mapKey); - jsonMap.put(mapKey, encode(mapValue, idMapper)); - } + JSONObject jsonMap = encodeMapContents(map, idMapper); return combineTypeAndValue(JsonEncoder.VTYPE_MAP, jsonMap); } else if (value instanceof Paintable) { Paintable paintable = (Paintable) value; @@ -178,6 +175,27 @@ public class JsonCodec implements Serializable { } } + private static JSONArray encodeArrayContents(Object[] array, + PaintableIdMapper idMapper) throws JSONException { + JSONArray jsonArray = new JSONArray(); + for (int i = 0; i < array.length; ++i) { + // TODO handle object graph loops? + jsonArray.put(encode(array[i], idMapper)); + } + return jsonArray; + } + + private static JSONObject encodeMapContents(Map map, + PaintableIdMapper idMapper) throws JSONException { + JSONObject jsonMap = new JSONObject(); + for (String mapKey : map.keySet()) { + // TODO handle object graph loops? + Object mapValue = map.get(mapKey); + jsonMap.put(mapKey, encode(mapValue, idMapper)); + } + return jsonMap; + } + private static JSONArray combineTypeAndValue(char type, Object value) { JSONArray outerArray = new JSONArray(); outerArray.put(String.valueOf(type)); diff --git a/src/com/vaadin/ui/AbstractComponent.java b/src/com/vaadin/ui/AbstractComponent.java index 318bcd7ea2..62be25f73b 100644 --- a/src/com/vaadin/ui/AbstractComponent.java +++ b/src/com/vaadin/ui/AbstractComponent.java @@ -29,6 +29,7 @@ import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; import com.vaadin.terminal.Resource; import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.gwt.client.ComponentState; import com.vaadin.terminal.gwt.server.ComponentSizeValidator; import com.vaadin.terminal.gwt.server.RpcManager; import com.vaadin.terminal.gwt.server.RpcTarget; @@ -163,6 +164,12 @@ public abstract class AbstractComponent implements Component, MethodEventSource */ private Map, RpcManager> rpcManagerMap = new HashMap, RpcManager>(); + /** + * Shared state object to be communicated from the server to the client when + * modified. + */ + private ComponentState sharedState; + /* Constructor */ /** @@ -853,6 +860,88 @@ public abstract class AbstractComponent implements Component, MethodEventSource } + /** + * Returns the shared state bean with information to be sent from the server + * to the client. + * + * Subclasses should override this method and set any relevant fields of the + * state returned by super.getState(). + * + * @since 7.0 + * + * @return updated component shared state + */ + public ComponentState getState() { + if (null == sharedState) { + sharedState = createState(); + } + // basic state: caption, size, enabled, ... + + if (!isVisible()) { + return null; + } + + // TODO for now, this superclass always recreates the state from + // scratch, whereas subclasses should only modify it + Map state = new HashMap(); + + if (getHeight() >= 0 + && (getHeightUnits() != Unit.PERCENTAGE || ComponentSizeValidator + .parentCanDefineHeight(this))) { + state.put(ComponentState.STATE_HEIGHT, "" + getCSSHeight()); + } + + if (getWidth() >= 0 + && (getWidthUnits() != Unit.PERCENTAGE || ComponentSizeValidator + .parentCanDefineWidth(this))) { + state.put(ComponentState.STATE_WIDTH, "" + getCSSWidth()); + } + + if (styles != null && styles.size() > 0) { + state.put(ComponentState.STATE_STYLE, getStyle()); + } + if (isReadOnly()) { + state.put(ComponentState.STATE_READONLY, true); + } + + if (isImmediate()) { + state.put(ComponentState.STATE_IMMEDIATE, true); + } + if (!isEnabled()) { + state.put(ComponentState.STATE_DISABLED, true); + } + if (getCaption() != null) { + state.put(ComponentState.STATE_CAPTION, getCaption()); + } + // TODO add icon (Resource) + + if (getDescription() != null && getDescription().length() > 0) { + state.put(ComponentState.STATE_DESCRIPTION, getDescription()); + } + + sharedState.setState(state); + + return sharedState; + } + + /** + * Creates the shared state bean to be used in server to client + * communication. + * + * Subclasses should implement this method and return a new instance of the + * correct state class. + * + * All configuration of the values of the state should be performed in + * {@link #getState()}, not in {@link #createState()}. + * + * @since 7.0 + * + * @return new shared state object + */ + protected ComponentState createState() { + return new ComponentState(); + } + /* Documentation copied from interface */ public void requestRepaint() {