package com.revolsys.record.schema; import java.io.IOException; import java.io.ObjectInputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.PreDestroy; import com.revolsys.collection.CollectionUtil; import com.revolsys.collection.list.Lists; import com.revolsys.collection.map.LinkedHashMapEx; import com.revolsys.collection.map.MapEx; import com.revolsys.collection.map.Maps; import com.revolsys.collection.map.WeakKeyValueMap; import com.revolsys.collection.set.Sets; import com.revolsys.datatype.DataType; import com.revolsys.datatype.DataTypes; import com.revolsys.geometry.model.ClockDirection; import com.revolsys.geometry.model.Geometry; import com.revolsys.geometry.model.GeometryFactory; import com.revolsys.io.PathName; import com.revolsys.io.map.MapObjectFactory; import com.revolsys.logging.Logs; import com.revolsys.record.ArrayRecord; import com.revolsys.record.Record; import com.revolsys.record.RecordFactory; import com.revolsys.record.code.CodeTable; import com.revolsys.record.code.CodeTableProperty; import com.revolsys.record.property.FieldProperties; import com.revolsys.record.property.RecordDefinitionProperty; import com.revolsys.record.property.ValueRecordDefinitionProperty; public class RecordDefinitionImpl extends AbstractRecordStoreSchemaElement implements RecordDefinition { private static final AtomicInteger INSTANCE_IDS = new AtomicInteger(0); private static final Map<Integer, RecordDefinitionImpl> RECORD_DEFINITION_CACHE = new WeakKeyValueMap<>(); public static void destroy(final RecordDefinitionImpl... recordDefinitionList) { for (final RecordDefinitionImpl recordDefinition : recordDefinitionList) { recordDefinition.destroy(); } } public static RecordDefinition getRecordDefinition(final int instanceId) { return RECORD_DEFINITION_CACHE.get(instanceId); } public static RecordDefinitionImpl newRecordDefinition(final Map<String, Object> properties) { return new RecordDefinitionImpl(properties); } private ClockDirection polygonOrientation = ClockDirection.OGC_SFS_COUNTER_CLOCKWISE; private Map<String, CodeTable> codeTableByFieldNameMap = new HashMap<>(); private Map<String, Object> defaultValues = new HashMap<>(); private String description; private final Map<String, Integer> fieldIdMap = new HashMap<>(); private final Map<String, FieldDefinition> fieldMap = new HashMap<>(); private List<String> fieldNames = Collections.emptyList(); private Set<String> fieldNamesSet = Collections.emptySet(); private List<FieldDefinition> fields = Collections.emptyList(); /** The index of the primary geometry field. */ private int geometryFieldDefinitionIndex = -1; private final List<Integer> geometryFieldDefinitionIndexes = new ArrayList<>(); private final List<String> geometryFieldDefinitionNames = new ArrayList<>(); /** The index of the ID field. */ private int idFieldDefinitionIndex = -1; private final List<Integer> idFieldDefinitionIndexes = new ArrayList<>(); private final List<String> idFieldDefinitionNames = new ArrayList<>(); private final List<FieldDefinition> idFieldDefinitions = new ArrayList<>(); private final Integer instanceId = INSTANCE_IDS.getAndIncrement(); private final List<String> internalFieldNames = new ArrayList<>(); private final List<FieldDefinition> internalFields = new ArrayList<>(); private RecordDefinitionFactory recordDefinitionFactory; @SuppressWarnings({ "unchecked", "rawtypes" }) private RecordFactory<Record> recordFactory = (RecordFactory)ArrayRecord.FACTORY; private final Map<String, Collection<Object>> restrictions = new HashMap<>(); private final List<RecordDefinition> superClasses = new ArrayList<>(); private GeometryFactory geometryFactory; public RecordDefinitionImpl() { super(null, (PathName)null); } @SuppressWarnings("unchecked") public RecordDefinitionImpl(final Map<String, Object> properties) { this(PathName.newPathName(Maps.getString(properties, "path"))); final List<Object> fields = (List<Object>)properties.get("fields"); for (final Object object : fields) { if (object instanceof FieldDefinition) { final FieldDefinition field = (FieldDefinition)object; addField(field.clone()); } else if (object instanceof Map) { final Map<String, Object> fieldProperties = (Map<String, Object>)object; final FieldDefinition field = FieldDefinition.newFieldDefinition(fieldProperties); addField(field); } } final Object geometryFactoryProperty = properties.get("geometryFactory"); if (geometryFactoryProperty instanceof Map) { final Map<String, Object> geometryFactoryDef = (Map<String, Object>)geometryFactoryProperty; if (geometryFactoryDef != null) { final GeometryFactory geometryFactory = MapObjectFactory.toObject(geometryFactoryDef); setGeometryFactory(geometryFactory); } } else if (geometryFactoryProperty instanceof GeometryFactory) { final GeometryFactory geometryFactory = (GeometryFactory)geometryFactoryProperty; setGeometryFactory(geometryFactory); } } public RecordDefinitionImpl(final PathName path) { super(path); RECORD_DEFINITION_CACHE.put(this.instanceId, this); } public RecordDefinitionImpl(final PathName path, final FieldDefinition... fields) { this(path, null, fields); } public RecordDefinitionImpl(final PathName path, final List<FieldDefinition> fields) { this(path, null, fields); } public RecordDefinitionImpl(final PathName path, final Map<String, Object> properties, final FieldDefinition... fields) { this(path, properties, Arrays.asList(fields)); } public RecordDefinitionImpl(final PathName path, final Map<String, Object> properties, final List<FieldDefinition> fields) { super(path); for (final FieldDefinition field : fields) { addField(field.clone()); } cloneProperties(properties); RECORD_DEFINITION_CACHE.put(this.instanceId, this); } public RecordDefinitionImpl(final RecordDefinition recordDefinition) { this(recordDefinition.getPathName(), recordDefinition.getProperties(), recordDefinition.getFields()); setPolygonRingDirection(recordDefinition.getPolygonRingDirection()); setIdFieldIndex(recordDefinition.getIdFieldIndex()); RECORD_DEFINITION_CACHE.put(this.instanceId, this); } public RecordDefinitionImpl(final RecordStoreSchema schema, final PathName pathName) { super(schema, pathName); final RecordStore recordStore = getRecordStore(); if (recordStore != null) { this.recordFactory = recordStore.getRecordFactory(); } RECORD_DEFINITION_CACHE.put(this.instanceId, this); } public RecordDefinitionImpl(final RecordStoreSchema schema, final PathName path, final Map<String, Object> properties, final List<FieldDefinition> fields) { this(schema, path); for (final FieldDefinition field : fields) { addField(field.clone()); } cloneProperties(properties); } public RecordDefinitionImpl(final RecordStoreSchema schema, final RecordDefinition recordDefinition) { this(schema, recordDefinition.getPathName()); for (final FieldDefinition field : recordDefinition.getFields()) { addField(field.clone()); } cloneProperties(recordDefinition.getProperties()); } public RecordDefinitionImpl(final String pathName) { this(PathName.newPathName(pathName)); } @Override public void addDefaultValue(final String fieldName, final Object defaultValue) { this.defaultValues.put(fieldName, defaultValue); } public synchronized void addField(final FieldDefinition field) { final int index = this.fieldNames.size(); final String name = field.getName(); String lowerName; if (name == null) { lowerName = null; } else { lowerName = name.toLowerCase(); } this.internalFieldNames.add(name); this.fieldNames = Lists.unmodifiable(this.internalFieldNames); this.fieldNamesSet = Sets.unmodifiableLinked(this.internalFieldNames); this.internalFields.add(field); this.fields = Lists.unmodifiable(this.internalFields); this.fieldMap.put(lowerName, field); this.fieldIdMap.put(lowerName, this.fieldIdMap.size()); final DataType dataType = field.getDataType(); if (dataType == null) { Logs.debug(this, field.toString()); } else { final Class<?> dataClass = dataType.getJavaClass(); if (Geometry.class.isAssignableFrom(dataClass)) { this.geometryFieldDefinitionIndexes.add(index); this.geometryFieldDefinitionNames.add(name); if (this.geometryFieldDefinitionIndex == -1) { this.geometryFieldDefinitionIndex = index; final GeometryFactory geometryFactory = field .getProperty(FieldProperties.GEOMETRY_FACTORY); if (geometryFactory == null && this.geometryFactory != null) { field.setProperty(FieldProperties.GEOMETRY_FACTORY, this.geometryFactory); } } } } field.setIndex(index); field.setRecordDefinition(this); final CodeTable codeTable = field.getCodeTable(); addFieldCodeTable(name, codeTable); } /** * Adds an field with the given case-sensitive name. * */ public FieldDefinition addField(final String fieldName, final DataType type) { return addField(fieldName, type, false); } public FieldDefinition addField(final String name, final DataType type, final boolean required) { final FieldDefinition field = new FieldDefinition(name, type, required); addField(field); return field; } public FieldDefinition addField(final String name, final DataType type, final int length, final boolean required) { final FieldDefinition field = new FieldDefinition(name, type, length, required); addField(field); return field; } public FieldDefinition addField(final String fieldName, final DataType type, final int length, final int scale) { final FieldDefinition field = new FieldDefinition(fieldName, type, length, scale, false); addField(field); return field; } public FieldDefinition addField(final String name, final DataType type, final int length, final int scale, final boolean required) { final FieldDefinition field = new FieldDefinition(name, type, length, scale, required); addField(field); return field; } public void addFieldCodeTable(final String fieldName, final CodeTable codeTable) { if (codeTable != null && fieldName != null) { this.codeTableByFieldNameMap.put(fieldName.toUpperCase(), codeTable); } } public void addRestriction(final String fieldPath, final Collection<Object> values) { this.restrictions.put(fieldPath, values); } public void addSuperClass(final RecordDefinition superClass) { if (!this.superClasses.contains(superClass)) { this.superClasses.add(superClass); } } public void cloneProperties(final Map<String, Object> properties) { if (properties != null) { for (final Entry<String, Object> property : properties.entrySet()) { final String propertyName = property.getKey(); final Object value = property.getValue(); if (value instanceof RecordDefinitionProperty) { RecordDefinitionProperty recordDefinitionProperty = (RecordDefinitionProperty)value; recordDefinitionProperty = recordDefinitionProperty.clone(); recordDefinitionProperty.setRecordDefinition(this); setProperty(propertyName, recordDefinitionProperty); } else { setProperty(propertyName, value); } } } } @Override public void deleteRecord(final Record record) { final RecordStore recordStore = getRecordStore(); if (recordStore == null) { throw new UnsupportedOperationException(); } else { recordStore.deleteRecord(record); } } @Override @PreDestroy public void destroy() { super.close(); RECORD_DEFINITION_CACHE.remove(this.instanceId); this.fieldIdMap.clear(); this.fieldMap.clear(); this.internalFieldNames.clear(); this.fields = Collections.emptyList(); this.internalFields.clear(); this.fieldNames = Collections.emptyList(); this.fieldNamesSet = Collections.emptySet(); this.codeTableByFieldNameMap.clear(); this.recordFactory = null; this.recordDefinitionFactory = new RecordDefinitionFactoryImpl(); this.defaultValues.clear(); this.description = ""; this.geometryFieldDefinitionIndex = -1; this.geometryFieldDefinitionIndexes.clear(); this.geometryFieldDefinitionNames.clear(); this.restrictions.clear(); this.superClasses.clear(); } @Override public CodeTable getCodeTableByFieldName(final CharSequence fieldName) { if (fieldName == null) { return null; } else { final RecordStore recordStore = getRecordStore(); CodeTable codeTable; final FieldDefinition fieldDefinition = getField(fieldName); if (fieldDefinition != null) { codeTable = fieldDefinition.getCodeTable(); if (codeTable != null) { return codeTable; } } codeTable = this.codeTableByFieldNameMap.get(fieldName.toString().toUpperCase()); if (codeTable == null && recordStore != null) { codeTable = recordStore.getCodeTableByFieldName(fieldName); } if (codeTable instanceof CodeTableProperty) { @SuppressWarnings("resource") final CodeTableProperty property = (CodeTableProperty)codeTable; if (property.getRecordDefinition() == this) { return null; } } if (fieldDefinition != null && codeTable != null) { fieldDefinition.setCodeTable(codeTable); } return codeTable; } } @Override public Object getDefaultValue(final String fieldName) { return this.defaultValues.get(fieldName); } @Override public Map<String, Object> getDefaultValues() { return this.defaultValues; } public String getDescription() { return this.description; } @Override public FieldDefinition getField(final CharSequence name) { if (name == null) { return null; } else { final String lowerName = name.toString().toLowerCase(); return this.fieldMap.get(lowerName); } } @Override public FieldDefinition getField(final int i) { if (i >= 0 && i < this.fields.size()) { return this.fields.get(i); } else { return null; } } @Override public Class<?> getFieldClass(final CharSequence name) { final DataType dataType = getFieldType(name); if (dataType == null) { return Object.class; } else { return dataType.getJavaClass(); } } @Override public Class<?> getFieldClass(final int i) { final DataType dataType = getFieldType(i); if (dataType == null) { return Object.class; } else { return dataType.getJavaClass(); } } @Override public int getFieldCount() { return this.fields.size(); } @Override public int getFieldIndex(final CharSequence name) { if (name == null) { return -1; } else { final String lowerName = name.toString().toLowerCase(); final Integer fieldId = this.fieldIdMap.get(lowerName); if (fieldId == null) { return -1; } else { return fieldId; } } } @Override public int getFieldLength(final int i) { try { final FieldDefinition field = this.fields.get(i); return field.getLength(); } catch (final ArrayIndexOutOfBoundsException e) { throw e; } } @Override public String getFieldName(final int i) { if (this.fields != null && i >= 0 && i < this.fields.size()) { final FieldDefinition field = this.fields.get(i); return field.getName(); } return null; } @Override public List<String> getFieldNames() { return this.fieldNames; } @Override public Set<String> getFieldNamesSet() { return this.fieldNamesSet; } @Override public List<FieldDefinition> getFields() { return this.fields; } @Override public int getFieldScale(final int i) { final FieldDefinition field = this.fields.get(i); return field.getScale(); } @Override public List<String> getFieldTitles() { final List<String> titles = new ArrayList<>(); for (final FieldDefinition field : getFields()) { titles.add(field.getTitle()); } return titles; } @Override public DataType getFieldType(final CharSequence name) { final int index = getFieldIndex(name); if (index == -1) { return null; } else { return getFieldType(index); } } @Override public DataType getFieldType(final int i) { final FieldDefinition field = this.fields.get(i); return field.getDataType(); } @Override public GeometryFactory getGeometryFactory() { final FieldDefinition geometryFieldDefinition = getGeometryField(); if (geometryFieldDefinition == null) { return null; } else { final GeometryFactory geometryFactory = geometryFieldDefinition .getProperty(FieldProperties.GEOMETRY_FACTORY); if (geometryFactory == null) { return this.geometryFactory; } return geometryFactory; } } @Override public FieldDefinition getGeometryField() { if (this.geometryFieldDefinitionIndex == -1 && this.geometryFieldDefinitionIndex < this.fields.size()) { return null; } else { return this.fields.get(this.geometryFieldDefinitionIndex); } } @Override public int getGeometryFieldIndex() { return this.geometryFieldDefinitionIndex; } @Override public List<Integer> getGeometryFieldIndexes() { return Collections.unmodifiableList(this.geometryFieldDefinitionIndexes); } @Override public String getGeometryFieldName() { return getFieldName(this.geometryFieldDefinitionIndex); } @Override public List<String> getGeometryFieldNames() { return Collections.unmodifiableList(this.geometryFieldDefinitionNames); } @Override public String getIconName() { final FieldDefinition geometryField = getGeometryField(); if (geometryField == null) { return "table"; } else { final DataType dataType = geometryField.getDataType(); if (dataType.equals(DataTypes.GEOMETRY_COLLECTION)) { return "table_geometry"; } else { return "table_" + dataType.toString().toLowerCase(); } } } @Override public FieldDefinition getIdField() { if (this.idFieldDefinitionIndex >= 0) { return this.fields.get(this.idFieldDefinitionIndex); } else { return null; } } @Override public int getIdFieldIndex() { return this.idFieldDefinitionIndex; } @Override public List<Integer> getIdFieldIndexes() { return Collections.unmodifiableList(this.idFieldDefinitionIndexes); } @Override public String getIdFieldName() { return getFieldName(this.idFieldDefinitionIndex); } @Override public List<String> getIdFieldNames() { return Collections.unmodifiableList(this.idFieldDefinitionNames); } @Override public List<FieldDefinition> getIdFields() { return Collections.unmodifiableList(this.idFieldDefinitions); } @Override public int getInstanceId() { return this.instanceId; } @Override public ClockDirection getPolygonRingDirection() { return this.polygonOrientation; } @Override public RecordDefinitionFactory getRecordDefinitionFactory() { if (this.recordDefinitionFactory == null) { final RecordStore recordStore = getRecordStore(); return recordStore; } else { return this.recordDefinitionFactory; } } @SuppressWarnings("unchecked") @Override public <R extends Record> RecordFactory<R> getRecordFactory() { return (RecordFactory<R>)this.recordFactory; } public Map<String, Collection<Object>> getRestrictions() { return this.restrictions; } @Override public boolean hasField(final CharSequence name) { if (name == null) { return false; } else { final String lowerName = name.toString().toLowerCase(); return this.fieldMap.containsKey(lowerName); } } @Override public boolean hasGeometryField() { return this.geometryFieldDefinitionIndex != -1; } @Override public boolean isFieldRequired(final CharSequence name) { final FieldDefinition field = getField(name); if (field == null) { return false; } else { return field.isRequired(); } } @Override public boolean isFieldRequired(final int i) { final FieldDefinition field = getField(i); return field.isRequired(); } @Override public boolean isInstanceOf(final RecordDefinition classDefinition) { if (classDefinition == null) { return false; } if (equals(classDefinition)) { return true; } for (final RecordDefinition superClass : this.superClasses) { if (superClass.isInstanceOf(classDefinition)) { return true; } } return false; } @Override public Record newRecord() { final RecordFactory<Record> recordFactory = this.recordFactory; if (recordFactory == null) { return new ArrayRecord(this); } else { return recordFactory.newRecord(this); } } private void readObject(final ObjectInputStream ois) throws ClassNotFoundException, IOException { ois.defaultReadObject(); RECORD_DEFINITION_CACHE.put(this.instanceId, this); } public RecordDefinitionImpl rename(final String path) { final RecordDefinitionImpl clone = new RecordDefinitionImpl(PathName.newPathName(path), getProperties(), this.fields); clone.setIdFieldIndex(this.idFieldDefinitionIndex); clone.setProperties(getProperties()); return clone; } public void replaceField(final FieldDefinition field, final FieldDefinition newFieldDefinition) { final String name = field.getName(); final String lowerName = name.toLowerCase(); final String newName = newFieldDefinition.getName(); if (this.fields.contains(field) && name.equals(newName)) { final int index = field.getIndex(); this.internalFields.set(index, newFieldDefinition); this.fields = Lists.unmodifiable(this.internalFields); this.fieldMap.put(lowerName, newFieldDefinition); newFieldDefinition.setIndex(index); } else { addField(newFieldDefinition); } } public void setCodeTableByFieldNameMap(final Map<String, CodeTable> codeTableByFieldNameMap) { this.codeTableByFieldNameMap = codeTableByFieldNameMap; } @Override public void setDefaultValues(final Map<String, ? extends Object> defaultValues) { this.defaultValues = Maps.newHash(defaultValues); } public void setDescription(final String description) { this.description = description; } @Override public void setGeometryFactory(final GeometryFactory geometryFactory) { final FieldDefinition geometryFieldDefinition = getGeometryField(); if (geometryFieldDefinition == null) { this.geometryFactory = geometryFactory; } else { geometryFieldDefinition.setProperty(FieldProperties.GEOMETRY_FACTORY, geometryFactory); } } /** * @param geometryFieldDefinitionIndex the geometryFieldDefinitionIndex to set */ public void setGeometryFieldIndex(final int geometryFieldDefinitionIndex) { this.geometryFieldDefinitionIndex = geometryFieldDefinitionIndex; } public void setGeometryFieldName(final String name) { final int id = getFieldIndex(name); setGeometryFieldIndex(id); } /** * @param idFieldDefinitionIndex the idFieldDefinitionIndex to set */ public void setIdFieldIndex(final int idFieldDefinitionIndex) { this.idFieldDefinitionIndex = idFieldDefinitionIndex; this.idFieldDefinitionIndexes.clear(); this.idFieldDefinitionIndexes.add(idFieldDefinitionIndex); this.idFieldDefinitionNames.clear(); this.idFieldDefinitionNames.add(getIdFieldName()); this.idFieldDefinitions.clear(); this.idFieldDefinitions.add(getIdField()); } public void setIdFieldName(final String name) { final int id = getFieldIndex(name); setIdFieldIndex(id); } public void setIdFieldNames(final Collection<String> names) { if (names != null) { if (names.size() == 1) { final String name = CollectionUtil.get(names, 0); setIdFieldName(name); } else { for (final String name : names) { final int index = getFieldIndex(name); if (index == -1) { Logs.error(this, "Cannot set ID " + getPath() + "." + name + " does not exist"); } else { this.idFieldDefinitionIndexes.add(index); this.idFieldDefinitionNames.add(name); this.idFieldDefinitions.add(getField(index)); } } } } } public void setIdFieldNames(final String... names) { setIdFieldNames(Arrays.asList(names)); } public void setPolygonRingDirection(final ClockDirection polygonOrientation) { this.polygonOrientation = polygonOrientation; } @Override public void setProperties(final Map<String, ? extends Object> properties) { if (properties != null) { for (final Entry<String, ? extends Object> entry : properties.entrySet()) { final String key = entry.getKey(); final Object value = entry.getValue(); if (value instanceof ValueRecordDefinitionProperty) { final ValueRecordDefinitionProperty valueProperty = (ValueRecordDefinitionProperty)value; final String propertyName = valueProperty.getPropertyName(); final Object propertyValue = valueProperty.getValue(); setProperty(propertyName, propertyValue); } if (value instanceof RecordDefinitionProperty) { final RecordDefinitionProperty property = (RecordDefinitionProperty)value; final RecordDefinitionProperty clonedProperty = property.clone(); clonedProperty.setRecordDefinition(this); } else { setProperty(key, value); } } } } @Override public void setProperty(final String name, Object value) { if (value instanceof ValueRecordDefinitionProperty) { final ValueRecordDefinitionProperty valueHolder = (ValueRecordDefinitionProperty)value; value = valueHolder.getValue(); } super.setProperty(name, value); } public void setRecordDefinitionFactory(final RecordDefinitionFactory recordDefinitionFactory) { this.recordDefinitionFactory = recordDefinitionFactory; } @Override public MapEx toMap() { final MapEx map = new LinkedHashMapEx(); addTypeToMap(map, "recordDefinition"); final String path = getPath(); map.put("path", path); final ClockDirection polygonOrientation = getPolygonRingDirection(); addToMap(map, "polygonOrientation", polygonOrientation, ClockDirection.NONE); final GeometryFactory geometryFactory = getGeometryFactory(); addToMap(map, "geometryFactory", geometryFactory, null); final List<FieldDefinition> fields = getFields(); addToMap(map, "fields", fields); return map; } }