view java/src/uk/ac/qmul/eecs/ccmi/diagrammodel/NodeProperties.java @ 8:ea7885bd9bff tip

fixed bug : render solid line as dotted/dashed when moving the stylus from dotted/dashed to solid
author ccmi-guest
date Thu, 03 Jul 2014 16:12:20 +0100
parents d66dd5880081
children
line wrap: on
line source
/*  
 CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool
  
 Copyright (C) 2011  Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program 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 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package uk.ac.qmul.eecs.ccmi.diagrammodel;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * This class represents the internal properties of a node. Internal properties can be seen as 
 * attributes that each single nodes owns and that, in a visual diagram, would normally be displayed inside
 * or in the neighbourhood of the node itself. This is a very high abstraction of the concept of properties 
 * of a node as the user can define their own type of properties through the property type definition object 
 * passed as argument in the constructor.  
 * An example of properties is <i>attributes</i> and <i>methods</i> of a class diagram in the UML language or just the 
 * <i>attributes</i> of entities in an ER diagram.   
 *
 */
public final class NodeProperties implements Cloneable {
	
	/**
	 * Creates a diagram element property data structure out of a property type specification. In a UML 
	 * diagram the NodeProperties for a class node would be constructed passing as arguments a linked hash 
	 * map containing the strings "attributes" and "properties" and the strings "public", "protected", "private",
	 * "static" as value for both the keys. "attributes" and "methods" would be the types of the NodeProperties,
	 *  that is, all the values inserted in the object such as values would fall under either type. For example
	 *  if a UML diagram class has the methods getX() and getY(), these would be inserted in the NodeProperties
	 *  object with a type "methods".  
	 * 
	 * @param typeDefinition a linked Hash Map holding the properties types as keys
	 * and the modifiers type definition of each property as values
	 */
	
	public NodeProperties(LinkedHashMap<String,Set<String>> typeDefinition){
		if(typeDefinition == null)
			this.typeDefinition = EMPTY_PROPERTY_TYPE_DEFINITION;
		else
			this.typeDefinition = typeDefinition;
		/* create the type collection out of the typeDefinition keyset */ 
		types = Collections.unmodifiableList(new ArrayList<String>(this.typeDefinition.keySet()));
		properties = new LinkedHashMap<String,Entry>();
		for(String s : types){
			Entry q = new Entry();
			q.values = new ArrayList<String>(); // property values to be filled by user + modifiers
			q.indexes = new ArrayList<Set<Integer>>();
			q.view = null;
			Set<String> modifierTypeDefinition = this.typeDefinition.get(s);
			if(modifierTypeDefinition == null)
				/* null modifiers for this property */
				q.modifiers = new Modifiers(EMPTY_MODIFIER_TYPE_DEFINITION,q.indexes);
			else if(modifierTypeDefinition.size() == 0)
				/* null modifiers for this property */
				q.modifiers = new Modifiers(EMPTY_MODIFIER_TYPE_DEFINITION,q.indexes);
			else
				q.modifiers = new Modifiers(modifierTypeDefinition, q.indexes);
			properties.put(s, q);
		}
	}
	
	/**
	 * Returns the type definition argument of the constructor this object has been created through.
	 *  
	 * @return the type definition
	 */
	public Map<String,Set<String>> getTypeDefinition(){
		return typeDefinition;
	}
	
	/**
	 * Returns the types of properties.
	 * 
	 * @return a list of strings holding types of properties 
	 */
	public List<String> getTypes(){
		return types;
	}
	
	/**
	 * @param propertyType the property type we want to get the values of.
	 * @return an array of string with the different properties set by the user or
	 * {@code null} if the specified type does not exist.
	 */
	public List<String> getValues(String propertyType){
		Entry e = properties.get(propertyType);
		if(e == null)
			throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType);
		return new ArrayList<String>(e.values);
	}
	
	/**
	 * Returns the view object associated with a property type in this NodeProperties instance. A view object is 
	 * defined by the client of this class and it holds the information needed for a visual representation
	 * of the property.
	 * 
	 * @param type the property type the returned view is associated with
	 * @return the View object or null if non has been set previously 
	 */
	public Object getView(String type){
		Entry e = properties.get(type);
		if(e == null)
			throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+type);
		return e.view;
	}
	
	/**
	 * Sets the View object for the a type of properties. The NodeProperties only works 
	 * as a holder as for the view objects. The client code of this class has to define
	 * its own view.    
	 * 
	 * @param type the type of property the view is associated with. 
	 * @param o an object defined by user
	 * @see #getView(String)
	 */
	public void setView(String type, Object o){
		Entry e = properties.get(type);
		if(e == null)
			throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+type);
		e.view = o;
	}
	
	/**
	 * Returns a reference to the modifier object associated with a property type. Changes to the returned 
	 * reference will affect the internal state of the NodeProperty object.
	 * 
	 * @param propertyType a property type  
	 * @return the modifiers of the specified property type or null if either
	 * the property type is null or it was not listed in the property type definition
	 * passed to the constructor
	 */
	public Modifiers getModifiers(String propertyType){
		Entry e = properties.get(propertyType);
		if(e == null)
			throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType);
		return e.modifiers;
	}
	
	/**
	 * Adds a value of the type passed as argument.
	 * 
	 * @param propertyType a property type defined in the property type definition passed as argument to the constructor
	 * @param propertyValue the value to add to the property type
	 */
	public void addValue(String propertyType, String propertyValue){
		Entry e = properties.get(propertyType);
		/* no such property type exists */
		if(e == null)
			throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType);
		e.values.add(propertyValue);
		e.indexes.add(new LinkedHashSet<Integer>());
	}
	
	/**
	 * Adds a value of the type passed as argument and sets the modifier indexes for it as for
	 *  {@link Modifiers#setIndexes(int, Set)}.
	 *  
	 * @param propertyType a property type  
	 * @param propertyValue a property value 
	 * @param modifierIndexes the modifier set of indexes
	 * 
	 * @throws IllegalArgumentException if propertyType 
	 * is not among the ones in the type definition passed as argument to the constructor 
	 */
	public void addValue(String propertyType, String propertyValue, Set<Integer> modifierIndexes){
		for(Integer i : modifierIndexes)
			if((i < 0)||(i >= getModifiers(propertyType).getTypes().size())) 
				throw new ArrayIndexOutOfBoundsException("Index "+ i +
						" corresponds to no Modifier type (modifierType size = "+
						getModifiers(propertyType).getTypes().size()+")" );
		
		Entry e = properties.get(propertyType);
		if(e == null)
			throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType);
		e.values.add(propertyValue);
		e.indexes.add(modifierIndexes);
	}
	
	/**
	 * Removes the value at the specified index for the specified property type.
	 * 
	 * @param propertyType a property type  
	 * @param valueIndex the index of the value to remove 
	 * @return the removed value 
	 * @throws IllegalArgumentException if propertyType 
	 * is not among the ones in the type definition passed as argument to the constructor
	 */
	public String  removeValue(String propertyType, int valueIndex){
		Entry e = properties.get(propertyType);
		if(e == null)
			throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType);
		e.indexes.remove(valueIndex);
		return e.values.remove(valueIndex);
	}
	
	/**
	 * Sets the value of a property type at the specified index to a new value.
	 *   
	 * @param propertyType a property type 
	 * @param valueIndex the index of the value which must be replaced
	 * @param newValue the new value for the specified index 
	 * @throws IllegalArgumentException if propertyType
	 * is not among the ones in the type definition passed as argument to the constructor
	 */
	public void setValue(String propertyType, int valueIndex, String newValue){
		Entry e = properties.get(propertyType);
		if(e == null)
			throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType);
		e.values.set(valueIndex, newValue);
	}
	
	/**
	 * Removes all the values and modifiers for a specific type.
	 * 
	 * @param propertyType the type whose property and modifiers must be removed
	 * @throws IllegalArgumentException if propertyType 
	 * is not among the ones in the type definition passed as argument to the constructor
	 */
	public void clear(String propertyType){
		Entry e = properties.get(propertyType);
		if(e == null)
			throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType);
		e.values.clear();
		e.indexes.clear();
	}
	
	/**
	 * Removes all the values and modifiers of this object.
	 */
	public void clear(){
		for(String type : types){
			clear(type);
		}
	}
	
	/**
	 * Returns true if this NodeProperties contains no values.
	 * 
	 * @return true if this NodeProperties contains no values
	 */
	public boolean isEmpty(){
		boolean empty = true;
		for(String type : types)
			if(!properties.get(type).values.isEmpty()){
				empty = false;
				break;
			}
		return empty;
	}
	
	/**
	 * Returns true if there are no values for the specified type in this NodeProperties instance.
	 * 
	 * @param propertyType the property type to be checked for value presence
	 * @return true if there are no values for the specified type in this NodeProperties instance
	 */
	public boolean typeIsEmpty(String propertyType){
		Entry e = properties.get(propertyType);
		if(e == null)
			throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+propertyType);
		return e.values.isEmpty();
	}
	
	/**
	 * true if this NodeProperties object has no types. This can happen if the constructor 
	 * has been called with an empty or null property type definition.
	 * 
	 * @return true if this NodeProperties object has no types
	 */
	public boolean isNull(){
		return types.isEmpty();
	}
	
	/**
	 * Returns a string representation of types, value and modifiers of this property. Such a 
	 * representation can be passed as argument to {@link #fill(String)} to populate a NodeProperties
	 * object. Such NodeProperties object though must have the same type and modifier definition of
	 * the object this method is called on.
	 * 
	 * @return a string representation of the values and modifiers of this object
	 */
	@Override
	public String toString(){
		StringBuilder builder = new StringBuilder();
		for(String type : types){
			builder.append(TYPE_ENTRY_SEPARATOR).append(type);
			int propertyValueIndex = 0;
			for(String value : properties.get(type).values){
				builder.append(VALUE_ENTRY_SEPARATOR);
				builder.append(value);
				for(int modifierIndex : getModifiers(type).getIndexes(propertyValueIndex)){
					builder.append(MODIFIER_ENTRY_SEPARATOR);
					builder.append(modifierIndex);
				}
				propertyValueIndex++;	
			}
		}
		return builder.toString();
	}

	/**
	 * Fills up the this property according to the string passed as arguments 
	 * The string must be generated by calling toString on a NodeProperty instance
	 * holding the same property types as type, otherwise an IllegalArgumentException 
	 * is likely to be thrown.   
	 * 
	 * @param s the string representation of the property values, such as the one returned
	 * by toString()
	 * @throws IllegalArgumentException if s contains property types which 
	 * are not among the ones in the type definition passed as argument to the constructor
	 */
	public void fill(String s){
		/* clear up previous values */
		clear();
		
		/* a property entry is a string with the type of the property followed by value           * 
		 * entries of that type. all value entries begin with the VALUE_SEPARATOR.                *
		 * a value entry is a property value followed by its modifiers entries, a modifier entry  *
		 * is a modifier beginning with the MODIFIER_SEPARATOR                                    */
		String propertyEntries[] = s.split(TYPE_ENTRY_SEPARATOR);
		for(String propertyEntry : propertyEntries){
			String[] typeEntries = propertyEntry.split(VALUE_ENTRY_SEPARATOR);
			String type = typeEntries[0];
			for(int i=1;i<typeEntries.length;i++){
				Entry e = properties.get(type);
				if(e == null)
					throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+type);
				String valueEntries[] = typeEntries[i].split(MODIFIER_ENTRY_SEPARATOR);
				String value = valueEntries[0];
				LinkedHashSet<Integer> modifiers = new LinkedHashSet<Integer>();
				for(int j=1;j<valueEntries.length;j++){
					modifiers.add(Integer.valueOf(valueEntries[j]));
				}
				addValue(type,value,modifiers);
			}
		}
	}

	@Override
	@SuppressWarnings(value = "unchecked")
	public Object clone(){
		NodeProperties p = null;
		try {
			p = (NodeProperties)super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}		
		p.properties = (LinkedHashMap<String,Entry>)properties.clone();
		for(String key : p.properties.keySet()){
			Entry old = p.properties.get(key);
			Entry q = new Entry();
			q.values = new  ArrayList<String>(); 
			q.indexes = new ArrayList<Set<Integer>>();
			q.view = old.view;
			if(old.modifiers != null){
				q.modifiers = new Modifiers(old.modifiers.modifierTypes, q.indexes);
				for(String modifierType : q.modifiers.getTypes())
					q.modifiers.setView(modifierType, old.modifiers.getView(modifierType));
			}
			p.properties.put(key, q);
		}
		return p;
	}
	
	private class Entry{
		List<String> values;
		List<Set<Integer>> indexes;
		Modifiers  modifiers;	
		Object view;
 	}
	
	/**
	 * A modifier is a label peculiar of a certain subset of properties. For example in 
	 * a UML class diagram one or more methods can be labelled as <i>public</i>, <i>private</i> or <i>protected</i>.
	 * Had a NodeProperties instance been used to represent the methods of a class node, there would be then 
	 * one modifier for each label: one for <i>public</i>, one for <i>private</i>, and one for <i>protected</i>. To each modifier
	 * a view-object can be associated, which describes how these labels would be rendered visually. 
	 * Following on from the UML example, a view-object would be used with the protected modifier
	 * to hold the information that the properties which are assigned such a modifier must be prefixed
	 * with the '#' sign.   
	 */
	public class Modifiers{		
		private Modifiers(Set<String> modifierTypes, List<Set<Integer>> indexes){
			views = new LinkedHashMap<String,Object>();
			indexesRef = indexes;
			this.modifierTypes = Collections.unmodifiableList(new ArrayList<String>(modifierTypes));
			for(String modifierType : modifierTypes){
				views.put(modifierType,null);
			}			
		}
		
		/* only used by NodeProperties.clone() method */
		private Modifiers(List<String> modifierTypes, List<Set<Integer>> indexes){
			views = new LinkedHashMap<String,Object>();
			indexesRef = indexes;
			this.modifierTypes = modifierTypes;
			for(String modifierType : modifierTypes){
				views.put(modifierType,null);
			}
		}
		
		/**
		 * Returns the list of modifier types, as per the type definition passed as argument to the NodeProperties
		 * constructor.
		 * 
		 * @return a list of modifier types
		 * @see NodeProperties#NodeProperties(LinkedHashMap)
		 */
		public List<String> getTypes(){
			return modifierTypes;
		}
		
		/**
		 * Returns the view object associated with a modifier type in this Modifier instance. A view object is 
		 * defined by the client of this class and it holds the information needed for a visual representation
		 * of the modifier. 
		 * 
		 * @param modifierType the property type the returned view is associated with
		 * @return the View object or null if non has been set previously 
		 */
		public Object getView(String modifierType){
			return views.get(modifierType);
		}
		
		/**
		 * Sets the View object for the a type of modifier. The NodeProperties only works 
		 * as a holder as for the view objects. The client code of this class has to define
		 * its own view, which will then be used by the client itself to visualise the modifier    
		 * 
		 * @param modifierType the type of modifier the view is associated with. 
		 * @param view an object defined by user defining how this property looks like
		 * @see #getView(String)
		 * @throws IllegalArgumentException if modifierType 
	     * is not among the ones in the type definition passed as argument to the constructor
		 */
		public void setView(String modifierType, Object view){
			if(!views.containsKey(modifierType))
				throw new IllegalArgumentException(ILLEGAL_TYPE_MSG+modifierType);
			views.put(modifierType,view);
		}
		
		/**
		 * Returns the modifier indexes for the specified property value. Each property type can be associated 
		 * with one or more modifier type and each property value can be assigned one or more modifier. The 
		 * integer set returned by this method tells the client code to which modifiers the property value at the index 
		 * passed as argument is assigned. The returned indexes refer to the modifier list returned by {@link #getTypes()}
		 *    
		 * @param propertyValueIndex the index of the property value for which the set of modifier indexes 
		 * is being queried 
		 * @return a set of indexes of the modifier types the property value at the index passed as argument
		 * is assigned to
		 */
		public Set<Integer> getIndexes(int propertyValueIndex){
			Set<Integer> set = indexesRef.get(propertyValueIndex);
			return (new LinkedHashSet<Integer>(set));
		}
		
		/**
		 * Set the modifier indexes for the property value at the index passed as argument 
		 * 
		 * @param propertyValueIndex the index of the property value 
		 * @param modifierIndexes a set of indexes which refer to the list returned by {@link #getTypes()}
		 * @throws ArrayIndexOutOfBoundsException if one or more of the indexes in modifierIndexes are lower than 0 or greater
		 * or equal to the size of the modifier type list returned by {@link #getTypes()}. The same exception is also thrown 
		 * if propertyValueIndex is lower than 0 or greater or equal to the property values list returned by {@link NodeProperties#getValues(String)}
		 * passing as argument the same property type passed to {@link NodeProperties#getModifiers(String)} to obtain
		 * this Modifiers instance. 
		 * @see #getIndexes(int)
		 */
		public void setIndexes(int propertyValueIndex, Set<Integer> modifierIndexes){
			for(Integer i : modifierIndexes)
				if((i < 0)||(i >= getTypes().size())) 
					throw new ArrayIndexOutOfBoundsException("Index "+ i + " corresponds to no Modifier Type (modifierType size = "+getTypes().size()+")" );

			Set<Integer> m = indexesRef.get(propertyValueIndex);
			m.clear();
			m.addAll(modifierIndexes);
		}
		
		/**
		 * Removes all the modifier indexes for a property value at the specified index.
		 *   
		 * @param propertyValueIndex the index of the property value 
		 */
		public void clear(int propertyValueIndex){
			Set<Integer> m = indexesRef.get(propertyValueIndex);
			m.clear();
		}
		
		/**
		 * true if this Modifiers object has no types. This can happen if the constructor 
		 * has been called with an empty or null modifier in the type definition passed as argument 
		 * to the constructor of the NodeProperties object holding this Modifiers object.
		 * 
		 * @return true if this Modifiers object has no types
		 */
		public boolean isNull(){
			return modifierTypes.isEmpty();
		}
		
		private LinkedHashMap<String,Object> views;
		private final List<String> modifierTypes;
		private List<Set<Integer>> indexesRef;
	}
	
	/* for each property (key) I associate a Couple (value) made out of  *
	 * a list of the property values plus a list of possible modifiers 	 */
	private LinkedHashMap<String,Entry> properties;
	private final List<String> types;
	private Map<String,Set<String>> typeDefinition;
	 
	private static final LinkedHashMap<String,Set<String>> EMPTY_PROPERTY_TYPE_DEFINITION = new LinkedHashMap<String,Set<String>>();
	private static final Set<String> EMPTY_MODIFIER_TYPE_DEFINITION = Collections.emptySet();
	private static final String ILLEGAL_TYPE_MSG = "argument must be in type definition list: ";
	/**
	 * A special static instance of the class corresponding to the null NodeProperties. A null NodeProperties instance will have isNull() returning true,
	 * which means it has no property types associated. 
	 * @see #isNull()   
	 */
	public static final NodeProperties NULL_PROPERTIES = new NodeProperties(EMPTY_PROPERTY_TYPE_DEFINITION);
	
	private final static String TYPE_ENTRY_SEPARATOR = "\n:";
	private final static String VALUE_ENTRY_SEPARATOR = "\n;";
	private final static String MODIFIER_ENTRY_SEPARATOR = "\n,";
}