/*
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:
*
* JSCDataEventTypes.EMPTY_STATE event
* JSCDataEventTypes.VALUE_CHANGED event
*
* 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.