org.xwiki.gwt.user.client.BackForwardCache.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.gwt.user.client.BackForwardCache.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This 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 software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.gwt.user.client;

import java.util.HashMap;
import java.util.Map;

import org.xwiki.gwt.dom.client.Document;
import org.xwiki.gwt.dom.client.Element;
import org.xwiki.gwt.dom.client.JavaScriptObject;

import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.json.client.JSONString;

/**
 * Allows us to preserve the state of the editor against the browser's <em>Back</em> button, the <em>soft</em> page
 * refresh and the browser crash by using the browser's cache mechanism. Modern browsers cache HTML form fields like
 * {@code input} and {@code textarea} as long as they are generated on the server and not on the client using
 * JavaScript. This class uses the value of such an element to save string properties.
 * 
 * @version $Id: fb803524fa4442f7ff594b5dedf1470d9307de92 $
 */
public class BackForwardCache implements Updatable {
    /**
     * The name of the boolean property that specifies if the {@link #PROPERTY_VALUE} needs to be updated with the
     * latest {@link #PROPERTY_CACHE} value.
     */
    private static final String PROPERTY_DIRTY = "__dirty";

    /**
     * The name of the property which holds the map of key-value pairs that is used for quick access to the cache.
     */
    private static final String PROPERTY_CACHE = "__cache";

    /**
     * The name of the property that is cached by the browser.
     */
    private static final String PROPERTY_VALUE = "value";

    /**
     * An HTML element whose value can be cached by the browser.
     */
    private final Element cacheable;

    /**
     * Schedules updates for the value of the {@link #cacheable} element.
     */
    private final DeferredUpdater updater = new DeferredUpdater(this);

    /**
     * Creates a new cache based on the given HTML element whose value can be cached by the browser. Examples of such
     * elements are {@code input} and {@code textarea}.
     * <p>
     * The browser doesn't cache the HTML elements added dynamically through JavaScript but only the static ones. Moving
     * an element inside the DOM tree prevents the browser from caching its value even if that element was generated on
     * the server.
     * 
     * @param cacheable any HTML element whose value can be cached by the browser
     */
    public BackForwardCache(Element cacheable) {
        // Fall back on an orphan hidden input element if the cache is not enabled.
        this.cacheable = cacheable != null ? cacheable : (Element) Document.get().createHiddenInputElement().cast();
    }

    /**
     * Retrieves the value of a cached property specified by the given key.
     * 
     * @param key a key in the cache
     * @return the value associated with the given key or {@code null} if there's no such key in the cache
     */
    public String get(String key) {
        return getMap().get(key);
    }

    /**
     * Retrieves the value of a cached property specified by the given key, falling back on a default value if the key
     * is not found in the cache.
     * 
     * @param key a key in the cache
     * @param defaultValue the value returned if the specified key is not found in the cache
     * @return the value associated with the given key or {@code defaultValue} if there's no such key in the cache
     */
    public String get(String key, String defaultValue) {
        String cachedValue = get(key);
        return cachedValue != null ? cachedValue : defaultValue;
    }

    /**
     * Adds a new key in the cache or updates the value of an existing key.
     * 
     * @param key a key in the cache
     * @param value the new value for the given key
     * @return the previous value associated with the given key
     */
    public String put(String key, String value) {
        String previousValue = getMap().put(key, value);
        if (value != previousValue && (value == null || !value.equals(previousValue))) {
            cacheable.setPropertyBoolean(PROPERTY_DIRTY, true);
            updater.deferUpdate();
        }
        return previousValue;
    }

    /**
     * @return the {@link Map} used to store the cache for quick access
     */
    @SuppressWarnings("unchecked")
    protected Map<String, String> getMap() {
        Map<String, String> map = (Map<String, String>) ((JavaScriptObject) cacheable.cast()).get(PROPERTY_CACHE);
        if (map == null) {
            map = deserialize(cacheable.getPropertyString(PROPERTY_VALUE));
            ((JavaScriptObject) cacheable.cast()).set(PROPERTY_CACHE, map);
        }
        return map;
    }

    @Override
    public boolean canUpdate() {
        // Don't update if the cacheable element is orphan or not dirty.
        return cacheable.getParentNode() != null && cacheable.getPropertyBoolean(PROPERTY_DIRTY);
    }

    @Override
    public void update() {
        cacheable.setPropertyString(PROPERTY_VALUE, serialize(getMap()));
        cacheable.setPropertyBoolean(PROPERTY_DIRTY, false);
    }

    /**
     * Parses the given string and extracts a map of key-value pairs.
     * 
     * @param data the string to be deserialized
     * @return the map whose serialization is the given string
     * @see #serialize(Map)
     */
    protected Map<String, String> deserialize(String data) {
        Map<String, String> map = new HashMap<String, String>();
        if (!StringUtils.isEmpty(data)) {
            JavaScriptObject jsObject = JavaScriptObject.fromJson(data);
            JsArrayString keys = jsObject.getKeys();
            for (int i = 0; i < keys.length(); i++) {
                String key = keys.get(i);
                Object value = jsObject.get(key);
                if (value != null) {
                    map.put(key, String.valueOf(value));
                }
            }
        }
        return map;
    }

    /**
     * Converts the given map of key-value pairs to a string from which it can be recomputed.
     * 
     * @param map the map to be serialized
     * @return a string representation of the given map
     * @see #deserialize(String)
     */
    protected String serialize(Map<String, String> map) {
        StringBuffer output = new StringBuffer("{");
        String separator = "";
        for (Map.Entry<String, String> entry : map.entrySet()) {
            output.append(separator);
            output.append(new JSONString(entry.getKey()));
            output.append(':');
            output.append(new JSONString(entry.getValue()));
            separator = ",";
        }
        output.append("}");
        return output.toString();
    }
}