org.waveprotocol.wave.client.gadget.StateMap.java Source code

Java tutorial

Introduction

Here is the source code for org.waveprotocol.wave.client.gadget.StateMap.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.waveprotocol.wave.client.gadget;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;

import org.waveprotocol.wave.model.util.ReadableStringMap;
import org.waveprotocol.wave.model.util.ReadableStringMap.ProcV;

/**
 * Overlay and JSON-converter class to hold key-value pairs. Works as
 * String-to-String map. The map may also contain null values in delta maps to
 * indicate keys to be deleted. The implementation makes sure that only
 * string and null values are present at any time. The keys are internally
 * prepended with ":" to avoid overriding JS object properties.
 * TODO(user): Some elements are borrowed from the StringMap class. Consider
 * unifying the JS map classes.
 *
 */
public class StateMap extends JavaScriptObject {
    /**
     * A procedure that accepts a key and the corresponding value from the map,
     * does something with them, and returns a boolean condition.
     *
     * @see StateMap#checkKeyValue(StateMap.CheckKeyValue) Implement this class to
     *      iterate over key-value pairs until either all key-value pairs are
     *      observed or the returned condition is false using the checkKeyValue
     *      method.
     */
    public static interface CheckKeyValue {
        /** The function */
        public boolean check(String key, String value);
    }

    /**
     * A procedure that accepts a key and the corresponding value from the map and
     * does something with them, but does not require to return a boolean
     * condition.
     *
     * Implement this interface to iterate over all key-value pairs using each
     * method.
     */
    public static interface Each {
        /** The procedure */
        public void apply(String key, String value);
    }

    /**
     * Helper class that implements CheckKeyValue to compare received key-value
     * pairs against key-values in a given StateMap.
     */
    public static class KeyValueComparator implements CheckKeyValue {
        private final StateMap otherMap;

        /**
         * Constructs the comparator for a given map.
         *
         * @param otherMap the map to compare key-values against.
         */
        public KeyValueComparator(StateMap otherMap) {
            this.otherMap = otherMap;
        }

        @Override
        public boolean check(String key, String value) {
            return (value == null) ? otherMap.has(key) && (otherMap.get(key) == null)
                    : value.equals(otherMap.get(key));
        }
    }

    /**
     * External construction is banned.
     */
    protected StateMap() {
    }

    /**
     * Creates gadget state object.
     */
    public static StateMap create() {
        return JavaScriptObject.createObject().cast();
    }

    /**
     * Creates gadget state object from a String-to-String map.
     */
    public static StateMap createFromStringMap(ReadableStringMap<String> map) {
        final StateMap stateMap = JavaScriptObject.createObject().cast();
        map.each(new ProcV<String>() {
            @Override
            public void apply(String key, String value) {
                stateMap.put(key, value);
            }
        });
        return stateMap;
    }

    /**
     * Checks whether the key is in the map.
     *
     * @param key the key.
     * @return true if a value indexed by the given key exists in the map.
     */
    public final native boolean has(String key) /*-{
                                                return this.hasOwnProperty(':' + key);
                                                }-*/;

    /**
     * Returns the value corresponding to the key.
     *
     * @param key the key.
     * @return the value that corresponds to the key, or null if not present.
     */
    public final native String get(String key) /*-{
                                               return this[':' + key];
                                               }-*/;

    /**
     * Puts the value in the map at the given key. The value can be null. Key-
     * value pairs with null value return true in has(key) and null in get(key).
     *
     * @param key the key.
     * @param value the value to set.
     */
    public final native void put(String key, String value) /*-{
                                                           this[':' + key] = value;
                                                           }-*/;

    /**
     * Removes the value with the given key from the map.
     *
     * @param key the key to remove.
     */
    public final void remove(String key) {
        if (has(key)) {
            nativeRemove(key);
        }
    }

    /**
     * Modifies the current state with the delta. The delta contains key-value
     * pairs to modify. If the value in the delta is null the key is removed from
     * the current state map.
     *
     * @param delta the delta map.
     */
    public final void applyDelta(StateMap delta) {
        delta.each(new Each() {
            @Override
            public void apply(String key, String value) {
                if (value != null) {
                    put(key, value);
                } else {
                    remove(key);
                }
            }
        });
    }

    /**
     * Calculates the delta that can be applied to the current state map to get
     * the new state map. Note that null values are treated as non-existent in
     * both this and newMap.
     *
     * @param newMap the new map to calculate the delta for.
     * @return the delta that can be applied to the current state map to get the
     *         new state map.
     */
    public final StateMap getDelta(final StateMap newMap) {
        final StateMap result = StateMap.create();
        newMap.each(new Each() {
            @Override
            public void apply(String key, String value) {
                if ((value != null) && !value.equals(get(key))) {
                    result.put(key, value);
                }
            }
        });
        each(new Each() {
            @Override
            public void apply(String key, String value) {
                if ((value != null) && (newMap.get(key) == null)) {
                    result.put(key, null);
                }
            }
        });
        return result;
    }

    /**
     * Compares key-value pairs with the otherState. This comparison is not
     * sensitive to the order in which the pairs are put in JS object. null
     * is not equal to an empty map.
     *
     * TODO(user): Consider implementing as equals; add hashCode.
     *
     * @param otherMap the map to compare to the current one.
     * @return true if the key-value pairs are identical, false otherwise.
     */
    public final boolean compare(final StateMap otherMap) {
        return (otherMap != null) && checkKeyValue(new KeyValueComparator(otherMap))
                && otherMap.checkKeyValue(new KeyValueComparator(this));
    }

    /**
     * Iterates over key-value pairs and calls CheckKeyValue.check(key, value),
     * interrupts the loop if the returned value is false.
     *
     * @param proc Interface that implements action for each key-value pair.
     */
    public final void each(final Each proc) {
        checkKeyValue(new CheckKeyValue() {
            @Override
            public boolean check(String key, String value) {
                proc.apply(key, value);
                return true;
            }
        });
    }

    /**
     * Iterates over key-value pairs and calls CheckKeyValue.check(key, value),
     * interrupts the loop if the returned value is false.
     *
     * @param proc Interface that implements action for each key-value pair.
     */
    public final boolean checkKeyValue(CheckKeyValue proc) {
        try {
            return checkKeyValueImpl(proc);
        } catch (Exception e) {
            GWT.getUncaughtExceptionHandler().onUncaughtException(e);
            return false;
        }
    }

    private final native boolean checkKeyValueImpl(CheckKeyValue proc) /*-{
                                                                       for (var key in this) {
                                                                       if (this.hasOwnProperty(key) && (key.charAt(0) === ':')) {
                                                                       if (!proc.
                                                                       @org.waveprotocol.wave.client.gadget.StateMap$CheckKeyValue::check(Ljava/lang/String;Ljava/lang/String;)
                                                                       (key.substring(1), this[key])) {
                                                                       return false;
                                                                       }
                                                                       }
                                                                       }
                                                                       return true;
                                                                       }-*/;

    /**
     * Clears the StateMap object.
     */
    public final void clear() {
        each(new Each() {
            @Override
            public void apply(String key, String value) {
                remove(key);
            }
        });
    }

    /**
     * Copy the contents of an existing StateMap into this one.
     *
     * @param map Map to copy key-value pairs from.
     */
    public final void copyFrom(StateMap map) {
        clear();
        map.each(new Each() {
            @Override
            public void apply(String key, String value) {
                put(key, value);
            }
        });
    }

    /**
     * Copy key-value pairs from jsonObject; accept only string, number, or
     * null values; skip all pairs with other values. This step makes sure that
     * the StateMap only contains sanitized keys prefixed with ':'. Note that
     * hasOwnProperty() cannot be used on jsonObject. However, typeof and null
     * comparison are safe.
     *
     * @param source JavaScriptObject to copy from.
     * @param target JavaScriptObject to copy into.
     */
    public final static native void copyJson(JavaScriptObject source, JavaScriptObject target) /*-{
                                                                                               for (var key in target) {
                                                                                               if (target.hasOwnProperty(key)) {
                                                                                               delete target[key];
                                                                                               }
                                                                                               }
                                                                                                   
                                                                                               for (var key in source) {
                                                                                               if (source[key] === null) {
                                                                                               target[':' + key] = null;
                                                                                               } else if (typeof source[key] === 'string') {
                                                                                               target[':' + key] = source[key];
                                                                                               } else if (typeof source[key] === 'number') {
                                                                                               target[':' + key] = String(source[key]);
                                                                                               }
                                                                                               }
                                                                                               }-*/;

    /**
     * Populates this StateMap with key-value pairs from the json String.
     *
     * @param json The JSON string to process.
     */
    public final native void fromJson(String json) /*-{
                                                   var jsonObject = {};
                                                   // Use safe eval defined in gadgets.json. This makes sure eval does not
                                                   // execute any dangerous operations.
                                                   if (/^[\],:{}\s]*$/.test(json.replace(/\\["\\\/b-u]/g, '@').
                                                   replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
                                                   replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
                                                   jsonObject = eval('(' + json + ')');
                                                   }
                                                       
                                                   @org.waveprotocol.wave.client.gadget.StateMap::copyJson(Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)
                                                   (jsonObject, this);
                                                   }-*/;

    /**
     * Populates this StateMap with key-value pairs from a JavaScriptObject.
     *
     * @param jsonObject The JSON object to process.
     */
    public final void fromJsonObject(JavaScriptObject jsonObject) {
        copyJson(jsonObject, this);
    }

    /**
     * Converts this StateMap to a JSON String. Eliminates the ":" key prefixes.
     *
     * @return a JSON string representation of this map.
     */
    public final String toJson() {
        final StringBuilder builder = new StringBuilder();
        builder.append("{");
        each(new Each() {
            private boolean firstKey = true;

            @Override
            public void apply(String key, String value) {
                if (firstKey) {
                    firstKey = false;
                } else {
                    builder.append(",");
                }
                builder.append(escapeValue(key) + ":");
                if (value == null) {
                    builder.append("null");
                } else {
                    builder.append(escapeValue(value));
                }
            }
        });
        builder.append("}");
        return builder.toString();
    }

    private final native void putKeyValue(JavaScriptObject jso, String key, String value) /*-{
                                                                                          jso[key] = value;
                                                                                          }-*/;

    /**
     * Convert this into a serializable JavaScriptObject. Eliminates the ":"
     * key prefixes.
     *
     * @return The JavaScriptObject of the valid keys.
     */
    public final JavaScriptObject asJavaScriptObject() {
        final JavaScriptObject jso = JavaScriptObject.createObject();
        each(new Each() {
            @Override
            public void apply(String key, String value) {
                putKeyValue(jso, key, value);
            }
        });
        return jso;
    }

    /**
     * Helper JS method: removes the value with the given key from the map, the
     * caller makes sure the key exists.
     *
     * @param key the key to remove.
     */
    private final native void nativeRemove(String key) /*-{
                                                       delete this[':' + key];
                                                       }-*/;

    /**
     * JavaScript string escape function.
     *
     * @param string input string.
     * @return escaped string.
     */
    private final native String escapeValue(String string) /*-{
                                                           // TODO(user): Replace this with a call to
                                                           // com.google.gwt.core.client.JsonUtils.escapeValue when it's checked in.
                                                           var escapable = new RegExp('[\\\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4' +
                                                           '\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]',
                                                           'g');
                                                           var meta = {
                                                           '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f',
                                                           '\r': '\\r', '"' : '\\"', '\\': '\\\\'};
                                                               
                                                           escapable.lastIndex = 0;
                                                           return escapable.test(string) ?
                                                           '"' + string.replace(escapable, function (a) {
                                                           var c = meta[a];
                                                           return typeof c === 'string' ? c :
                                                           '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
                                                           }) + '"' :
                                                           '"' + string + '"';
                                                           }-*/;
}