package com.revolsys.record.schema;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import com.revolsys.beans.ObjectPropertyException;
import com.revolsys.collection.map.LinkedHashMapEx;
import com.revolsys.collection.map.MapEx;
import com.revolsys.collection.map.Maps;
import com.revolsys.comparator.NumericComparator;
import com.revolsys.datatype.DataType;
import com.revolsys.datatype.DataTypeProxy;
import com.revolsys.datatype.DataTypes;
import com.revolsys.geometry.model.Geometry;
import com.revolsys.geometry.operation.valid.IsValidOp;
import com.revolsys.identifier.Identifier;
import com.revolsys.io.map.MapSerializer;
import com.revolsys.properties.BaseObjectWithProperties;
import com.revolsys.record.Record;
import com.revolsys.record.code.CodeTable;
import com.revolsys.record.code.CodeTableProperty;
import com.revolsys.util.CaseConverter;
import com.revolsys.util.JavaBeanUtil;
import com.revolsys.util.MathUtil;
import com.revolsys.util.Property;
import com.revolsys.util.Strings;
/**
* The FieldDefinition class defines the name, type and other properties about each
* field on a {@link Record} in the {@link RecordDefinition}.
*
* @see Record
* @see RecordDefinition
*/
public class FieldDefinition extends BaseObjectWithProperties
implements CharSequence, Cloneable, MapSerializer, DataTypeProxy {
public static FieldDefinition newFieldDefinition(final Map<String, Object> properties) {
return new FieldDefinition(properties);
}
private final Map<Object, Object> allowedValues = new LinkedHashMap<>();
private CodeTable codeTable;
private Object defaultValue;
/** The description of the field. */
private String description;
private int index;
/** The maximum length of an field value. */
private int length;
private Object maxValue;
private Object minValue;
/** The name of the field. */
private String name;
private Reference<RecordDefinition> recordDefinition;
/** The flag indicating if a value is required for the field. */
private boolean required;
/** The maximum number of decimal places. */
private int scale;
private String title;
/** The data type of the field value. */
private DataType type;
public FieldDefinition() {
}
public FieldDefinition(final FieldDefinition field) {
this.allowedValues.putAll(field.getAllowedValues());
this.defaultValue = field.getDefaultValue();
this.description = field.getDescription();
this.length = field.getLength();
this.maxValue = field.getMaxValue();
this.minValue = field.getMinValue();
this.name = field.getName();
this.required = field.isRequired();
this.scale = field.getScale();
setTitle(field.getTitle());
this.type = field.getDataType();
final Map<String, Object> properties = field.getProperties();
setProperties(properties);
}
public FieldDefinition(final int index) {
this.index = index;
}
public FieldDefinition(final Map<String, Object> properties) {
this.name = Maps.getString(properties, "name");
final String title = Maps.getString(properties, "title");
setTitle(title);
this.description = Maps.getString(properties, "description");
this.type = DataTypes.getDataType(Maps.getString(properties, "dataType"));
this.required = Maps.getBool(properties, "required");
this.length = Maps.getInteger(properties, "length", 0);
this.scale = Maps.getInteger(properties, "scale", 0);
this.minValue = properties.get("minValue");
if (this.minValue == null) {
this.minValue = MathUtil.getMinValue(getTypeClass());
} else {
final DataType dataType = this.type;
final Object value = this.minValue;
this.minValue = dataType.toString(value);
}
if (this.maxValue == null) {
this.maxValue = MathUtil.getMaxValue(getTypeClass());
} else {
final DataType dataType = this.type;
final Object value = this.maxValue;
this.maxValue = dataType.toString(value);
}
}
/**
* Construct a new field.
*
* @param name The name of the field.
* @param type The data type of the field value.
* @param required The flag indicating if a value is required for the
* field.
*/
public FieldDefinition(final String name, final DataType type, final boolean required) {
this(name, type, 0, 0, required, null, null);
}
/**
* Construct a new field.
*
* @param name The name of the field.
* @param type The data type of the field value.
* @param length The maximum length of an field value, 0 for no maximum.
* @param required The flag indicating if a value is required for the
* field.
* @param properties The meta data properties about the field.
*/
public FieldDefinition(final String name, final DataType type, final boolean required,
final Map<String, Object> properties) {
this(name, type, 0, 0, required, properties);
}
public FieldDefinition(final String name, final DataType dataType, final boolean required,
final String description) {
this(name, dataType, 0, 0, required, description, null);
}
/**
* Construct a new field.
*
* @param name The name of the field.
* @param type The data type of the field value.
* @param length The maximum length of an field value, 0 for no maximum.
* @param required The flag indicating if a value is required for the
* field.
*/
public FieldDefinition(final String name, final DataType type, final int length,
final boolean required) {
this(name, type, length, 0, required, null, null);
}
/**
* Construct a new field.
*
* @param name The name of the field.
* @param type The data type of the field value.
* @param length The maximum length of an field value, 0 for no maximum.
* @param scale The maximum number of decimal places.
* @param required The flag indicating if a value is required for the
* field.
* @param properties The meta data properties about the field.
*/
public FieldDefinition(final String name, final DataType type, final int length,
final boolean required, final Map<String, Object> properties) {
this(name, type, length, 0, required, properties);
}
public FieldDefinition(final String name, final DataType type, final int length,
final boolean required, final String description) {
this(name, type, length, 0, required, description, null);
}
/**
* Construct a new field.
*
* @param name The name of the field.
* @param type The data type of the field value.
* @param required The flag indicating if a value is required for the
* field.
* @param properties The meta data properties about the field.
*/
public FieldDefinition(final String name, final DataType type, final Integer length,
final Integer scale, final Boolean required) {
this(name, type, length, scale, required, "");
}
/**
* Construct a new field.
*
* @param name The name of the field.
* @param type The data type of the field value.
* @param length The maximum length of an field value, 0 for no maximum.
* @param scale The maximum number of decimal places.
* @param required The flag indicating if a value is required for the
* field.
* @param properties The meta data properties about the field.
*/
public FieldDefinition(final String name, final DataType type, final Integer length,
final Integer scale, final Boolean required, final Map<String, Object> properties) {
this(name, type, length, scale, required, null, properties);
}
/**
* Construct a new field.
*
* @param name The name of the field.
* @param type The data type of the field value.
* @param length The maximum length of an field value, 0 for no maximum.
* @param scale The maximum number of decimal places.
* @param required The flag indicating if a value is required for the
* field.
* @param properties The meta data properties about the field.
*/
public FieldDefinition(final String name, final DataType type, final Integer length,
final Integer scale, final Boolean required, final String description) {
setName(name);
this.description = description;
this.type = type;
if (required != null) {
this.required = required;
}
if (length != null) {
this.length = length;
}
if (scale != null) {
this.scale = scale;
}
this.description = description;
this.minValue = MathUtil.getMinValue(getTypeClass());
this.maxValue = MathUtil.getMaxValue(getTypeClass());
}
/**
* Construct a new field.
*
* @param name The name of the field.
* @param type The data type of the field value.
* @param length The maximum length of an field value, 0 for no maximum.
* @param scale The maximum number of decimal places.
* @param required The flag indicating if a value is required for the
* field.
* @param properties The meta data properties about the field.
*/
public FieldDefinition(final String name, final DataType type, final Integer length,
final Integer scale, final Boolean required, final String description,
final Map<String, Object> properties) {
setName(name);
this.type = type;
if (required != null) {
this.required = required;
}
if (length != null) {
this.length = length;
}
if (scale != null) {
this.scale = scale;
}
this.description = description;
final Class<?> typeClass = getTypeClass();
this.minValue = MathUtil.getMinValue(typeClass);
this.maxValue = MathUtil.getMaxValue(typeClass);
setProperties(properties);
}
public void addAllowedValue(final Object value, final Object text) {
this.allowedValues.put(value, text);
}
public void appendType(final StringBuilder string) {
string.append(this.type);
if (this.length > 0) {
string.append('(');
string.append(this.length);
if (this.scale > 0) {
string.append(',');
string.append(this.scale);
}
string.append(')');
}
}
@Override
public char charAt(final int index) {
return this.name.charAt(index);
}
@Override
public FieldDefinition clone() {
return new FieldDefinition(this);
}
@Override
public boolean equals(final Object object) {
if (object instanceof FieldDefinition) {
final FieldDefinition fieldDefinition = (FieldDefinition)object;
final String name2 = fieldDefinition.getName();
return this.name.equals(name2);
} else if (object instanceof String) {
final String name2 = (String)object;
return this.name.equals(name2);
} else {
return false;
}
}
public Map<Object, Object> getAllowedValues() {
return this.allowedValues;
}
public CodeTable getCodeTable() {
return this.codeTable;
}
/**
* Get the data type of the field value.
*
* @return The data type of the field value.
*/
@Override
public DataType getDataType() {
return this.type;
}
@SuppressWarnings("unchecked")
public <T> T getDefaultValue() {
return (T)this.defaultValue;
}
public String getDescription() {
return this.description;
}
public int getIndex() {
return this.index;
}
/**
* Get the maximum length of the field value. The length 0 should be used
* if there is no maximum.
*
* @return The maximum length of an field value.
*/
public int getLength() {
return this.length;
}
public int getMaxStringLength() {
int length = this.length;
if (this.scale > 0) {
length += 1;
length += this.scale;
}
if (Number.class.isAssignableFrom(this.type.getJavaClass())) {
length += 1;
} else if (DataTypes.DATE.equals(this.type)) {
return 10;
}
return length;
}
@SuppressWarnings("unchecked")
public <V> V getMaxValue() {
return (V)this.maxValue;
}
@SuppressWarnings("unchecked")
public <V> V getMinValue() {
return (V)this.minValue;
}
/**
* Get the name of the field.
*
* @return The name of the field.
*/
public String getName() {
return this.name;
}
public RecordDefinition getRecordDefinition() {
if (this.recordDefinition == null) {
return null;
} else {
return this.recordDefinition.get();
}
}
/**
* Get the maximum number of decimal places of the field value.
*
* @return The maximum number of decimal places.
*/
public int getScale() {
return this.scale;
}
public String getSimpleType() {
final StringBuilder string = new StringBuilder();
String typeName;
if (Number.class.isAssignableFrom(getTypeClass())) {
typeName = "NUMBER";
} else if (CharSequence.class.isAssignableFrom(getTypeClass())) {
typeName = "CHARACTER";
} else {
typeName = this.type.getName().toUpperCase();
}
string.append(typeName);
if (this.length > 0) {
string.append('(');
string.append(this.length);
if (this.scale > 0) {
string.append(',');
string.append(this.scale);
}
string.append(')');
}
return string.toString();
}
public String getTitle() {
return this.title;
}
/**
* Get the data type class of the field value.
*
* @return The data type of the field value.
*/
public Class<?> getTypeClass() {
if (this.type == null) {
return Object.class;
} else {
return this.type.getJavaClass();
}
}
/**
* Get the data type of the field value.
*
* @return The data type of the field value.
*/
public String getTypeDescription() {
final StringBuilder typeDescription = new StringBuilder();
appendType(typeDescription);
return typeDescription.toString();
}
public boolean hasCodeTable() {
return this.codeTable != null;
}
/**
* Return the hash code of the field.
*
* @return The hash code.
*/
@Override
public int hashCode() {
return this.name.hashCode();
}
/**
* Get the flag indicating if a value is required for the field.
*
* @return True if a value is required, false otherwise.
*/
public boolean isRequired() {
return this.required;
}
public boolean isValid(final Object value) {
try {
validate(value);
return true;
} catch (final Throwable e) {
return false;
}
}
@Override
public int length() {
return this.name.length();
}
public FieldDefinition setAllowedValues(final Collection<?> allowedValues) {
for (final Object allowedValue : allowedValues) {
this.allowedValues.put(allowedValue, allowedValue);
}
return this;
}
public FieldDefinition setAllowedValues(final Map<?, ?> allowedValues) {
this.allowedValues.putAll(allowedValues);
return this;
}
public FieldDefinition setCodeTable(final CodeTable codeTable) {
this.codeTable = codeTable;
return this;
}
public FieldDefinition setDefaultValue(final Object defaultValue) {
this.defaultValue = defaultValue;
return this;
}
public FieldDefinition setDescription(final String description) {
this.description = description;
return this;
}
void setIndex(final int index) {
this.index = index;
}
public FieldDefinition setLength(final int length) {
this.length = length;
return this;
}
public FieldDefinition setMaxValue(final Object maxValue) {
this.maxValue = maxValue;
return this;
}
public FieldDefinition setMinValue(final Object minValue) {
this.minValue = minValue;
return this;
}
public FieldDefinition setName(final String name) {
this.name = name;
String title = getTitle();
if (!Property.hasValue(title)) {
title = CaseConverter.toCapitalizedWords(name);
setTitle(title);
}
return this;
}
protected void setRecordDefinition(final RecordDefinition recordDefinition) {
this.recordDefinition = new WeakReference<>(recordDefinition);
}
public FieldDefinition setRequired(final boolean required) {
this.required = required;
return this;
}
public FieldDefinition setScale(final int scale) {
this.scale = scale;
return this;
}
public FieldDefinition setTitle(final String title) {
if (Property.hasValue(title)) {
this.title = title;
} else {
final String name = getName();
this.title = CaseConverter.toCapitalizedWords(name);
}
return this;
}
public FieldDefinition setType(final DataType type) {
this.type = type;
return this;
}
public void setValue(final Record record, Object value) {
if (record != null) {
final int index = getIndex();
value = toFieldValue(value);
record.setValue(index, value);
}
}
public void setValueClone(final Record record, Object value) {
if (record != null) {
final int index = getIndex();
value = toFieldValue(value);
value = JavaBeanUtil.clone(value);
record.setValue(index, value);
}
}
@Override
public CharSequence subSequence(final int beginIndex, final int endIndex) {
return this.name.subSequence(beginIndex, endIndex);
}
public String toCodeString(final Object value) {
if (value == null) {
return null;
} else if (this.codeTable == null) {
if (value instanceof String) {
final String string = (String)value;
if (!Property.hasValue(string)) {
return null;
}
}
final String string = this.type.toString(value);
return string;
} else {
final Object codeValue = this.codeTable.getValue(value);
if (codeValue == null) {
if (value instanceof String) {
final String string = (String)value;
if (!Property.hasValue(string)) {
return null;
}
}
final String string = this.type.toString(value);
return string;
} else {
return codeValue.toString();
}
}
}
/**
* Convert the object to a value that is valid for the field. If the value can't be converted then
* the original value will be returned. This can result in invalid values in the record but those can be picked up
* with validation. Otherwise invalid values would silently be removed.
*
* @param value
* @return
*/
@SuppressWarnings("unchecked")
public <V> V toFieldValue(final Object value) {
try {
return toFieldValueException(value);
} catch (final Throwable e) {
return (V)value;
}
}
public <V> V toFieldValueException(final Object value) {
if (value == null) {
return null;
} else {
try {
if (value instanceof String) {
final String string = (String)value;
if (!Property.hasValue(string)) {
return null;
}
}
if (this.codeTable != null) {
final Identifier identifier = this.codeTable.getIdentifier(value);
if (identifier == null) {
throw new IllegalArgumentException(getName() + "='" + value
+ "' cannot be found in code table " + this.codeTable.getName());
} else {
return identifier.toSingleValue();
}
}
final V fieldValue = this.type.toObject(value);
return fieldValue;
} catch (final IllegalArgumentException e) {
throw e;
} catch (final Throwable e) {
throw new IllegalArgumentException(
getName() + "='" + value + "' is not a valid " + getDataType().getValidationName(), e);
}
}
}
@Override
public MapEx toMap() {
final MapEx map = new LinkedHashMapEx();
addTypeToMap(map, "field");
map.put("name", getName());
map.put("title", getTitle());
addToMap(map, "description", getDescription(), "");
map.put("dataType", getDataType().getName());
map.put("length", getLength());
map.put("scale", getScale());
map.put("required", isRequired());
addToMap(map, "minValue", getMinValue(), null);
addToMap(map, "maxValue", getMaxValue(), null);
addToMap(map, "defaultValue", getDefaultValue(), null);
addToMap(map, "allowedValues", getAllowedValues(), Collections.emptyMap());
return map;
}
@Override
public String toString() {
return this.name;
}
public String toString(final Object value) {
if (value == null) {
return null;
} else {
if (value instanceof String) {
final String string = (String)value;
if (!Property.hasValue(string)) {
return null;
}
}
final String string = this.type.toString(value);
return string;
}
}
public Object validate(Object value) {
final String fieldName = getName();
value = toFieldValueException(value);
if (value == null) {
if (isRequired()) {
throw new IllegalArgumentException(fieldName + " is required");
}
} else {
final RecordDefinition recordDefinition = getRecordDefinition();
final CodeTable codeTable = recordDefinition.getCodeTableByFieldName(fieldName);
if (codeTable == null) {
final int maxLength = getLength();
if (value instanceof Number) {
final Number number = (Number)value;
final BigDecimal bigNumber = new BigDecimal(number.toString());
final int length = bigNumber.precision();
if (maxLength > 0) {
if (length > maxLength) {
throw new IllegalArgumentException(
fieldName + "=" + value + " length " + length + " > " + maxLength);
}
}
final int scale = bigNumber.scale();
final int maxScale = getScale();
if (maxScale > 0) {
if (scale > maxScale) {
throw new IllegalArgumentException(
fieldName + "=" + value + " scale " + scale + " > " + maxScale);
}
}
final Number minValue = getMinValue();
if (minValue != null) {
if (NumericComparator.numericCompare(number, minValue) < 0) {
throw new IllegalArgumentException(fieldName + "=" + value + " > " + minValue);
}
}
final Number maxValue = getMaxValue();
if (maxValue != null) {
if (NumericComparator.numericCompare(number, maxValue) > 0) {
throw new IllegalArgumentException(fieldName + "=" + value + " < " + maxValue);
}
}
} else if (value instanceof String) {
final String string = (String)value;
final int length = string.length();
if (maxLength > 0) {
if (length > maxLength) {
throw new IllegalArgumentException(
fieldName + "=" + value + " length " + length + " > " + maxLength);
}
}
} else if (value instanceof Geometry) {
final Geometry geometry = (Geometry)value;
final IsValidOp validOp = new IsValidOp(geometry, false);
if (!validOp.isValid()) {
final String errors = Strings.toString(validOp.getErrors());
throw new IllegalArgumentException("Geometry not valid: " + errors);
}
}
if (!this.allowedValues.isEmpty()) {
if (!this.allowedValues.containsKey(value)) {
throw new IllegalArgumentException(fieldName + "=" + value + " not in ("
+ Strings.toString(",", this.allowedValues) + ")");
}
}
} else {
final Identifier id = codeTable.getIdentifier(value);
if (id == null) {
String codeTableName;
if (codeTable instanceof CodeTableProperty) {
@SuppressWarnings("resource")
final CodeTableProperty property = (CodeTableProperty)codeTable;
codeTableName = property.getTypeName();
} else {
codeTableName = codeTable.toString();
}
throw new IllegalArgumentException(
"Unable to find code for '" + value + "' in " + codeTableName);
}
}
}
return value;
}
public Object validate(final Record record, final Object value) {
final String fieldName = getName();
try {
return validate(value);
} catch (final Throwable e) {
throw new ObjectPropertyException(record, fieldName, e.getMessage(), e);
}
}
}