All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.zkoss.bind.tracker.impl.TrackerImpl Maven / Gradle / Ivy

There is a newer version: 10.0.0-jakarta
Show newest version
/* TrackerImpl.java

	Purpose:
		
	Description:
		
	History:
		Aug 24, 2011 7:31:14 PM, Created by henrichen

Copyright (C) 2011 Potix Corporation. All Rights Reserved.
*/

package org.zkoss.bind.tracker.impl;

import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.WeakHashMap;

import org.zkoss.bind.impl.WeakIdentityMap;
import org.zkoss.bind.sys.Binding;
import org.zkoss.bind.sys.ChildrenBinding;
import org.zkoss.bind.sys.FormBinding;
import org.zkoss.bind.sys.LoadBinding;
import org.zkoss.bind.sys.PropertyBinding;
import org.zkoss.bind.sys.ReferenceBinding;
import org.zkoss.bind.sys.tracker.Tracker;
import org.zkoss.bind.sys.tracker.TrackerNode;
import org.zkoss.bind.xel.zel.BindELContext;
import org.zkoss.util.IdentityHashSet;
import org.zkoss.zk.ui.Component;

/**
 * Implementation of dependency tracking.
 * @author henrichen
 * @since 6.0.0
 */
public class TrackerImpl implements Tracker, Serializable {
	private static final long serialVersionUID = 1463169907348730644L;
	private LinkedHashMap> _compMap = new LinkedHashMap>(); //comp -> path -> head TrackerNode
	private Map> _nullMap = new HashMap>(); //property -> Set of head TrackerNode that eval to null
	private transient Map> _beanMap = new WeakIdentityMap>(); //bean -> Set of TrackerNode
	private transient EqualBeansMap _equalBeansMap = new EqualBeansMap(); //bean -> beans (use to manage equal beans)
	
	public void addTracking(Component comp, String[] series, Binding binding) {
		//Track only LoadBinding
		if (!(binding instanceof LoadBinding)) {
			return;
		}
		
		final TrackerNode node = getOrCreateTrackerNode(comp, series);
		
		//node is leaf of this series, add the binding to it
		node.addBinding(binding);
	}
	
	public void addDependsOn(Component srcComp, String[] srcSeries, Binding srcBinding, Component dependsOnComp, String[] dependsOnSeries) {
		//Track only LoadBinding
		if (!(srcBinding instanceof LoadBinding)) {
			return;
		}
		if (dependsOnComp == null) {
			dependsOnComp = srcComp; //share same component context for @DependsOn case
		}
		final TrackerNode dependsOnNode = getOrCreateTrackerNode(dependsOnComp, dependsOnSeries);
		//bug# 1: depends-on is not working in nested C->B->A when A changed
		final TrackerNode srcnode =  getOrCreateTrackerNode(srcComp, srcSeries);
		dependsOnNode.addAssociate(srcnode); 
	}
	
	private TrackerNode getOrCreateTrackerNode(Component comp, String[] series) {
		Map nodes = _compMap.get(comp);
		if (nodes == null) {
			nodes = new HashMap(4);
			_compMap.put(comp, nodes);
		}
		
		TrackerNode parentNode = null;
		for(String script : series) {
			TrackerNode node = null;
			if (parentNode == null) { //head node
				node = nodes.get(script);
				if (node == null) {
					node = newTrackerNode(script);
					nodes.put(script, node);
				}
			} else {
				node = parentNode.getDependent(script);
				if (node == null) {
					node = newTrackerNode(script);
				}
				parentNode.addDependent(script, node);
			}
			parentNode = node;
		}
		return parentNode;
	}
	
	//ZSS-1989, sub-class could override this method to provide better tracker-node impl.
	protected TrackerNode newTrackerNode(Object script){
		return new TrackerNodeImpl(script); 
	}
	
	public void removeTrackings(Component comp) {
		final Map nodesMap = _compMap.remove(comp);
		if (nodesMap != null) {
			final Set removed = new HashSet();
			final Collection nodes = nodesMap.values();
			for (TrackerNode node : nodes) {
				removed.add(node);
				removed.addAll(node.getDependents());
			}
			removeAllFromBeanMap(removed);
			removeNodes(_nullMap.values(), removed);
		}
	}

	private void getLoadBindingsPerProperty(Collection nodes, String prop, LinkedHashSet bindings, LinkedHashSet kidbases, Set visited) {
		if (".".equals(prop)) { //all base object
			for (TrackerNode node : nodes) {
				getLoadBindings0(node, bindings, kidbases, visited);
			}
		} else if ("*".equals(prop)) { //all binding properties of the base object
			for (TrackerNode node : nodes) {
				final Set kids = node.getDirectDependents();
				getNodesLoadBindings(kids, bindings, kidbases, visited);
			}
		} else {
			for (TrackerNode node : nodes) {
				final TrackerNode kid = node.getDependent(prop);
				if (kid != null) {
					getLoadBindings0(kid, bindings, kidbases, visited);
				}
			}
		}
	}
	
	public Set getLoadBindings(Object base, String prop) {
		final LinkedHashSet bindings = new LinkedHashSet();
		final Set visited = new HashSet();
		collectLoadBindings(base, prop, bindings, visited);
		return bindings;
	}
	
	private void collectLoadBindings(Object base, String prop, LinkedHashSet bindings, Set visited) {
		final LinkedHashSet kidbases = new LinkedHashSet(); //collect kid as base bean
		if (base != null) {
			if ("*".equals(base)) { //loadAll, when base == "*"
				final Collection> nodesMaps = _compMap.values();
				if (nodesMaps != null) {
					for(Map nodesMap : nodesMaps) {
						final Collection nodes = nodesMap.values();
						if (nodes != null) {
							getLoadBindingsPerProperty(nodes, prop, bindings, kidbases, visited);
						}
					}
				}
			} else {
				final Set nodes = getAllTrackerNodesByBean(base);
				if (nodes != null && !nodes.isEmpty()) {
					getLoadBindingsPerProperty(nodes, prop, bindings, kidbases, visited);
				}
			}
		} else { //base == null)
			if ("*".equals(prop)) {
				for (Set basenodes : _nullMap.values()) {
					getNodesLoadBindings(basenodes, bindings, kidbases, visited);
				}
			} else {
				final Set basenodes = _nullMap.get(prop);
				getNodesLoadBindings(basenodes, bindings, kidbases, visited);
			}
		}
		
		for (Object kidbase : kidbases) {
			collectLoadBindings(kidbase, "*", bindings, visited); //recursive, for kid base
		}
	}
	
	public void tieValue(Object comp, Object base, Object script, Object propName, Object value) {
		if (base == null) { //track from component
			//locate head TrackerNodes of this component
			final Map bindingNodes = _compMap.get(comp);
			if (bindingNodes != null) {
				final TrackerNode node = bindingNodes.get(script);
				//ZK-877: NPE in a save only binding
				//No corresponding LoadBinding with the head script in the specified component. 
				if (node != null) {
					if (value != null) {
						addBeanMap(node, value);
					} else {
						removeAllBeanMap(node); //dependent nodes shall be null, too. Remove them from _beanMap 
						addNullMap(node); //head TrackerNode evaluate to null
					}
				}
			}
		} else {
			final Set baseNodes = getAllTrackerNodesByBean(base);
			if (baseNodes != null) { //FormBinding will keep base nodes only (so no associated dependent nodes)
				final Set propNodes = new LinkedHashSet(); //normal nodes; i.e. a base + property node. e.g. vm.selectedPerson
				for (TrackerNode baseNode : baseNodes) {
					final TrackerNode node = baseNode.getDependent(script);
					if (node == null) { //FormBinding will keep base nodes only (so no associated dependent nodes)
						continue;
					}
					propNodes.add(node);
					if (BindELContext.isBracket((String)script)) {
						baseNode.tieProperty(propName, script);
					}
				}

				if (value != null) {
					for (TrackerNode node : propNodes) { //normal nodes
						addBeanMap(node, value);
					}
				} else { //value == null
					for (TrackerNode node : propNodes) { //normal nodes
						removeAllBeanMap(node); //dependent nodes shall be null, too. Remove them from _beanMap
					}
				}
			}
		}
	}
	
	//add node into the _beanMap
	private void addBeanMap(TrackerNode node, Object value) {
		//add node into _beanMap
		if (!value.equals(node.getBean())) {
			//try to remove from the _beanMap
			removeBeanMap(node);
			
			//add into _beanMap
			if (!BindELContext.isImmutable(value)) {
				LinkedHashSet nodes = _beanMap.get(value);
				if (nodes == null) {
					nodes = new LinkedHashSet();
					_beanMap.put(value, nodes);
					_equalBeansMap.put(value);
				}
				nodes.add(node);
				//only when value is not a primitive that we shall store it
				node.setBean(value);
			}
		}
		
		//maybe a head node, try remove it from the nullMap
		removeNullMap(node);
	}
	
	//add head node into the _nullMap
	private void addNullMap(TrackerNode node) {
		//add node into _nullMap
		final Object propName = node.getFieldScript();
		LinkedHashSet nodes = _nullMap.get(propName);
		if (nodes == null) {
			nodes = new LinkedHashSet();
			_nullMap.put(propName, nodes);
		}
		nodes.add(node);
		
		//remove node from the _beanMap
		removeBeanMap(node);
	}
	
	//remove head node from the _nullMap
	private void removeNullMap(TrackerNode node) {
		final Object propName = node.getFieldScript();
		final Set nodes = _nullMap.get(propName);
		if (nodes != null) {
			nodes.remove(node);
			if (nodes.isEmpty()) {
				_nullMap.remove(propName);
			}
		}
	}
	
	//remove this node and all its dependent nodes from _beanMap
	private void removeAllBeanMap(TrackerNode node) {
		removeBeanMap(node);
		//all dependent node shall be removed, too.
		final Set kidnodes = node.getDependents();
		for(TrackerNode kid : kidnodes) {
			removeBeanMap(kid);
		}
	}
	
	//remove node from the _beanMap
	private void removeBeanMap(TrackerNode node) {
		final Object value = node.getBean();
		if (value != null) {
			node.setBean(null);
			final Set nodes = _beanMap.get(value);
			if (nodes != null) {
				nodes.remove(node); //remove this node from the _beanMap
				if (nodes.isEmpty()) {
					_equalBeansMap.remove(value); //sync the equalBeanMap 
					_beanMap.remove(value);
				}
			}
		}
	}
	
	private void getNodesLoadBindings(Set basenodes, LinkedHashSet bindings, LinkedHashSet kidbases, Set visited) {
		if (basenodes != null) {
			for (TrackerNode node : basenodes) {
				if (node != null) {
					getLoadBindings0(node, bindings, kidbases, visited);
				}
			}
		}
	}
	
	private void getLoadBindings0(TrackerNode node, LinkedHashSet bindings, Set kidbases, Set visited) {
		if (visited.contains(node)) { //already visited
			return;
		}
		visited.add(node);
		
		bindings.addAll(node.getLoadBindings());
		final Set refBindings = node.getReferenceBindings();
		bindings.addAll(refBindings);
		for (ReferenceBinding refBinding : refBindings) {
			refBinding.invalidateCache();
			//ZK-950: The expression reference doesn't update while change the instant of the reference
			//Have to load bindings that refer this ReferenceBinding as well
			collectLoadBindings(refBinding, ".", bindings, visited); //recursive
		}
		
		//bug #1: depends-on is not working in nested C->B->A when A changed
		for(TrackerNode associate: node.getAssociates()) {
			getLoadBindings0(associate, bindings, kidbases, visited); //recursive
		}
		
		final Object kidbase = node.getBean();
		if (kidbases != null && kidbase != null) {
			kidbases.add(kidbase);
		} else {
			//check dependents
			final Set nodes = node.getDirectDependents(); 
			for (TrackerNode kid : nodes) {
				getLoadBindings0(kid, bindings, null, visited); //recursive
			}
		}
	}
	
	//given base and postfix, found the associated TrackerNode. 
	@SuppressWarnings("unused")
	private Set getNodes(Object base, String postfix) {
		Set nodes = getAllTrackerNodesByBean(base);
		String[] props = postfix.split("\\.");
		for (String prop : props) {
			nodes = getDependents(nodes, prop);
		}
		return nodes;
	}
	
	//get dependents of a group of TrackerNodes.
	private Set getDependents(Set parentnodes, String prop) {
		final Set kidnodes = new HashSet();
		for (TrackerNode node : parentnodes) {
			final TrackerNode kid = node.getDependent(prop);
			if (kid != null) {
				kidnodes.add(kid);
			}
		}
		return kidnodes;
	}

	//remove all specified nodes from the _beanMap 
	private void removeAllFromBeanMap(Collection removed) {
		final Collection>> nodesets = _beanMap.entrySet(); 
		for (final Iterator>> it = nodesets.iterator(); it.hasNext();) {
			final Entry> nodeset = it.next();
			final Object bean = nodeset.getKey();
			nodeset.getValue().removeAll(removed);
			if (nodeset.getValue().isEmpty()) {
				it.remove();
				_equalBeansMap.remove(bean);
			}
		}
	}
	
	private void removeNodes(Collection> nodesets, Collection removed) {
		for (final Iterator> it = nodesets.iterator(); it.hasNext();) {
			final Set nodeset = it.next();
			nodeset.removeAll(removed);
			if (nodeset.isEmpty()) {
				it.remove();
			}
		}
	}
	
	private Set getAllTrackerNodesByBean(Object bean) {
		final Set results = new LinkedHashSet();
		getAllTrackerNodesByBean0(bean, results);
		return results;
	}
	
	private void getAllTrackerNodesByBean0(Object bean, Set results) {
		final Set beans = _equalBeansMap.getEqualBeans(bean); //return a set of equal beans
		final Set nodes = new LinkedHashSet();
		for (Object obj : beans) {
			Set beanNodes = _beanMap.get(obj);
			if(beanNodes!=null){//zk-1185, _beanMap could contains no such entry, and returned null.
				nodes.addAll(beanNodes);
			}
		}
		results.addAll(nodes);
		getAllTrackerNodesByBeanNodes(nodes, results);
	}
	
	//ZK-950: The expression reference doesn't update while change the instant of the reference
	//Check if the passed in bean nodes contains ReferenceBindings; have to collect those
	//nodes that refers those ReferenceBindings as well
	private void getAllTrackerNodesByBeanNodes(Set nodes, Set results) {
		for (TrackerNode node : nodes) {
			final Set refBindings = node.getReferenceBindings();
			for (ReferenceBinding refBinding : refBindings) {
				getAllTrackerNodesByBean0(refBinding, results); //recursive
			}
		}
	}
	
	//Returns equal beans with the given bean in an IdentityHashSet() 
	public Set getEqualBeans(Object bean) {
		return _equalBeansMap.getEqualBeans(bean); //return a set of equal beans
	}
	
	private void readObject(java.io.ObjectInputStream s)
	throws java.io.IOException, ClassNotFoundException {
		s.defaultReadObject();
		
		_beanMap = new WeakIdentityMap>(); //bean -> Set of TrackerNode
		_equalBeansMap = new EqualBeansMap(); //bean -> beans (use to manage equal beans)
	}
	
	private static class EqualBeansMap {
		private transient WeakHashMap _innerMap = new WeakHashMap(); //bean -> EqualBeans
		private transient WeakIdentityMap _identityMap = new WeakIdentityMap(); //bean -> EqualBeans
		
		//bug #ZK-678: NotifyChange on Map is not work
		private void syncInnerMap(EqualBeans equalBeans, Object bean) {
			//hashCode of bean has changed, must reset
			boolean found = false;
			final WeakHashMap newMap = new WeakHashMap(_innerMap.size());
			//ZK-781. Copy one by one to reset _innerMap
			for(Iterator> it = _innerMap.entrySet().iterator(); it.hasNext();) {
				final Entry entry = it.next();
				final EqualBeans beans = entry.getValue();
				if (equalBeans.equals(beans)) { //found
					found = true;
					continue;
				}
				newMap.put(entry.getKey(), entry.getValue());
			}
			if (found) {
				_innerMap = newMap;
				//reput equalBeans (item inside might not equal to each other any more)
				for (Object b : equalBeans.getBeans()) {
					_identityMap.remove(b);
					put(b); //recursive
				}
			}
		}
		
		public void put(Object bean) {
			EqualBeans equalBeans = _innerMap.get(bean);
			if (equalBeans == null) { //hashcode might changed
				equalBeans = _identityMap.remove(bean);
				if (equalBeans != null) { //hashcode is changed
					syncInnerMap(equalBeans, bean);
					return;
				} else { //a new bean
					equalBeans = new EqualBeans(bean);
					_innerMap.put(bean, equalBeans);
				}
			} else {
				equalBeans.put(bean);
			}
			_identityMap.put(bean, equalBeans);
		}
		
		public void remove(Object bean) {
			EqualBeans equalBeans = _innerMap.remove(bean);
			if (equalBeans != null) {
				_identityMap.remove(bean);
				removeFromEqualBeansAndReput(equalBeans, bean);
			} else { //hashcode might changed
				equalBeans = _identityMap.remove(bean);
				if (equalBeans != null) { //hashcode is changed
					//hashCode of bean has changed, must reset
					boolean found = false;
					final WeakHashMap newMap = new WeakHashMap(_innerMap.size());   
					//ZK-781. Copy one by one to reset _innerMap
					for(Iterator> it = _innerMap.entrySet().iterator(); it.hasNext();) {
						final Entry entry = it.next();
						final EqualBeans beans = entry.getValue();
						if (equalBeans.equals(beans)) { //found
							found = true;
							continue;
						}
						newMap.put(entry.getKey(), entry.getValue());
					}
					if (found) {
						_innerMap = newMap;
						removeFromEqualBeansAndReput(equalBeans, bean); //remove from EqualBeans
					}
				}
			}
		}
		
		private void removeFromEqualBeansAndReput(EqualBeans equalBeans, Object bean) {
			final Object proxy = equalBeans.remove(bean);
			if (!equalBeans.isEmpty()) {
				_innerMap.put(proxy, equalBeans); //reput into _innerMap with new Proxy
			}
		}
		
		public Set getEqualBeans(Object bean) {
			EqualBeans equalBeans = _innerMap.get(bean);
			if (equalBeans == null) { //hashcode might changed
				equalBeans = _identityMap.remove(bean);
				if (equalBeans != null) { //hashcode is changed
					syncInnerMap(equalBeans, bean);
					equalBeans = _identityMap.get(bean);
				}
			}
			return equalBeans == null ? Collections.emptySet() : equalBeans.getBeans(); 
		}
		
		public int size() {
			return _innerMap.size();
		}
		
		private Set> entrySet() {
			return _innerMap.entrySet();
		}
	}
	
	private static class EqualBeans {
		private transient WeakReference _proxy; //surrogate object as the key for the _beanSet
		private transient WeakIdentityMap _beanSet; //different instance of beans equal to each other
		
		public EqualBeans(Object proxy) {
			_proxy = new WeakReference(proxy);
			_beanSet = new WeakIdentityMap(2);
			_beanSet.put(proxy, Boolean.TRUE);
		}
		
		public void put(Object value) {
			_beanSet.put(value, Boolean.TRUE);
		}
		
		public Set getBeans() {
			return _beanSet != null ? 
					new IdentityHashSet(_beanSet.keySet()) : Collections.emptySet();
		}
		
		//return proxy bean(could be migrated or not)
		public Object remove(Object value) {
			_beanSet.remove(value);
			if (_beanSet.isEmpty()) {
				_beanSet = null;
			} else if (System.identityHashCode(_proxy.get()) == System.identityHashCode(value)) {
				//proxy deleted, must migrate proxy
				for(final Iterator it = _beanSet.keySet().iterator(); it.hasNext(); ) { 
					final Object obj = it.next();
					if (obj != null) {
						_proxy = new WeakReference(obj); //migrate
						break;
					} else {
						it.remove();
					}
				}
			}
			return _proxy.get();
		}
		
		public boolean isEmpty() {
			return _beanSet == null || _beanSet.isEmpty();
		}
	}
	
	//------ debug dump ------//
	public void dump() {
		dumpCompMap();
		dumpBeanMap();
		dumpNullMap();
		dumpEqualBeansMap();
	}
	
	private void dumpBeanMap() {
		System.out.println("******* _beanMap: *********");
		System.out.println("******* size: "+_beanMap.size());
		for(Object bean : _beanMap.keySet()) {
			System.out.println("bean:"+bean+"------------");
			Set nodes = _beanMap.get(bean);
			if(_beanMap!=null){
				for(TrackerNode node : nodes) {
					dumpNodeTree(node, 4);
				}	
			}else{
				System.out.println("NO TrackerNode bound to this bean.");
			}
		}
	}
	
	private void dumpCompMap() {
		System.out.println("******* _compMap: *********");
		System.out.println("******* size: "+_compMap.size());
		for(Component comp: _compMap.keySet()) {
			System.out.println("comp:"+comp+"------------");
			Map nodes = _compMap.get(comp);
			for(Entry entry : nodes.entrySet()) {
				System.out.println("----field:"+entry.getKey()+"");
				dumpNodeTree(entry.getValue(), 4);
			}
		}
	}

	private void dumpNullMap() {
		System.out.println("******* _nullMap: *********");
		System.out.println("******* size: "+_nullMap.size());
		for(Object field: _nullMap.keySet()) {
			System.out.println("field:"+field+"------");
			Set nodes = _nullMap.get(field);
			for(TrackerNode node : nodes) {
				dumpNodeTree(node, 4);
			}
		}
	}

	private void dumpEqualBeansMap() {
		System.out.println("******* _equalBeansMap: *********");
		System.out.println("******* size: "+_equalBeansMap.size());
		
		for(Entry entry: _equalBeansMap.entrySet()) {
			System.out.println("proxy:"+entry.getKey());
			System.out.println("val:"+entry.getValue().getBeans());
			System.out.println("----");
		}
	}

	private void dumpNodeTree(TrackerNode node, int indent) {
		dumpNode(node, indent);
		for(TrackerNode kid : node.getDirectDependents()) {
			dumpNodeTree(kid, indent + 4);
		}
	}
	
	private void dumpNode(TrackerNode node, int spaces) {
		System.out.println(dumpSpace(spaces)+node.getFieldScript()+":"+node.getBean());
		dumpBindings(node, spaces);
		dumpPropNameMapping(node, spaces);
		dumpAssociate(node, spaces);
	}
	
	private void dumpNode0(TrackerNode node, int spaces) {
		System.out.println(dumpSpace(spaces)+node.getFieldScript()+":"+node.getBean());
		dumpBindings(node, spaces);
		dumpPropNameMapping(node, spaces);
	}
	
	private void dumpAssociate(TrackerNode node, int spaces) {
		if (node.getAssociates().isEmpty()) return; //don't dump if empty
		System.out.println(dumpSpace(spaces)+"[dependents:");
		for(TrackerNode dependent : node.getAssociates()) {
			dumpNode0(dependent, spaces+4); //avoid recursive
		}
		System.out.println(dumpSpace(spaces)+"]");
	}
	
	private void dumpBindings(TrackerNode node, int spaces) {
		if(node.getBindings().isEmpty()) return;//don't dump if empty
		System.out.println(dumpSpace(spaces)+"[bindings:");
		for(Binding binding : node.getBindings()) {
			dumpBinding(binding, spaces+4);
		}
		System.out.println(dumpSpace(spaces)+"]");
	}
	
	private void dumpBinding(Binding binding, int spaces) {
		if (binding instanceof PropertyBinding) {
			System.out.println(dumpSpace(spaces)+((PropertyBinding)binding).getPropertyString()+":"+binding);
		} else if (binding instanceof FormBinding) {
			System.out.println(dumpSpace(spaces)+((FormBinding)binding).getPropertyString()+":"+binding);
		} else if(binding instanceof ChildrenBinding){
			System.out.println(dumpSpace(spaces)+((ChildrenBinding)binding).getPropertyString()+":"+binding);
		} else if(binding instanceof ReferenceBinding) {
			System.out.println(dumpSpace(spaces)+((ReferenceBinding)binding).getPropertyString()+":"+binding);
		} else{
			System.out.println(dumpSpace(spaces)+":"+binding);
		}
	}
	
	private void dumpPropNameMapping(TrackerNode node, int spaces) {
		if(node.getPropNameMapping().size()==0) return;//don't dump if empty
		System.out.println(dumpSpace(spaces)+"[propertys:");
		for(Entry entry : node.getPropNameMapping().entrySet()) {
			dumpEntry(entry, spaces+4);
		}
		System.out.println(dumpSpace(spaces)+"]");
	}
	
	private void dumpEntry(Entry entry, int spaces) {
		System.out.println(dumpSpace(spaces)+entry.getKey()+"="+entry.getValue());
	}
	
	private String dumpSpace(int space) {
		char[] spaces = new char[space];
		Arrays.fill(spaces, ' ');
		return new String(spaces);
	}
}