package com.eas.client.metadata;
import com.eas.script.AlreadyPublishedException;
import com.eas.script.HasPublished;
import com.eas.script.NoPublisherException;
import com.eas.script.Scripts;
import com.eas.util.CollectionEditingSupport;
import java.beans.PropertyChangeSupport;
import java.util.*;
import jdk.nashorn.api.scripting.JSObject;
/**
* This class is intended to hold fields information. Supports clone, copy
* operations. Supports factory method for new fields creation. Supports field
* search, operations with primary and foreign keys.
*/
public class Fields implements HasPublished {
public static class OrmDef {
private final String baseName;// if not null, -> property is a scalar orm expanding
private final String name;
private final String oppositeName;
private final JSObject jsDef;
public OrmDef(String aName, String aOppositeName, JSObject aJsDef) {
this(null, aName, aOppositeName, aJsDef);
}
public OrmDef(String aBaseName, String aName, String aOppositeName, JSObject aDef) {
baseName = aBaseName;
name = aName;
oppositeName = aOppositeName;
jsDef = aDef;
}
public String getBaseName() {
return baseName;
}
public String getOppositeName() {
return oppositeName;
}
public String getName() {
return name;
}
public JSObject getJsDef() {
return jsDef;
}
}
private static final String DEFAULT_PARAM_NAME_PREFIX = "Field";
protected String tableDescription;
protected List<Field> fields = new ArrayList<>();
// Map of field name to it's index (0-based)
protected Map<String, Integer> fieldsHash;
protected PropertyChangeSupport changeSupport = new PropertyChangeSupport(this);
protected CollectionEditingSupport<Fields, Field> collectionSupport = new CollectionEditingSupport<>(this);
protected JSObject instanceConstructor;
protected Map<String, OrmDef> ormScalarDefinitions = new HashMap<>();
protected Map<String, OrmDef> ormCollectionsDefinitions = new HashMap<>();
protected Map<String, Set<OrmDef>> ormScalarExpandings = new HashMap<>();
protected JSObject published;
/**
* The default constructor.
*/
public Fields() {
super();
}
/**
* The copy constructor.
*
* @param aSource Instance of <code>Fields</code> class to be used as the
* information source.
*/
public Fields(Fields aSource) {
this();
if (aSource != null) {
for (int i = 0; i < aSource.getFieldsCount(); i++) {
if (aSource.get(i + 1) != null) {
fields.add(i, aSource.get(i + 1).copy());
} else {
fields.add(i, null);
}
}
assert fields.size() == aSource.getFieldsCount();
setTableDescription(aSource.getTableDescription());
}
}
public JSObject getInstanceConstructor() {
return instanceConstructor;
}
public void setInstanceConstructor(JSObject aValue) {
instanceConstructor = aValue;
}
public void putOrmScalarDefinition(String aName, OrmDef aDefinition) {
if (aName != null && !aName.isEmpty() && aDefinition != null) {
if (!ormScalarDefinitions.containsKey(aName)) {
ormScalarDefinitions.put(aName, aDefinition);
Set<OrmDef> expandings = ormScalarExpandings.get(aDefinition.getBaseName());
if (expandings == null) {
expandings = new HashSet<>();
ormScalarExpandings.put(aDefinition.getBaseName(), expandings);
}
expandings.add(aDefinition);
}
}
}
public Map<String, OrmDef> getOrmScalarDefinitions() {
return Collections.unmodifiableMap(ormScalarDefinitions);
}
public Map<String, Set<OrmDef>> getOrmScalarExpandings() {
return Collections.unmodifiableMap(ormScalarExpandings);
}
public void putOrmCollectionDefinition(String aName, OrmDef aDefinition) {
if (aName != null && !aName.isEmpty() && aDefinition != null) {
if (!ormCollectionsDefinitions.containsKey(aName)) {
ormCollectionsDefinitions.put(aName, aDefinition);
}
}
}
public Map<String, OrmDef> getOrmCollectionsDefinitions() {
return Collections.unmodifiableMap(ormCollectionsDefinitions);
}
public PropertyChangeSupport getChangeSupport() {
return changeSupport;
}
public CollectionEditingSupport<Fields, Field> getCollectionSupport() {
return collectionSupport;
}
/**
* Rebuild hashmap with fields by name mapping.
*/
protected void validateFieldsHash() {
if (fieldsHash == null) {
fieldsHash = new HashMap<>();
for (int i = 0; i < fields.size(); i++) {
String fieldName = fields.get(i).getName();
if (fieldName != null) {
fieldsHash.put(fieldName.toLowerCase(), i);
}
}
}
}
/**
* Invlidates inner fields hash. Fields are hashed by field name. It's not
* recomended to call it directly. It's intended for internal use.
*/
public void invalidateFieldsHash() {
fieldsHash = null;
}
public boolean isEqual(Fields other) {
if (other == null) {
return false;
}
if (tableDescription == null ? other.getTableDescription() != null : !tableDescription.equals(other.getTableDescription())) {
return false;
}
if (fields.size() != other.getFieldsCount()) {
return false;
}
for (int i = 0; i < fields.size(); i++) {
if (!fields.get(i).isEqual(other.get(i + 1))) {
return false;
}
}
return true;
}
/**
* Returns if the <code>Fields</code> contains no fields.
*
* @return <code>True</code> if this instance contains no fields.
*/
public boolean isEmpty() {
return fields.isEmpty();
}
/**
* Returns default name prefix for generating new field names.
*
* @return New (unique) name for this instance of <code>Fields</code>.
* @see #generateNewName()
* @see #generateNewName(String)
*/
public String getDefaultNamePrefix() {
return DEFAULT_PARAM_NAME_PREFIX;
}
/**
* Returns table description if this <code>Fields</code> instance represents
* database table fields set.
*
* @return Table description.
*/
public String getTableDescription() {
return tableDescription;
}
/**
* Sets the table description if this <code>Fields</code> instance
* represents database table fields set.
*
* @param aValue Table description value.
*/
public void setTableDescription(String aValue) {
String oldValue = tableDescription;
tableDescription = aValue;
changeSupport.firePropertyChange("tableDescription", oldValue, aValue);
}
/**
* Generates and returns new(unique) field name for this fields set.
*
* @return New(unique) field name for this fields set.
* @see #getDefaultNamePrefix()
* @see #generateNewName(String)
* @see #isNameAlreadyPresent(String aName, Field aField2Skip)
*/
public String generateNewName() {
return generateNewName(null);
}
/**
* Generates and returns new(unique) field name for this fields set. New
* name will start with <code>aPrefix</code> value.
*
* @param aPrefix A string value new name will start with.
* @return New(unique) field name for this fields set.
* @see #getDefaultNamePrefix()
* @see #generateNewName()
* @see #isNameAlreadyPresent(String aName, Field aField2Skip)
*/
public String generateNewName(String aPrefix) {
if (aPrefix == null || aPrefix.isEmpty()) {
aPrefix = getDefaultNamePrefix();
}
int counter = 0;
String currentName = aPrefix;
while (contains(currentName)) {
currentName += ++counter;
}
return currentName;
}
/**
* Tests whether the name already present in this fields set skipping
* particular field instance.
*
* @param aName Name to test.
* @param aField2Skip <code>Field</code> to skip.
* @return <code>True</code> if the aspecified name already present.
* @see #getDefaultNamePrefix()
* @see #generateNewName()
* @see #generateNewName(String)
*/
public boolean isNameAlreadyPresent(String aName, Field aField2Skip) {
if (aName == null) {
aName = "";
}
for (int i = 0; i < getFieldsCount(); i++) {
Field field = fields.get(i);
if (field == aField2Skip) {
continue;
}
String paramName = field.getName();
if (paramName == null) {
paramName = "";
}
if (aName.toLowerCase().equals(paramName.toLowerCase())) {
return true;
}
}
return false;
}
/**
* Yet another method to retrive fields count. Intended for collections like
* syntax, in script for example.
*
* @return Fields count.
* @see #getFieldsCount()
*/
public int getLength() {
return getFieldsCount();
}
/**
* Returns fields count, contained in this fields set.
*
* @return Fields count, contained in this fields set.
*/
public int getFieldsCount() {
if (fields != null) {
return fields.size();
}
return 0;
}
/**
* Clears all fields from this set.
*/
public void clear() {
if (fields != null) {
Collection<Field> oldCollection = fields.subList(0, fields.size());
fields.clear();
invalidateFieldsHash();
collectionSupport.fireElementsRemoved(oldCollection);
collectionSupport.fireCleared();
}
}
/**
* Returns fields, representing primary keys.
*
* @return A vector comprised of <code>Field</code> instances.
*/
public List<Field> getPrimaryKeys() {
List<Field> pks = new ArrayList<>();
for (int i = 0; i < getFieldsCount(); i++) {
Field lfield = fields.get(i);
if (lfield != null && lfield.isPk()) {
pks.add(lfield);
}
}
return pks;
}
/**
* Returns fields, representing foreign keys.
*
* @return A vector comprised of <code>Field</code> instances.
*/
public List<Field> getForeinKeys() {
List<Field> fks = new ArrayList<>();
for (int i = 0; i < getFieldsCount(); i++) {
Field lfield = fields.get(i);
if (lfield != null && lfield.isFk()) {
fks.add(lfield);
}
}
return fks;
}
/**
* Returns indicies of primary-key fields. Values of indicies are 1-based;
*
* @return A vector comprised of <code>Field</code> indicies.
*/
public List<Integer> getPrimaryKeysIndicies() {
List<Integer> pksIndicies = new ArrayList<>();
for (int i = 0; i < getFieldsCount(); i++) {
Field lfield = fields.get(i);
if (lfield != null && lfield.isPk()) {
pksIndicies.add(i + 1);
}
}
return pksIndicies;
}
/**
* Adds particular <code>Field</code> instance to this fields set.
*
* @param aField A <code>Field</code> to add.
* @return <code>True</code> if adding have succeded.
*/
public boolean add(Field aField) {
if (aField != null && fields != null) {
boolean res = fields.add(aField);
invalidateFieldsHash();
collectionSupport.fireElementAdded(aField);
return res;
}
return false;
}
/**
* Adds particular <code>Field</code> instance to this fields set at the
* specified position.
*
* @param index An index at which aField is to be added, starting on 1.
* @param aField A <code>Field</code> to add.
*/
public void add(int index, Field aField) {
if (aField != null && fields != null) {
fields.add(index - 1, aField);
invalidateFieldsHash();
collectionSupport.fireElementAdded(aField);
}
}
/**
* Removes a particular <code>Field</code> instance from this fields set.
*
* @param aField <code>Field</code> instance to remove.
* @return <code>True</code> if removing succeded.
*/
public boolean remove(Field aField) {
int oldSize = fields.size();
fields.remove(aField);
invalidateFieldsHash();
collectionSupport.fireElementRemoved(aField);
return oldSize > fields.size();
}
/**
* Changes the order of fields.
*
* @param newOrder New order for the fields, from 1 to fields count.
*/
public void reorder(int[] newOrder) {
if (newOrder.length != fields.size()) {
throw new IllegalArgumentException("Order argument size must be equal to fields size."); //NOI18N
}
//Check argument's values correctness
Set<Integer> orderNumbers = new HashSet<>();
for (int i = 0; i < newOrder.length; i++) {
orderNumbers.add(newOrder[i]);
}
for (int i = 1; i <= newOrder.length; i++) {
if (!orderNumbers.remove(i)) {
throw new IllegalArgumentException("Order argument is not correct - some values are missing."); //NOI18N
}
}
if (!orderNumbers.isEmpty()) {
throw new IllegalArgumentException("Order argument is not correct - some values are out of range."); //NOI18N
}
List<Field> oldFields = new ArrayList<>();
oldFields.addAll(fields);
Field[] farr = new Field[newOrder.length];
for (int i = 0; i < oldFields.size(); i++) {
farr[newOrder[i] - 1] = oldFields.get(i);
}
fields = new ArrayList<>();
fields.addAll(Arrays.asList(farr));
invalidateFieldsHash();
collectionSupport.fireReodered();
}
/**
* Returns <code>Field</code> instance at the specified index. Index is 1
* based.
*
* @param index Index of <code>Field</code> instance you are interested in.
* @return <code>Field</code> instance at the specified index. If wrong
* index specified, than null is returned.
*/
public Field get(int index) {
if (fields != null && index > 0 && index <= fields.size()) {
return fields.get(index - 1);
}
return null;
}
/**
* Returns <code>Field</code> instance with the specified name.
*
* @param aFieldName Field name of <code>Field</code> instance you are
* interested in.
* @return <code>Field</code> instance with the specified name. If absent
* name is specified, than null is returned.
*/
public Field get(String aFieldName) {
if (fields != null && aFieldName != null) {
return get(find(aFieldName));
}
return null;
}
/**
* Returns ordinal position of <code>Field</code> instance with the
* specified name.
*
* @param aFieldName Field name of <code>Field</code> instance you are
* interested in.
* @return 1 based ordinal position of <code>Field</code> instance with the
* specified name. If absent name is specified, than 0 is returned.
*/
public int find(String aFieldName) {
if (fields != null && aFieldName != null) {
validateFieldsHash();
assert fieldsHash != null;
Integer fIndex = fieldsHash.get(aFieldName.toLowerCase());
if (fIndex != null && fIndex >= 0 && fIndex < fields.size()) {
return fIndex + 1;
}
}
return 0;
}
/**
* Test if this <code>Fields</code> object contains a field with the
* <code>aFieldName</code> name.
*
* @param aFieldName Name to test.
* @return True if fields contains a field with such name.
*/
public boolean contains(String aFieldName) {
return find(aFieldName) > 0;
}
/**
* Copies this <code>Fields</code> instance, creating a new one.
*
* @return New instance of <code>Fields</code>.
* @see #copy()
*/
@Override
public Fields clone() {
return copy();
}
/**
* Copies this <code>Fields</code> instance, creating a new one.
*
* @return New instance of <code>Fields</code>.
* @see #clone()
*/
public Fields copy() {
return new Fields(this);
}
/**
* Returns the fields vector as an abstract collection.
*
* @return Fields vector as an abstract collection.
*/
public Collection<Field> toCollection() {
return fields;
}
@Override
public JSObject getPublished() {
if (published == null) {
JSObject publisher = Scripts.getSpace().getPublisher(this.getClass().getName());
if (publisher == null || !publisher.isFunction()) {
throw new NoPublisherException();
}
published = (JSObject) publisher.call(null, new Object[]{this});
}
return published;
}
@Override
public void setPublished(JSObject aValue) {
if (published != null) {
throw new AlreadyPublishedException();
}
published = aValue;
}
}