/*
 This file is part of the software framework JStatCom, 
 a library for building applications for data based analysis.

 Copyright (C) 2007  Markus Kraetzig

 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.jstatcom.model;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javolution37.javolution.xml.XmlElement;
import javolution37.javolution.xml.XmlFormat;

import com.jstatcom.util.SerializationHelper;
import com.jstatcom.util.UMatrix;

/**
 * An object wrapper that conforms to the JSCData interface. This
 * class acts like an ordered map where the keys are string values and the
 * values are one of
 * String, String[], Integer, int[], int[][], Double, double[], double[][], Boolean, boolean[].
 * On read/write access all array values are copied to prevent external
 * references to them. The JSCMap was inspired by the list
 * datatype in the R statistics language. It can be used as an input and return
 * parameter in calls the the RStatEngine.
 * 
 * 

* A JSCMap is empty, if there are no keys stored. *

* Usage Note:
*

* Whenever the value of this data object changes (by adding, removing * elements), registered JSCDataListeners are notified via a * JSCDataEvent. In case of value changes, the event objects * contain an array with the keys before and after the change, but not the * values itself. *

* This class is thread-safe. All access to mutable data is synchronized. All * events are fired outside the synchronized blocks. * * @author Markus Kraetzig */ public final class JSCMap extends AbstractJSCData { private final Map valueMap = new LinkedHashMap(); private final String name; private JSCDataEvent clearEvent = null; private JSCDataEvent notEmptyEvent = null; /** * XML format field for (de)serialization. */ public static final XmlFormat JSCMap_XML = new XmlFormat( JSCMap.class) { public void format(JSCMap data, XmlElement xml) { synchronized (data) { xml.setAttribute("name", data.name); for (String key : data.valueMap.keySet()) { xml.add(key, "key"); xml.add(SerializationHelper.encode(data.valueMap.get(key)), "value"); } } } public JSCMap parse(XmlElement xml) { String nam = xml.getAttribute("name") + ""; JSCMap rtn = new JSCMap(nam); // assemble map while (xml.hasNext()) { String key = xml.getNext(); String encoded = xml.getNext(); rtn.valueMap.put(key, SerializationHelper.decode(encoded)); } return rtn; } }; /** * Creates an empty JSCMap. * * @param name * the identifier of that object * @throws IllegalArgumentException * if name is not valid or * if (name == null) * * @see com.jstatcom.model.JSCConstants#isValidName */ public JSCMap(String name) { if (name == null) throw new IllegalArgumentException("Argument was null."); name = JSCConstants.checkNameThrowEx(name); this.name = name; } /** * Creates an empty JSCMap with a default name. * */ public JSCMap() { this(JSCTypes.MAP + "_DEFAULT"); } /** * Clears this data object. Fires: *

* if it was not empty before. After calling this method * isEmpty returns true. */ public void clear() { String[] oldValue = null; boolean valChangedListeners = false; if (eventSupport != null) valChangedListeners = eventSupport.getListenersForType( JSCDataEventTypes.VALUE_CHANGED).size() > 0; synchronized (this) { if (valueMap.size() == 0) // Nothing changes. return; if (valChangedListeners) oldValue = getCurrentKeys(); valueMap.clear(); } // Dispatch event outside synchronized block. if (clearEvent == null) clearEvent = JSCDataEvent.valueOfEmptyState(this, true); if (eventSupport == null) return; getEventSupport().dispatchEvent(clearEvent); if (valChangedListeners) { getEventSupport().dispatchEvent( JSCDataEvent.valueOfChanged(this, oldValue, new String[0])); } } private String[] getCurrentKeys() { List keys = new ArrayList(); keys.addAll(valueMap.keySet()); return keys.toArray(new String[0]); } /** * @see com.jstatcom.model.JSCData */ public synchronized JSCMap copy() { JSCMap copy = new JSCMap(name); deepCopyMap(valueMap, copy.valueMap); return copy; } private void deepCopyMap(Map fromMap, Map toMap) { for (String s : fromMap.keySet()) { Object val = fromMap.get(s); toMap.put(s, deepCopyObject(val)); } } private Object deepCopyObject(Object val) { if (val instanceof String || val instanceof Integer || val instanceof Double || val instanceof Boolean) { return val; } else if (val instanceof String[]) { String[] valsArr = (String[]) val; String[] newVal = new String[valsArr.length]; System.arraycopy(valsArr, 0, newVal, 0, valsArr.length); return newVal; } else if (val instanceof int[]) { int[] valsArr = (int[]) val; int[] newVal = new int[valsArr.length]; System.arraycopy(valsArr, 0, newVal, 0, valsArr.length); return newVal; } else if (val instanceof double[]) { double[] valsArr = (double[]) val; double[] newVal = new double[valsArr.length]; System.arraycopy(valsArr, 0, newVal, 0, valsArr.length); return newVal; } else if (val instanceof boolean[]) { boolean[] valsArr = (boolean[]) val; boolean[] newVal = new boolean[valsArr.length]; System.arraycopy(valsArr, 0, newVal, 0, valsArr.length); return newVal; } else if (val instanceof int[][]) { int[][] valsArr = (int[][]) val; int[][] newVal = UMatrix.cloneIntArray(valsArr); return newVal; } else if (val instanceof double[][]) { double[][] valsArr = (double[][]) val; double[][] newVal = UMatrix.cloneDoubleArray(valsArr); return newVal; } else if (val == null) { return null; } else throw new IllegalArgumentException("Value type cannot be handled: " + val); } /** * Gets whether this is empty. If this method returns * true, then value returns an empty * Map. * * @return true if empty */ public synchronized boolean isEmpty() { return valueMap.size() == 0; } /** * Checks whether both objects contain the same keys only. Values are not * compared. * * @see com.jstatcom.model.JSCData */ public synchronized boolean isEqual(JSCData o) { if (o == this) return true; if (!(o instanceof JSCMap)) return false; JSCMap otherMap = (JSCMap) o; boolean ret = true; synchronized (this) { synchronized (o) { // compare keys if (!valueMap.keySet().equals(otherMap.valueMap.keySet())) return false; } } return ret; } /** * @see com.jstatcom.model.JSCData */ public String name() { return name; } /** * Copy-on-write adds a new key value pair, where val must be * of one . Fires a JSCDataEvent of type: *
    *
  • JSCDataEventTypes.VALUE_CHANGED if a new key is added * that was not there before or if an existing key ist overwritten, no * matter whether the new object is logically equivalent or not *
  • JSCDataEventTypes.EMPTY_STATE if this was empty * before *
* * @param key * must be non null, otherwise arbitrary * @param val * value, must be one of * String, String[], Integer, int[], int[][], Double, double[], double[][], Boolean, boolean[] * @throws IllegalArgumentException * if (key == null) or if val has * a class that is not supported */ public void put(String key, Object val) { if (key == null) throw new IllegalArgumentException("Key was null."); boolean wasEmpty = false; String[] oldValue; String[] newValue; synchronized (this) { wasEmpty = isEmpty(); oldValue = getCurrentKeys(); valueMap.put(key, deepCopyObject(val)); newValue = getCurrentKeys(); } // Dispatch event outside synchronized block. if (eventSupport == null) return; if (getEventSupport().getListenersForType( JSCDataEventTypes.VALUE_CHANGED).size() > 0) getEventSupport().dispatchEvent( JSCDataEvent.valueOfChanged(this, oldValue, newValue)); if (wasEmpty) { if (notEmptyEvent == null) notEmptyEvent = JSCDataEvent.valueOfEmptyState(this, false); getEventSupport().dispatchEvent(notEmptyEvent); } } /** * Removes the element belonging to key and returns the * underying value. The following events are fired: *
    *
  • JSCDataEventTypes.VALUE_CHANGED if key * exists *
  • JSCDataEventTypes.EMPTY_STATE if this is empty * afterwards but nonempty before calling this method *
* * @param key * must be non null, otherwise arbitrary * @return object belonging to key, may be null * if it was set to null or if key did * not exist * @throws IllegalArgumentException * if (key == null) */ public Object remove(String key) { if (key == null) throw new IllegalArgumentException("Key was null."); String[] oldValue; String[] newValue; Object rtnVal; synchronized (this) { if (isEmpty()) return null; if (!valueMap.containsKey(key)) // no change return null; oldValue = getCurrentKeys(); rtnVal = valueMap.remove(key); newValue = getCurrentKeys(); } // Dispatch event outside synchronized block. if (eventSupport == null) return rtnVal; if (getEventSupport().getListenersForType( JSCDataEventTypes.VALUE_CHANGED).size() > 0) getEventSupport().dispatchEvent( JSCDataEvent.valueOfChanged(this, oldValue, newValue)); if (newValue.length == 0) { // is empty now, way not before if (clearEvent == null) clearEvent = JSCDataEvent.valueOfEmptyState(this, true); getEventSupport().dispatchEvent(clearEvent); } return rtnVal; } /** * Gets the element belonging to key and returns the * underying value or null otherwise. * * @param key * must be non null, otherwise arbitrary * @return object belonging to key, may be null * if it was set to null or if key * does not exist * @throws IllegalArgumentException * if (key == null) */ public Object get(String key) { if (key == null) throw new IllegalArgumentException("Key was null."); synchronized (this) { return valueMap.get(key); } } /** * Gets a deep copy of the map holding all key value pairs. * * @return copied map with copied values */ public Map getAll() { Map rtnMap = new LinkedHashMap(); synchronized (this) { deepCopyMap(valueMap, rtnMap); } return rtnMap; } /** * Checks whether key is contained. * * @param key * to check for * @return true if contained, false otherwise * @throws IllegalArgumentException * if (key == null) */ public boolean containsKey(String key) { if (key == null) throw new IllegalArgumentException("Key was null."); synchronized (this) { return valueMap.containsKey(key); } } public synchronized int size() { return valueMap.size(); } /** * Gets the type of this JSCData object. * * @return JSCTypes.MAP */ public JSCTypes type() { return JSCTypes.MAP; } /** * Gets the underlying value, which is stored as a * Map<String, Object>. * * @return the value object or an empty Map if this is empty */ public synchronized Object value() { Map toMap = new LinkedHashMap(); deepCopyMap(valueMap, toMap); return toMap; } @Override public synchronized String display() { if (valueMap.size() == 0) return "{}"; StringBuilder b = new StringBuilder(); b.append("{"); for (String key : valueMap.keySet()) { Object val = valueMap.get(key); b.append(key + "="); if (val instanceof Integer || val instanceof Double || val instanceof Boolean) { b.append(val); } else if (val instanceof String) { b.append("\"" + val + "\""); } else if (val instanceof String[]) { String[] valsArr = (String[]) val; b.append("String[" + valsArr.length + "]"); } else if (val instanceof int[]) { int[] valsArr = (int[]) val; b.append("int[" + valsArr.length + "]"); } else if (val instanceof double[]) { double[] valsArr = (double[]) val; b.append("double[" + valsArr.length + "]"); } else if (val instanceof boolean[]) { boolean[] valsArr = (boolean[]) val; b.append("boolean[" + valsArr.length + "]"); } else if (val instanceof int[][]) { int[][] valsArr = (int[][]) val; int rows = valsArr.length; int cols = 0; if (rows > 0) cols = valsArr[0].length; b.append("int[" + rows + "][" + cols + "]"); } else if (val instanceof double[][]) { double[][] valsArr = (double[][]) val; int rows = valsArr.length; int cols = 0; if (rows > 0) cols = valsArr[0].length; b.append("double[" + rows + "][" + cols + "]"); } else if (val == null) { b.append("null"); } b.append(","); } b.delete(b.length() - 1, b.length()); b.append("}"); return b.toString(); } }

Ads help maintain this website.