/*
* MapManager.java
* Eisenkraut
*
* Copyright (c) 2004-2016 Hanns Holger Rutz. All rights reserved.
*
* This software is published under the GNU General Public License v3+
*
*
* For further information, please contact Hanns Holger Rutz at
* contact@sciss.de
*
*
* Changelog:
* 13-May-05 created from de.sciss.meloncillo.util.MapManager
*/
package de.sciss.eisenkraut.util;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import de.sciss.app.BasicEvent;
import de.sciss.app.EventManager;
import de.sciss.eisenkraut.io.XMLRepresentation;
import de.sciss.io.IOUtil;
public class MapManager
implements EventManager.Processor, XMLRepresentation {
/**
* Code for <code>Event.getOwnerModType()</code>:
* the object has been changed in a way that affects its graphical
* representation.
*
* @see MapManager.Event#getOwnerModType()
*/
public static final int OWNER_VISUAL = 0x10000;
// ------- private -------
private final Map<String, Object> backingMap;
protected final Object owner;
private final Map<String, Context> contextMap;
private final EventManager elm = new EventManager(this);
/**
*/
public MapManager(Object owner, Map<String, Object> backingMap) {
this.owner = owner;
this.backingMap = backingMap;
contextMap = new HashMap<String, Context>();
}
public MapManager(Object owner) {
this(owner, new HashMap<String, Object>());
}
public void addListener(MapManager.Listener listener) {
elm.addListener(listener);
}
public void removeListener(MapManager.Listener listener) {
elm.removeListener(listener);
}
public void cloneMap(MapManager orig) {
for (String key : orig.backingMap.keySet()) {
this.backingMap.put(key, orig.backingMap.get(key)); // all supported types are immutable
}
for (String key : orig.contextMap.keySet()) {
this.contextMap.put(key, new Context(orig.contextMap.get(key)));
}
}
// the returned set is allowed to be modified
public Set<String> keySet(int inclusionFlags, int exclusionFlags) {
Set<String> allKeys = backingMap.keySet();
HashSet<String> fltKeys = new HashSet<String>(allKeys);
if ((inclusionFlags == Context.ALL_INCLUSIVE) &&
(exclusionFlags == Context.NONE_EXCLUSIVE)) return fltKeys;
Iterator<String> iter = fltKeys.iterator();
Context c;
while (iter.hasNext()) {
c = contextMap.get(iter.next());
if ((c == null) || ((c.flags & inclusionFlags) == 0) || ((c.flags & exclusionFlags) != 0)) {
iter.remove();
}
}
return fltKeys;
}
public boolean containsKey(String key) {
return backingMap.containsKey(key);
}
public void putContext(Object source, String key, MapManager.Context context) {
contextMap.put(key, context);
Object o = getValue( key );
if( (context.defaultValue != null) ) {
if( o == null ) {
putValue( source, key, context.defaultValue );
} else {
if( !o.getClass().equals( context.defaultValue.getClass() )) {
// Long and Integer should be exchangeable due to NumberField ( XXX )
if( !(o instanceof Number) || !(context.defaultValue instanceof Number) ) {
System.err.println( "Warning! Map context and value classes inconsistent - "+
"for key '"+key+"' context wants '"+context.defaultValue.getClass()+
"' but finds '"+o.getClass()+"'. Replacing by default value!" );
putValue( source, key, context.defaultValue );
}
}
}
}
}
public MapManager.Context getContext(String key) {
return contextMap.get(key);
}
public Object putValue(Object source, String key, Object value) {
Object oldVal = backingMap.put(key, value);
if( (source != null) &&
(((oldVal == null) && (value != null)) ||
((oldVal != null) && (value == null)) ||
((oldVal != null) && (value != null) && !(oldVal.equals( value ))))) {
Set<String> keySet = new HashSet<String>( 1 );
keySet.add( key );
elm.dispatchEvent( new MapManager.Event( this, source, keySet ));
checkOwnerModification( source, keySet );
}
return( oldVal );
}
public Object removeValue(Object source, String key) {
final Object result = backingMap.remove(key);
if ((result != null) && (source != null)) {
final Set<String> keySet = new HashSet<String>(1);
keySet.add(key);
elm.dispatchEvent(new MapManager.Event(this, source, keySet));
checkOwnerModification(source, keySet);
}
return result;
}
public void putAllValues(Object source, Map<String, Object> map) {
backingMap.putAll(map);
if ((source != null)) {
Set<String> keySet = new HashSet<String>(map.keySet());
elm.dispatchEvent(new MapManager.Event(this, source, keySet));
checkOwnerModification(source, keySet);
}
}
public Object getValue(String key) {
return backingMap.get(key);
}
public void clearValues(Object source) {
Set<String> keySet = new HashSet<String>(backingMap.keySet());
boolean dispatch = !keySet.isEmpty() && (source != null);
backingMap.clear();
if (dispatch) {
elm.dispatchEvent(new MapManager.Event(this, source, keySet));
checkOwnerModification(source, keySet);
}
}
/**
* Copies all contexts from this map to
* another map, which satisfy the given flags.
*
* @param inclusionFlags only copy contexts whose flags
* contain all of these flags
* @param exclusionFlags only copy contexts whose flags
* do not contain any of these flags
* @param targetMap to map to which the contexts should be copied
*/
public void copyContexts(Object source, int inclusionFlags, int exclusionFlags, MapManager targetMap) {
Iterator<String> iter = this.keySet( inclusionFlags, exclusionFlags ).iterator();
String key;
Context c;
while (iter.hasNext()) {
key = iter.next();
c = this.contextMap.get(key);
targetMap.putContext(source, key, c);
}
}
public void dispatchOwnerModification(Object source, int ownerModType, Object ownerModParam) {
elm.dispatchEvent(new MapManager.Event(this, source, ownerModType, ownerModParam));
}
private void checkOwnerModification(Object source, Set<String> keySet) {
Iterator<String> iter = keySet.iterator();
String key;
Context c;
while (iter.hasNext()) {
key = iter.next();
c = getContext(key);
if ((c != null) && ((c.flags & Context.FLAG_VISUAL) != 0)) {
dispatchOwnerModification(source, OWNER_VISUAL, null);
return;
}
}
}
public void debugDump() {
System.err.println("..... map : hash = " + hashCode() + ". owner = " + owner.getClass().getName() + " . contains : ");
Iterator<String> iter = backingMap.keySet().iterator();
String key, val;
while (iter.hasNext()) {
key = iter.next();
val = backingMap.get(key).toString();
System.err.println(" key = " + key + " ; val = " + val);
}
}
// --------------------- EventManager.Processor interface ---------------------
/**
* This is called by the EventManager
* if new events are to be processed. This
* will invoke the listener's <code>propertyChanged</code> method.
*/
public void processEvent( BasicEvent e )
{
MapManager.Listener listener;
int i;
MapManager.Event mme = (MapManager.Event) e;
for( i = 0; i < elm.countListeners(); i++ ) {
listener = (MapManager.Listener) elm.getListener( i );
switch( e.getID() ) {
case MapManager.Event.MAP_CHANGED:
listener.mapChanged( mme );
break;
case MapManager.Event.OWNER_MODIFIED:
listener.mapOwnerModified( mme );
break;
default:
assert false : e.getID();
break;
}
} // for( i = 0; i < this.countListeners(); i++ )
}
// --------------------- XMLRepresentation interface ---------------------
private static final String XML_ELEM_ENTRY = "entry";
// private static final String XML_ELEM_FLAG = "flag";
private static final String XML_ATTR_KEY = "key";
private static final String XML_ATTR_VALUE = "value";
private static final String XML_ATTR_TYPE = "type";
// private static final String XML_ATTR_ID = "id";
// private static final String XML_VALUE_OBSERVER = "observer";
// private static final String XML_VALUE_EDITOR = "editor";
// private static final String XML_VALUE_LIST = "list";
// private static final String XML_VALUE_CONTEXT = "context";
private static final String[] XML_VALUE_TYPES = {
"int", "long", "float", "double", "boolean", "string", "file"
};
public void toXML(Document domDoc, Element node, Map<Object, Object> options)
throws IOException {
Iterator<String> keys = backingMap.keySet().iterator();
try {
while( keys.hasNext() ) {
String key = keys.next();
Context c = contextMap.get( key );
if( (c == null) || ((c.flags & Context.FLAG_NO_STORAGE) != 0) ) continue;
Object value = backingMap.get( key );
Element child = domDoc.createElement( XML_ELEM_ENTRY );
child.setAttribute( XML_ATTR_KEY, key);
child.setAttribute( XML_ATTR_VALUE, value.toString() );
child.setAttribute( XML_ATTR_TYPE, XML_VALUE_TYPES[ c.type ]);
// if( (c.flags & Context.FLAG_CONTEXT_STORAGE) != 0 ) {
// if( (c.flags & Context.FLAG_OBSERVER_DISPLAY) != 0 ) {
// createFlag( domDoc, child, XML_VALUE_OBSERVER );
// }
// if( (c.flags & Context.FLAG_EDITOR_DISPLAY) != 0 ) {
// createFlag( domDoc, child, XML_VALUE_EDITOR );
// }
// if( (c.flags & Context.FLAG_LIST_DISPLAY) != 0 ) {
// createFlag( domDoc, child, XML_VALUE_LIST );
// }
// if( (c.flags & Context.FLAG_CONTEXT_STORAGE) != 0 ) {
// createFlag( domDoc, child, XML_VALUE_CONTEXT );
// }
// }
node.appendChild( child );
}
}
catch( DOMException e1 ) {
throw IOUtil.map( e1 ); // rethrow exception
}
}
// private void createFlag( Document domDoc, Element node, String id )
// {
// Element child = domDoc.createElement( XML_ELEM_FLAG );
// child.setAttribute( XML_ATTR_ID, id );
// node.appendChild( child );
// }
public void fromXML(Document domDoc, Element node, Map<Object, Object> options)
throws IOException {
final NodeList nl = node.getChildNodes();
final Set<String> keySet = new HashSet<String>( backingMap.keySet() );
int type;
Element xmlChild;
String key, value, typeStr;
Context c;
Object o;
backingMap.clear();
for (int i = 0; i < nl.getLength(); i++) {
if (!(nl.item(i) instanceof Element)) continue;
xmlChild = (Element) nl.item(i);
if (xmlChild.getTagName().equals(XML_ELEM_ENTRY)) {
key = xmlChild.getAttribute(XML_ATTR_KEY);
value = xmlChild.getAttribute(XML_ATTR_VALUE);
typeStr = xmlChild.getAttribute(XML_ATTR_TYPE);
if (typeStr == null) { // #IMPLIED = String
type = Context.TYPE_STRING;
} else {
for (type = 0; type < XML_VALUE_TYPES.length; type++) {
if (XML_VALUE_TYPES[type].equals(typeStr)) break;
}
if (type == XML_VALUE_TYPES.length) {
System.err.println("skipping map entry of unknown type : key = " + key + "; type = " + typeStr);
continue;
}
}
c = contextMap.get(key);
if (c != null && c.type != type) {
System.err.println("skipping map entry of deviant type : key = " + key + "; type = " + typeStr +
"; expected = " + XML_VALUE_TYPES[c.type]);
continue;
} else if (c == null) {
c = new Context(0, type, null, null, null, null); // LLL XXX should read flags, constraints and label
contextMap.put(key, c);
}
try {
switch (type) {
case Context.TYPE_INTEGER:
o = new Integer(value);
break;
case Context.TYPE_LONG:
o = new Long(value);
break;
case Context.TYPE_FLOAT:
o = new Float(value);
break;
case Context.TYPE_DOUBLE:
o = new Double(value);
break;
case Context.TYPE_BOOLEAN:
o = Boolean.valueOf(value);
break;
case Context.TYPE_STRING:
o = value;
break;
case Context.TYPE_FILE:
o = new File(value);
break;
default:
assert false : type;
o = null;
}
backingMap.put(key, o);
keySet.add(key);
} catch (NumberFormatException e1) {
System.err.println("couldn't parse map entry : key " + key + "; type = " + typeStr + "; value = " + value);
}
}
}
elm.dispatchEvent( new MapManager.Event( this, this, keySet ));
}
// --------------------- inner classes ---------------------
public static class Context
{
public static final int ALL_INCLUSIVE = -1;
public static final int NONE_EXCLUSIVE = 0;
public static final int FLAG_OBSERVER_DISPLAY = 0x01; // item should be visible in an observer
public static final int FLAG_EDITOR_DISPLAY = 0x02; // item should be visible in an editor
public static final int FLAG_LIST_DISPLAY = 0x04; // item should be visible in a list view
public static final int FLAG_NO_STORAGE = 0x08; // item should not be stored in the session file
// public static final int FLAG_CONTEXT_STORAGE = 0x10; // context flags and constratins should be saved
public static final int FLAG_VISUAL = 0x20; // item affects visual representation
public static final int FLAG_DYNAMIC = 0x40; // item should be effective only if a plug-in is active
public static final int TYPE_INTEGER = 0x00;
public static final int TYPE_LONG = 0x01;
public static final int TYPE_FLOAT = 0x02;
public static final int TYPE_DOUBLE = 0x03;
public static final int TYPE_BOOLEAN = 0x04;
public static final int TYPE_STRING = 0x05;
public static final int TYPE_FILE = 0x06;
public final int flags;
public final int type;
public final Object typeConstraints; // e.g. NumberSpace
// public int gui;
public final String label;
public final String dynamic;
public final Object defaultValue;
/**
* typeConstraints must be an immutable object!
*/
public Context( int flags, int type, Object typeConstraints, String label, String dynamic,
Object defaultValue )
{
this.flags = flags;
this.type = type;
this.typeConstraints = typeConstraints;
this.label = label;
this.dynamic = dynamic;
this.defaultValue = defaultValue;
}
protected Context( Context orig )
{
this.flags = orig.flags;
this.type = orig.type;
this.typeConstraints = orig.typeConstraints; // == null ? null : orig.typeConstraints.clone();
this.label = orig.label;
this.dynamic = orig.dynamic;
this.defaultValue = orig.defaultValue;
}
}
/**
* A simple interface describing
* the method that gets called from
* the event dispatching thread when
* new objects have been queued.
*/
public interface Listener {
public void mapChanged(MapManager.Event e);
public void mapOwnerModified(MapManager.Event e);
}
@SuppressWarnings("serial")
public static class Event
extends BasicEvent {
public static final int MAP_CHANGED = 0;
public static final int OWNER_MODIFIED = 1;
private final MapManager map;
private final Set<String> propertyNames;
private final Object param;
private final int ownerModType;
public Event(MapManager map, Object source, Set<String> propertyNames) {
super(source, MAP_CHANGED, System.currentTimeMillis());
this.map = map;
this.propertyNames = propertyNames;
this.param = null;
this.ownerModType = -1;
}
public Event(MapManager map, Object source, int ownerModType, Object ownerModParam) {
super(source, OWNER_MODIFIED, System.currentTimeMillis());
this.map = map;
this.param = ownerModParam;
this.propertyNames = null;
this.ownerModType = ownerModType;
}
public MapManager getManager() {
return map;
}
public Set<String> getPropertyNames() {
return propertyNames;
}
public Object getOwner() {
return map.owner;
}
public int getOwnerModType() {
return ownerModType;
}
public Object getOwnerModParam() {
return param;
}
/**
* Returns false always at the moment
*/
public boolean incorporate(BasicEvent oldEvent) {
return false;
}
}
}