/** A class representing the type of a RecordToken. Copyright (c) 1997-2014 The Regents of the University of California. All rights reserved. Permission is hereby granted, without written agreement and without license or royalty fees, to use, copy, modify, and distribute this software and its documentation for any purpose, provided that the above copyright notice and the following two paragraphs appear in all copies of this software. IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. PT_COPYRIGHT_VERSION_2 COPYRIGHTENDKEY */ package ptolemy.data.type; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import ptolemy.data.OrderedRecordToken; import ptolemy.data.RecordToken; import ptolemy.data.Token; import ptolemy.graph.CPO; import ptolemy.graph.InequalityTerm; import ptolemy.kernel.util.IllegalActionException; import ptolemy.kernel.util.InternalErrorException; import ptolemy.util.StringUtilities; /////////////////////////////////////////////////////////////////// //// RecordType /** A class representing the type of a RecordToken. To set the type of a typeable object (such as a port or parameter) to a record with particular fields, create an instance of this class and call setTypeEquals() with that instance as an argument.

Note that a record type with more fields is a subtype of a record type with a subset of the fields. For example, {x = double, y = int} is a subtype of {x = double}. When a record of type {x = double, y = int} is converted to one of type {x = double}, the extra field is preserved. The converted record, therefore, may have more fields than the type indicates, but not fewer.

A consequence of this is that all record types are subtypes of the empty record type. Hence, to require that a typeable object be a record type without specifying what the fields are, use

 typeable.setTypeAtMost(BaseType.RECORD);
 
Note, however, that by itself this type constraint will not be useful because it does not, by itself, prevent the type from resolving to unknown (the unknown type is at the bottom of the type lattice, and hence satisfies this type constraint). To force a port to resolve to the empty record type, you can state
 typeable.setTypeEquals(BaseType.RECORD);
 
Equivalently, in a port configurer dialog, you can set the type of a port to record, which references an empty record. @author Yuhong Xiong, Elaine Cheong and Steve Neuendorffer; contributors: J. S. Senecal, Marten Lohstroh @version $Id: RecordType.java 70398 2014-10-22 23:44:32Z cxh $ @since Ptolemy II 3.0 @Pt.ProposedRating Red (neuendor) @Pt.AcceptedRating Red (cxh) */ public class RecordType extends AssociativeType implements Cloneable { /** Construct a new RecordType with the specified labels and types. * To leave the types of some fields undeclared, use BaseType.UNKNOWN. * The labels and the types are specified in two arrays. These two * arrays must have the same length, and their elements have one to * one correspondence. That is, the i'th entry in the types array is * the type for the i'th label in the labels array. Record labels may be * arbitrary strings. To construct the empty record type, set the length * of the argument arrays to 0. * * * @param labels An array of String. * @param types An array of Type. * @exception IllegalArgumentException If the two arrays do not have * the same size. * @exception NullPointerException If one of the arguments is null. */ public RecordType(String[] labels, Type[] types) { if (labels.length != types.length) { throw new IllegalArgumentException("RecordType: the labels " + "and types arrays do not have the same size."); } for (int i = 0; i < labels.length; i++) { if (labels[i] == null) { throw new IllegalArgumentException("RecordType: the " + i + "'th element of the labels array is null."); } //labels[i] = StringUtilities.sanitizeName(labels[i]); labels[i] = labels[i]; if (!_fields.containsKey(labels[i])) { FieldType fieldType = new FieldType(types[i]); _fields.put(labels[i], fieldType); } else { throw new IllegalArgumentException("RecordType: The " + "labels array contain duplicate element: " + labels[i]); } } } /** Construct a RecordType with the labels and values specified by * a given Map object. The object cannot contain any null keys or * values. * @param fieldMap A Map that has keys of type String and * values of type Token. * @exception IllegalActionException If the map contains null * keys or values, or if it contains non-String keys or non-Type * values */ public RecordType(Map fieldMap) throws IllegalActionException { Iterator> fields = fieldMap.entrySet() .iterator(); while (fields.hasNext()) { Map.Entry entry = fields.next(); if (entry.getKey() == null || entry.getValue() == null) { throw new IllegalActionException("RecordType: given map" + " contains either null keys or null values."); } if (!(entry.getKey() instanceof String) || !(entry.getValue() instanceof Type)) { throw new IllegalActionException("RecordType: given map" + " contains either non-String keys or" + " non-Type values."); } _fields.put(entry.getKey(), new FieldType((Type) entry.getValue())); } } /////////////////////////////////////////////////////////////////// //// public methods //// /** Return a deep copy of this RecordType if it is a variable, or * itself if it is a constant. * @return A RecordType. */ @Override public Object clone() { if (isConstant()) { return this; } else { // empty record is a constant, so this record type is not empty. // construct the labels and declared types array Object[] labelsObj = _fields.keySet().toArray(); String[] labels = new String[labelsObj.length]; Type[] types = new Type[labelsObj.length]; for (int i = 0; i < labels.length; i++) { labels[i] = (String) labelsObj[i]; FieldType fieldType = _fields.get(labels[i]); types[i] = fieldType._declaredType; } RecordType newObj = new RecordType(labels, types); try { newObj.updateType(this); } catch (IllegalActionException ex) { throw new InternalErrorException("RecordType.clone: Cannot " + "update new instance. " + ex.getMessage()); } return newObj; } } /** Convert the argument token into a RecordToken having this * type, if compatible. The argument must be a RecordToken, * and its type must be a subtype of this record type. The * argument token must have at least the fields of this type. * Extra fields in the argument token that are not in this * type are preserved. * @param token A token. * @return An RecordToken. * @exception IllegalActionException If token is incompatible * with this type. */ @Override public Token convert(Token token) throws IllegalActionException { if (!isCompatible(token.getType())) { throw new IllegalArgumentException( Token.notSupportedConversionMessage(token, this.toString())); } RecordToken recordToken = (RecordToken) token; // Undeclared fields are preserved. // If we don't do this, it would not be possible to express // type constraints to accommodate actors that operate // on the entirety of any received record, which may be just // any record, or a record that must least contains a certain // set of labels. Object[] labelArray = recordToken.labelSet().toArray(); // Arrays that will be used to create the new token. String[] labelStringArray = new String[labelArray.length]; Token[] values = new Token[labelArray.length]; for (int i = 0; i < labelArray.length; i++) { String label = (String) labelArray[i]; // Convert each field of the record. Token fieldToken = recordToken.get(label); Type newFieldType = get(label); // If the type of the field is specified, then convert it. // Otherwise, leave it alone. if (newFieldType != null) { values[i] = newFieldType.convert(fieldToken); } else { values[i] = fieldToken; } // Store the label for each field. labelStringArray[i] = label; } // If the original token preserves ordering of the // fields, then the new one will too. // FIXME: This seems questionable. But if we don't // do this, then the ordering will be mysteriously lost // when, for example, an OrderedRecordToken is put // into an ArrayToken. A better solution would be // to include OrderedRecordToken in the type system. if (recordToken instanceof OrderedRecordToken) { return new OrderedRecordToken(labelStringArray, values); } else { return new RecordToken(labelStringArray, values); } } /** Return the depth of a record type. The depth of a * record type is the number of times it * contains other structured types. For example, a record * of arrays has depth 2. * @return the depth of a record type. */ @Override public int depth() { Object[] labelsObj = _fields.keySet().toArray(); String[] labels = new String[labelsObj.length]; int[] depth = new int[labelsObj.length]; int maxDepth = 1; for (int i = 0; i < labels.length; i++) { labels[i] = (String) labelsObj[i]; Type fieldType = get(labels[i]); depth[i] = 1; if (fieldType instanceof StructuredType) { depth[i] += ((StructuredType) fieldType).depth(); } if (depth[i] > maxDepth) { maxDepth = depth[i]; } } return maxDepth; } /** Determine if the argument represents the same RecordType as this * object. Two record types are equal if they have the same field names * and the type of each field is the same. * @param object Another object. * @return True if the argument represents the same RecordType as * this object. */ @Override public boolean equals(Object object) { if (!(object instanceof RecordType)) { return false; } RecordType recordType = (RecordType) object; // Check that the label sets are equal Set myLabelSet = _fields.keySet(); Set argLabelSet = recordType._fields.keySet(); if (!myLabelSet.equals(argLabelSet)) { return false; } Iterator fieldNames = myLabelSet.iterator(); while (fieldNames.hasNext()) { String label = fieldNames.next(); Type myType = this.get(label); Type argType = recordType.get(label); if (!myType.equals(argType)) { return false; } } return true; } /** Return the type of the specified label. If this type does not * contain the specified label, return null. * @param label The specified label. * @return a Type. */ @Override public Type get(String label) { FieldType fieldType = _fields.get(label); if (fieldType == null) { return null; } return fieldType._resolvedType; } /** Return the class for tokens that this type represents. * @return The class for tokens that this type represents. */ @Override public Class getTokenClass() { return RecordToken.class; } /** Return the InequalityTerm representing the type of the specified * label. * @param label The specified label. * @return An InequalityTerm. * @see ptolemy.graph.InequalityTerm */ public InequalityTerm getTypeTerm(String label) { return _fields.get(label); } /** Return a hash code value for this object. */ @Override public int hashCode() { return _fields.keySet().hashCode() + 2917; } /** Return true if this type does not correspond to a single token * class. This occurs if the type is not instantiable, or it * represents either an abstract base class or an interface. * @return true if the type of any field is abstract. */ @Override public boolean isAbstract() { // Loop through all of the fields. Iterator fieldNames = _fields.keySet().iterator(); while (fieldNames.hasNext()) { String label = fieldNames.next(); Type type = this.get(label); // Return false if the field is not instantiable. if (type.isAbstract()) { return true; } } return false; } /** Set the elements that have declared type BaseType.UNKNOWN (the leaf * type variable) to the specified type. * @param type the type to set the leaf type variable to. */ @Override public void initialize(Type type) { try { Iterator fieldNames = _fields.keySet().iterator(); while (fieldNames.hasNext()) { String label = fieldNames.next(); FieldType fieldType = _fields.get(label); if (fieldType.isSettable()) { fieldType.initialize(type); } } } catch (IllegalActionException iae) { throw new InternalErrorException("RecordType.initialize: Cannot " + "initialize the element type to " + type + " " + iae.getMessage()); } } /** Test whether the argument type is compatible with this type * (is less than or equal to this type in the type lattice). The * given type will be compatible with this type if it is * BaseType.UNKNOWN, or it is a RecordType that contains at * least the fields of this record type. * @param type An instance of Type. * @return True if the argument is compatible with this type. */ @Override public boolean isCompatible(Type type) { if (type.equals(BaseType.UNKNOWN)) { return true; } if (!(type instanceof RecordType)) { return false; } RecordType argumentRecordType = (RecordType) type; // Loop through all of the fields of this type... Iterator iterator = _fields.keySet().iterator(); while (iterator.hasNext()) { String label = iterator.next(); // The given type cannot be losslessly converted to this type // if it does not contain one of the fields of this type. Type argumentFieldType = argumentRecordType.get(label); if (argumentFieldType == null) { // argument token does not contain this label return false; } // The given type cannot be losslessly converted to this type // if the individual fields are not compatible. Type thisFieldType = this.get(label); if (!thisFieldType.isCompatible(argumentFieldType)) { return false; } } return true; } /** Test if this RecordType is a constant. A RecordType is a constant if * the declared type of all of its fields are constant. * @return True if this type is a constant. */ @Override public boolean isConstant() { // Loop through all of the fields. Iterator fieldTypes = _fields.values().iterator(); while (fieldTypes.hasNext()) { FieldType fieldType = fieldTypes.next(); Type type = fieldType._declaredType; // Return false if the field is not constant. if (!type.isConstant()) { return false; } } return true; } /** Test if this type corresponds to an instantiable token * class. A RecordType is instantiable if all of its fields are * instantiable. * @return True if this type is instantiable. */ @Override public boolean isInstantiable() { // Loop through all of the fields. Iterator fieldNames = _fields.keySet().iterator(); while (fieldNames.hasNext()) { String label = fieldNames.next(); Type type = this.get(label); // Return false if the field is not instantiable. if (!type.isInstantiable()) { return false; } } return true; } /** Test whether the specified type is a substitution instance of this * type. One record is a substitution instance of another if they * have fields with the same names and each field of the given type is * a substitution instance of the corresponding field in this type. * @param type A Type. * @return True if the argument is a substitution instance of this type. * @see Type#isSubstitutionInstance */ @Override public boolean isSubstitutionInstance(Type type) { if (!(type instanceof RecordType)) { return false; } RecordType recordType = (RecordType) type; // Check if this record type and the argument have the same // label set. Set myLabelSet = _fields.keySet(); Set argLabelSet = recordType._fields.keySet(); if (!myLabelSet.equals(argLabelSet)) { return false; } // Loop over all the labels. Iterator fieldNames = myLabelSet.iterator(); while (fieldNames.hasNext()) { String label = fieldNames.next(); FieldType fieldType = _fields.get(label); Type myDeclaredType = fieldType._declaredType; Type argType = recordType.get(label); if (!myDeclaredType.isSubstitutionInstance(argType)) { return false; } } return true; } /** Return the labels of this record type as a Set. * @return A Set containing strings. */ public Set labelSet() { return _fields.keySet(); } /** Return the string representation of this type. The format is * {label = type, label = type, ...}. * The record fields are listed in the lexicographical order of the * labels determined by the java.lang.String.compareTo() method. * @return A String. */ @Override public String toString() { Object[] labelArray = _fields.keySet().toArray(); // Order the labels int size = labelArray.length; for (int i = 0; i < size - 1; i++) { for (int j = i + 1; j < size; j++) { String labeli = (String) labelArray[i]; String labelj = (String) labelArray[j]; if (labeli.compareTo(labelj) >= 0) { Object temp = labelArray[i]; labelArray[i] = labelArray[j]; labelArray[j] = temp; } } } // construct the string representation of this token. StringBuffer results = new StringBuffer("{"); for (int i = 0; i < size; i++) { String label = (String) labelArray[i]; String type = this.get(label).toString(); if (i != 0) { results.append(", "); } // quote and escape labels that are not valid Java identifiers if (!StringUtilities.isValidIdentifier(label)) { label = "\"" + StringUtilities.escapeString(label) + "\""; } results.append(label + " = " + type); } return results.toString() + "}"; } /** Update this Type to the specified RecordType. * The specified type must be a RecordType and have the same structure * as this one, and have depth less than the MAXDEPTHDOUND. * This method will only update the component whose declared type is * BaseType.UNKNOWN, and leave the constant part of this type intact. * @param newType A StructuredType. * @exception IllegalActionException If the specified type is not a * RecordType or it does not have the same structure as this one. */ @Override public void updateType(StructuredType newType) throws IllegalActionException { super.updateType(newType); if (this.isConstant()) { if (this.equals(newType)) { return; } throw new IllegalActionException("RecordType.updateType: " + "This type is a constant and the argument is not the" + " same as this type. This type: " + this.toString() + " argument: " + newType.toString()); } // This type is a variable. if (!this.isSubstitutionInstance(newType)) { throw new IllegalActionException("RecordType.updateType: " + "Cannot update this type to the new type."); } Iterator fieldNames = _fields.keySet().iterator(); while (fieldNames.hasNext()) { String label = fieldNames.next(); FieldType fieldType = _fields.get(label); if (fieldType.isSettable()) { Type newFieldType = ((RecordType) newType).get(label); fieldType.setValue(newFieldType); } } } /////////////////////////////////////////////////////////////////// //// public fields //// /** An empty record. */ public static RecordType EMPTY_RECORD = new RecordType(new String[0], new Type[0]); /////////////////////////////////////////////////////////////////// //// protected methods //// /** Compare this type with the specified type. The specified type * must be a RecordType, otherwise an exception will be thrown. * * This method returns one of ptolemy.graph.CPO.LOWER, * ptolemy.graph.CPO.SAME, ptolemy.graph.CPO.HIGHER, * ptolemy.graph.CPO.INCOMPARABLE, indicating this type is lower * than, equal to, higher than, or incomparable with the * specified type in the type hierarchy, respectively. * @param type a RecordType. * @return An integer. * @exception IllegalArgumentException If the specified type is * not a RecordType. */ @Override protected int _compare(StructuredType type) { if (!(type instanceof RecordType)) { throw new IllegalArgumentException("RecordType._compare: " + "The argument is not a RecordType."); } if (this.equals(type)) { return CPO.SAME; } if (_isLessThanOrEqualTo(this, (RecordType) type)) { return CPO.LOWER; } if (_isLessThanOrEqualTo((RecordType) type, this)) { return CPO.HIGHER; } return CPO.INCOMPARABLE; } /** Return a static instance of RecordType. * @return a RecordType. */ @Override protected StructuredType _getRepresentative() { return EMPTY_RECORD; } /** Return the greatest lower bound of this type with the specified * type. The specified type must be a RecordType, otherwise an * exception will be thrown. * @param type a RecordType. * @return a RecordType. * @exception IllegalArgumentException If the specified type is * not a RecordType. */ @Override protected StructuredType _greatestLowerBound(StructuredType type) { if (!(type instanceof RecordType)) { throw new IllegalArgumentException( "RecordType.greatestLowerBound: The argument is not a " + "RecordType."); } RecordType recordType = (RecordType) type; // the label set of the GLB is the union of the two label sets. Set unionSet = new HashSet(); Set myLabelSet = _fields.keySet(); Set argLabelSet = recordType._fields.keySet(); unionSet.addAll(myLabelSet); unionSet.addAll(argLabelSet); // construct the GLB RecordToken Object[] labelArray = unionSet.toArray(); int size = labelArray.length; String[] labels = new String[size]; Type[] types = new Type[size]; for (int i = 0; i < size; i++) { labels[i] = (String) labelArray[i]; Type type1 = this.get(labels[i]); Type type2 = recordType.get(labels[i]); if (type1 == null) { types[i] = type2; } else if (type2 == null) { types[i] = type1; } else { types[i] = (Type) TypeLattice.lattice().greatestLowerBound( type1, type2); } } return new RecordType(labels, types); } /** Return the least Upper bound of this type with the specified * type. The specified type must be a RecordType, otherwise an * exception will be thrown. * @param type a RecordType. * @return a RecordType. * @exception IllegalArgumentException If the specified type is * not a RecordType. */ @Override protected StructuredType _leastUpperBound(StructuredType type) { if (!(type instanceof RecordType)) { throw new IllegalArgumentException("RecordType.leastUpperBound: " + "The argument is not a RecordType."); } RecordType recordType = (RecordType) type; // the label set of the LUB is the intersection of the two label sets. Set intersectionSet = new HashSet(); Set myLabelSet = _fields.keySet(); Set argLabelSet = recordType._fields.keySet(); intersectionSet.addAll(myLabelSet); intersectionSet.retainAll(argLabelSet); // construct the GLB RecordToken Object[] labelArray = intersectionSet.toArray(); int size = labelArray.length; String[] labels = new String[size]; Type[] types = new Type[size]; for (int i = 0; i < size; i++) { labels[i] = (String) labelArray[i]; Type type1 = this.get(labels[i]); Type type2 = recordType.get(labels[i]); types[i] = (Type) TypeLattice.lattice().leastUpperBound(type1, type2); } return new RecordType(labels, types); } /////////////////////////////////////////////////////////////////// //// private methods //// // Test if the first RecordType is less than or equal to the second private boolean _isLessThanOrEqualTo(RecordType t1, RecordType t2) { Set labelSet1 = t1._fields.keySet(); Set labelSet2 = t2._fields.keySet(); if (!labelSet1.containsAll(labelSet2)) { return false; } // iterate over the labels of the second type Iterator iter = labelSet2.iterator(); while (iter.hasNext()) { String label = iter.next(); Type type1 = t1.get(label); Type type2 = t2.get(label); int result = TypeLattice.compare(type1, type2); if (result == CPO.HIGHER || result == CPO.INCOMPARABLE) { return false; } } return true; } /////////////////////////////////////////////////////////////////// //// private variables //// // Mapping from label to field information. private Map _fields = new HashMap(); /////////////////////////////////////////////////////////////////// //// inner class //// // A class that encapsulates the declared and resolved types of a // field and implements the InequalityTerm interface. private class FieldType implements InequalityTerm { // Construct an instance of FieldType. private FieldType(Type declaredType) { try { _declaredType = (Type) declaredType.clone(); _resolvedType = _declaredType; } catch (CloneNotSupportedException cnse) { throw new InternalErrorException("RecordType.FieldType: " + "The specified type cannot be cloned."); } } /////////////////////////////////////////////////////////////// //// public inner methods //// /** Return this RecordType. * @return a RecordType. */ @Override public Object getAssociatedObject() { return RecordType.this; } /** Return the resolved type. * @return a Type. */ @Override public Object getValue() { return _resolvedType; } /** Return this FieldType in an array if it represents a type * variable. Otherwise, return an array of size zero. * @return An array of InequalityTerm. */ @Override public InequalityTerm[] getVariables() { if (isSettable()) { InequalityTerm[] variable = new InequalityTerm[1]; variable[0] = this; return variable; } return new InequalityTerm[0]; } /** Reset the variable part of the element type to the specified * type. * @param e A Type. * @exception IllegalActionException If this type is not settable, * or the argument is not a Type. */ @Override public void initialize(Object e) throws IllegalActionException { if (!isSettable()) { throw new IllegalActionException("RecordType$FieldType." + "initialize: The type is not settable."); } if (!(e instanceof Type)) { throw new IllegalActionException("FieldType.initialize: " + "The argument is not a Type."); } if (_declaredType == BaseType.UNKNOWN) { _resolvedType = (Type) e; } else { // this field type is a structured type. ((StructuredType) _resolvedType).initialize((Type) e); } } /** Test if this field type is a type variable. * @return True if this field type is a type variable. */ @Override public boolean isSettable() { return !_declaredType.isConstant(); } /** Check whether the current element type is acceptable. * The element type is acceptable if it represents an * instantiable object. * @return True if the element type is acceptable. */ @Override public boolean isValueAcceptable() { return _resolvedType.isInstantiable(); } /** Set the element type to the specified type. * @param e a Type. * @exception IllegalActionException If the specified type violates * the declared field type. */ @Override public void setValue(Object e) throws IllegalActionException { if (!isSettable()) { throw new IllegalActionException( "RecordType$FieldType.setValue: The type is not " + "settable."); } if (!_declaredType.isSubstitutionInstance((Type) e)) { throw new IllegalActionException("FieldType.setValue: " + "Cannot update the field type of this RecordType " + "to the new type." + " Field type: " + _declaredType.toString() + ", New type: " + e.toString()); } if (_declaredType == BaseType.UNKNOWN) { try { _resolvedType = (Type) ((Type) e).clone(); } catch (CloneNotSupportedException cnse) { throw new InternalErrorException( "RecordType$FieldType.setValue: " + "The specified type cannot be cloned."); } } else { ((StructuredType) _resolvedType).updateType((StructuredType) e); } } /** Return a string representation of this term. * @return A String. */ @Override public String toString() { return "(RecordFieldType, " + getValue() + ")"; } /////////////////////////////////////////////////////////////// //// private inner variables //// private Type _declaredType = null; private Type _resolvedType = null; } }