| /* |
| * Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0, |
| * or the Eclipse Distribution License v. 1.0 which is available at |
| * http://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause |
| */ |
| |
| // Contributors: |
| // Oracle - initial API and implementation from Oracle TopLink |
| package org.eclipse.persistence.internal.sessions; |
| |
| import java.io.*; |
| import java.util.*; |
| |
| import org.eclipse.persistence.internal.core.sessions.CoreAbstractRecord; |
| import org.eclipse.persistence.internal.helper.*; |
| import org.eclipse.persistence.exceptions.*; |
| import org.eclipse.persistence.sessions.DataRecord; |
| |
| /** |
| * <p> |
| * <b>Purpose</b>: Define the abstract definition of a record for internal use. |
| * Public API should reference the Map or Record interface. |
| * Subclasses are DatabaseRecord and XMLRecord. |
| * <p> |
| * <b>Responsibilities</b>: <ul> |
| * <li> Implement the Record and Map interfaces. |
| * </ul> |
| * @see DatabaseField |
| */ |
| public abstract class AbstractRecord extends CoreAbstractRecord implements DataRecord, Cloneable, Serializable, Map { |
| |
| /** Use vector to store the fields/values for optimal performance.*/ |
| protected Vector<DatabaseField> fields; |
| |
| /** Use vector to store the fields/values for optimal performance.*/ |
| protected Vector values; |
| |
| /** Optimize field creation for field name lookup. */ |
| protected DatabaseField lookupField; |
| |
| /** PERF: Cache the row size. */ |
| protected int size; |
| |
| /** INTERNAL: indicator showing that no entry exists for a given key. */ |
| public static final AbstractRecord.NoEntry noEntry = new AbstractRecord.NoEntry(); |
| |
| /** INTERNAL: flag for any database field containing a null value */ |
| protected boolean nullValueInFields; |
| |
| /** INTERNAL: SerializedObjectPolicy support */ |
| protected transient Object sopObject; |
| |
| /** |
| * INTERNAL: |
| * NoEntry: This is used to differentiate between the two kinds |
| * of nulls: no entry exists, and the field is actually mapped |
| * to null. |
| */ |
| public static class NoEntry { |
| private NoEntry() { |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * converts JDBC results to collections of rows. |
| */ |
| protected AbstractRecord() { |
| this.fields = new NonSynchronizedVector<>(); |
| this.values = new NonSynchronizedVector(); |
| this.size = 0; |
| this.nullValueInFields = false; |
| } |
| |
| /** |
| * INTERNAL: |
| * converts JDBC results to collections of rows. |
| */ |
| protected AbstractRecord(int initialCapacity) { |
| this.fields = new NonSynchronizedVector<>(initialCapacity); |
| this.values = new NonSynchronizedVector(initialCapacity); |
| this.size = 0; |
| this.nullValueInFields = false; |
| } |
| |
| /** |
| * INTERNAL: |
| * converts JDBC results to collections of rows. |
| */ |
| protected AbstractRecord(Vector fields, Vector values) { |
| this.fields = fields; |
| this.values = values; |
| this.nullValueInFields = false; |
| resetSize(); |
| } |
| |
| /** |
| * INTERNAL: |
| * converts JDBC results to collections of rows. |
| */ |
| protected AbstractRecord(Vector fields, Vector values, int size) { |
| this.fields = fields; |
| this.values = values; |
| this.nullValueInFields = false; |
| this.size = size; |
| } |
| |
| /** |
| * Reset the row size. |
| * This must be reset after any change to the row. |
| */ |
| protected void resetSize() { |
| if (this.fields == null) { |
| this.size = 0; |
| } else { |
| this.size = this.fields.size(); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Add the field-value pair to the row. Will not check, |
| * will simply add to the end of the row |
| */ |
| public void add(DatabaseField key, Object value) { |
| this.fields.add(key); |
| this.values.add(value); |
| this.size++; |
| } |
| |
| /** |
| * PUBLIC: |
| * Clear the contents of the row. |
| */ |
| @Override |
| public void clear() { |
| this.fields = new Vector(); |
| this.values = new Vector(); |
| resetSize(); |
| } |
| |
| /** |
| * INTERNAL: |
| * Clone the row and its values. |
| */ |
| @Override |
| public AbstractRecord clone() { |
| try { |
| AbstractRecord clone = (AbstractRecord)super.clone(); |
| clone.fields = (Vector)this.fields.clone(); |
| clone.values = (Vector)this.values.clone(); |
| return clone; |
| } catch (CloneNotSupportedException exception) { |
| throw new InternalError(); |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * Check if the value is contained in the row. |
| */ |
| public boolean contains(Object value) { |
| return containsValue(value); |
| } |
| |
| /** |
| * PUBLIC: |
| * Check if the field is contained in the row. |
| * Conform to hashtable interface. |
| */ |
| @Override |
| public boolean containsKey(Object key) { |
| if (key instanceof String) { |
| return containsKey((String)key); |
| } |
| if (key instanceof DatabaseField) { |
| return containsKey((DatabaseField)key); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * PUBLIC: |
| * Check if the field is contained in the row. |
| */ |
| public boolean containsKey(String fieldName) { |
| return containsKey(getLookupField(fieldName)); |
| } |
| |
| /** |
| * INTERNAL: |
| * Check if the field is contained in the row. |
| */ |
| public boolean containsKey(DatabaseField key) { |
| // Optimize check. |
| int index = key.index; |
| if ((index >= 0) && (index < this.size)) { |
| DatabaseField field = this.fields.get(index); |
| if ((field == key) || field.equals(key)) { |
| return true; |
| } |
| } |
| return this.fields.contains(key); |
| } |
| |
| /** |
| * PUBLIC: |
| * Check if the value is contained in the row. |
| */ |
| @Override |
| public boolean containsValue(Object value) { |
| return getValues().contains(value); |
| } |
| |
| /** |
| * PUBLIC: |
| * Returns an Enumeration of the values. |
| */ |
| public Enumeration elements() { |
| return getValues().elements(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Returns a set of the keys. |
| */ |
| @Override |
| public Set entrySet() { |
| return new EntrySet(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Retrieve the value for the field name. |
| * A field is constructed on the name to check the hash table. |
| * If missing null is returned. |
| */ |
| @Override |
| public Object get(Object key) { |
| if (key instanceof String) { |
| return get((String)key); |
| } else if (key instanceof DatabaseField) { |
| return get((DatabaseField)key); |
| } |
| return null; |
| } |
| |
| /** |
| * PUBLIC: |
| * Retrieve the value for the field name. |
| * A field is constructed on the name to check the hash table. |
| * If missing null is returned. |
| */ |
| public Object get(String fieldName) { |
| return get(getLookupField(fieldName)); |
| } |
| |
| /** |
| * Internal: factored out of getIndicatingNoEntry(String) to reduce complexity and have |
| * get(string) use get(DatabaseField) instead of getIndicatingNoEntry and then doing an extra check |
| */ |
| protected DatabaseField getLookupField(String fieldName){ |
| if (this.lookupField == null) { |
| this.lookupField = new DatabaseField(fieldName); |
| } else { |
| this.lookupField.resetQualifiedName(fieldName); |
| } |
| return this.lookupField; |
| } |
| |
| /** |
| * PUBLIC: |
| * Retrieve the value for the field name. |
| * A field is constructed on the name to check the hash table. |
| * If missing DatabaseRow.noEntry is returned. |
| */ |
| public Object getIndicatingNoEntry(String fieldName) { |
| // Optimized the field creation. |
| return getIndicatingNoEntry(getLookupField(fieldName)); |
| } |
| |
| /** |
| * INTERNAL: |
| * Retrieve the value for the field. If missing null is returned. |
| */ |
| public Object get(DatabaseField key) { |
| // PERF: Direct variable access. |
| // ** Code duplicated in getIndicatingNoEntry, replaceAt ensure kept in synch ** |
| // Optimize check. |
| int index = key.index; |
| if ((index >= 0) && (index < this.size)) { |
| DatabaseField field = this.fields.get(index); |
| if ((field == key) || field.equals(key)) { |
| return this.values.get(index); |
| } |
| } |
| int fieldsIndex = this.fields.indexOf(key); |
| if (fieldsIndex >= 0) { |
| // PERF: If the fields index was not set, then set it. |
| if (index == -1) { |
| key.setIndex(fieldsIndex); |
| } |
| return this.values.get(fieldsIndex); |
| } else { |
| return null; |
| } |
| } |
| |
| //----------------------------------------------------------------------------// |
| public Object getValues(DatabaseField key) { |
| return get(key); |
| } |
| |
| public Object getValues(String key) { |
| return get(key); |
| } |
| |
| //----------------------------------------------------------------------------// |
| |
| /** |
| * INTERNAL: |
| * Retrieve the value for the field. If missing DatabaseRow.noEntry is returned. |
| */ |
| public Object getIndicatingNoEntry(DatabaseField key) { |
| // PERF: Direct variable access. |
| // ** Code duplicated in get, ensure kept in synch ** |
| // Optimize check. |
| int index = key.index; |
| if ((index >= 0) && (index < this.size)) { |
| DatabaseField field = this.fields.get(index); |
| if ((field == key) || field.equals(key)) { |
| return this.values.get(index); |
| } |
| } |
| int fieldsIndex = this.fields.indexOf(key); |
| if (fieldsIndex >= 0) { |
| // PERF: If the fields index was not set, then set it. |
| if (index == -1) { |
| key.setIndex(fieldsIndex); |
| } |
| return this.values.get(fieldsIndex); |
| } else { |
| return AbstractRecord.noEntry; |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Returns the row's field with the same name. |
| */ |
| public DatabaseField getField(DatabaseField key) { |
| // Optimize check. |
| int index = key.index; |
| if ((index >= 0) && (index < getFields().size())) { |
| DatabaseField field = getFields().elementAt(index); |
| if ((field == key) || field.equals(key)) { |
| return field; |
| } |
| } |
| for (index = 0; index < getFields().size(); index++) { |
| DatabaseField field = getFields().elementAt(index); |
| if ((field == key) || field.equals(key)) { |
| return field; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * INTERNAL: |
| */ |
| public Vector<DatabaseField> getFields() { |
| return fields; |
| } |
| |
| /** |
| * INTERNAL: |
| */ |
| public Vector getValues() { |
| return values; |
| } |
| |
| /** |
| * PUBLIC: |
| * Return if the row is empty. |
| */ |
| @Override |
| public boolean isEmpty() { |
| return size() == 0; |
| } |
| |
| /** |
| * INTERNAL: |
| * Return true if the AbstractRecord has been marked as valid |
| * to check the update call cache with, false otherwise. |
| */ |
| public boolean hasNullValueInFields() { |
| return this.nullValueInFields; |
| } |
| |
| /** |
| * PUBLIC: |
| * Returns an Enumeration of the DatabaseField objects. |
| */ |
| public Enumeration keys() { |
| return getFields().elements(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Returns a set of the keys. |
| */ |
| @Override |
| public Set keySet() { |
| return new KeySet(); |
| } |
| |
| /** |
| * Defines the virtual keySet. |
| */ |
| protected class KeySet extends EntrySet { |
| /** |
| * Default constructor. |
| */ |
| public KeySet() { |
| } |
| |
| @Override |
| public Iterator iterator() { |
| return new RecordKeyIterator(); |
| } |
| @Override |
| public boolean contains(Object object) { |
| return AbstractRecord.this.containsKey(object); |
| } |
| @Override |
| public boolean remove(Object object) { |
| return AbstractRecord.this.remove(object) != null; |
| } |
| } |
| |
| /** |
| * Defines the virtual valuesSet. |
| */ |
| protected class ValuesSet extends EntrySet { |
| /** |
| * Default constructor. |
| */ |
| public ValuesSet() { |
| } |
| |
| @Override |
| public Iterator iterator() { |
| return new RecordValuesIterator(); |
| } |
| @Override |
| public boolean contains(Object object) { |
| return AbstractRecord.this.contains(object); |
| } |
| @Override |
| public boolean remove(Object object) { |
| int index = getValues().indexOf(object); |
| if (index == -1) { |
| return false; |
| } |
| AbstractRecord.this.remove(getFields().get(index)); |
| return true; |
| } |
| } |
| |
| /** |
| * Defines the virtual entrySet. |
| */ |
| protected class EntrySet extends AbstractSet { |
| /** |
| * Default constructor. |
| */ |
| public EntrySet() { |
| super(); |
| } |
| |
| @Override |
| public Iterator iterator() { |
| return new RecordEntryIterator(); |
| } |
| @Override |
| public int size() { |
| return AbstractRecord.this.size(); |
| } |
| @Override |
| public boolean contains(Object object) { |
| if (!(object instanceof Entry)) { |
| return false; |
| } |
| return AbstractRecord.this.containsKey(((Entry)object).getKey()); |
| } |
| @Override |
| public boolean remove(Object object) { |
| if (!(object instanceof Entry)) { |
| return false; |
| } |
| AbstractRecord.this.remove(((Entry)object).getKey()); |
| return true; |
| } |
| @Override |
| public void clear() { |
| AbstractRecord.this.clear(); |
| } |
| } |
| |
| /** |
| * Defines the virtual entrySet iterator. |
| */ |
| protected class RecordEntryIterator implements Iterator { |
| int index; |
| |
| RecordEntryIterator() { |
| this.index = 0; |
| } |
| |
| @Override |
| public boolean hasNext() { |
| return this.index < AbstractRecord.this.size(); |
| } |
| |
| @Override |
| public Object next() { |
| if (!hasNext()) { |
| throw new NoSuchElementException(); |
| } |
| this.index++; |
| return new RecordEntry(getFields().get(this.index - 1), getValues().get(this.index - 1)); |
| } |
| |
| @Override |
| public void remove() { |
| if (this.index >= AbstractRecord.this.size()) { |
| throw new IllegalStateException(); |
| } |
| AbstractRecord.this.remove(getFields().get(this.index)); |
| } |
| } |
| |
| /** |
| * Defines the virtual keySet iterator. |
| */ |
| protected class RecordKeyIterator extends RecordEntryIterator { |
| /** |
| * Default constructor. |
| */ |
| public RecordKeyIterator() { |
| } |
| |
| @Override |
| public Object next() { |
| if (!hasNext()) { |
| throw new NoSuchElementException(); |
| } |
| this.index++; |
| return getFields().get(this.index - 1); |
| } |
| } |
| |
| /** |
| * Defines the virtual valuesSet iterator. |
| */ |
| protected class RecordValuesIterator extends RecordEntryIterator { |
| /** |
| * Default constructor. |
| */ |
| public RecordValuesIterator() { |
| } |
| |
| @Override |
| public Object next() { |
| if (!hasNext()) { |
| throw new NoSuchElementException(); |
| } |
| this.index++; |
| return getValues().get(this.index - 1); |
| } |
| } |
| |
| /** |
| * Entry class for implementing Map interface. |
| */ |
| protected static class RecordEntry implements Entry { |
| Object key; |
| Object value; |
| |
| public RecordEntry(Object key, Object value) { |
| this.key = key; |
| this.value = value; |
| } |
| |
| @Override |
| public Object getKey() { |
| return key; |
| } |
| |
| @Override |
| public Object getValue() { |
| return value; |
| } |
| |
| @Override |
| public Object setValue(Object value) { |
| Object oldValue = this.value; |
| this.value = value; |
| return oldValue; |
| } |
| |
| @Override |
| public boolean equals(Object object) { |
| if (!(object instanceof Map.Entry)) { |
| return false; |
| } |
| Map.Entry entry = (Map.Entry)object; |
| return compare(key, entry.getKey()) && compare(value, entry.getValue()); |
| } |
| |
| @Override |
| public int hashCode() { |
| return ((key == null) ? 0 : key.hashCode()) ^ ((value == null) ? 0 : value.hashCode()); |
| } |
| |
| @Override |
| public String toString() { |
| return key + "=" + value; |
| } |
| |
| private boolean compare(Object object1, Object object2) { |
| return (object1 == null ? object2 == null : object1.equals(object2)); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Merge the provided row into this row. Existing field values in this row will |
| * be replaced with values from the provided row. Fields not in this row will be |
| * added from provided row. Values not in provided row will remain in this row. |
| */ |
| |
| public void mergeFrom(AbstractRecord row){ |
| for (int index = 0; index < row.size(); ++index){ |
| this.put(row.getFields().get(index), row.getValues().get(index)); |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * Add the field-value pair to the row. |
| */ |
| @Override |
| public Object put(Object key, Object value) throws ValidationException { |
| if (key instanceof String) { |
| return put((String)key, value); |
| } else if (key instanceof DatabaseField) { |
| return put((DatabaseField)key, value); |
| } else { |
| throw ValidationException.onlyFieldsAreValidKeysForDatabaseRows(); |
| } |
| } |
| |
| /** |
| * PUBLIC: |
| * Add the field-value pair to the row. |
| */ |
| public Object put(String key, Object value) { |
| return put(new DatabaseField(key), value); |
| } |
| |
| /** |
| * INTERNAL: |
| * Add the field-value pair to the row. |
| */ |
| public Object put(DatabaseField key, Object value) { |
| int index = this.fields.indexOf(key); |
| if (index >= 0) { |
| return this.values.set(index, value); |
| } else { |
| add(key, value); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * PUBLIC: |
| * Add all of the elements. |
| */ |
| @Override |
| public void putAll(Map map) { |
| Iterator entriesIterator = map.entrySet().iterator(); |
| while (entriesIterator.hasNext()) { |
| Map.Entry entry = (Map.Entry)entriesIterator.next(); |
| put(entry.getKey(), entry.getValue()); |
| } |
| } |
| |
| /** |
| * INTERNAL: |
| * Remove the field key from the row. |
| */ |
| @Override |
| public Object remove(Object key) { |
| if (key instanceof String) { |
| return remove((String)key); |
| } else if (key instanceof DatabaseField) { |
| return remove((DatabaseField)key); |
| } |
| return null; |
| } |
| |
| /** |
| * INTERNAL: |
| * Remove the field key from the row. |
| */ |
| public Object remove(String fieldName) { |
| return remove(new DatabaseField(fieldName)); |
| } |
| |
| /** |
| * INTERNAL: |
| * Remove the field key from the row. |
| */ |
| public Object remove(DatabaseField key) { |
| int index = getFields().indexOf(key); |
| if (index >= 0) { |
| getFields().removeElementAt(index); |
| Object value = getValues().elementAt(index); |
| getValues().removeElementAt(index); |
| resetSize(); |
| return value; |
| } |
| return null; |
| } |
| |
| /** |
| * INTERNAL: |
| * replaces the value at index with value |
| */ |
| public void replaceAt(Object value, int index) { |
| this.values.set(index, value); |
| } |
| |
| /** |
| * INTERNAL: |
| * replaces the value at field with value |
| */ |
| public void replaceAt(Object value, DatabaseField key) { |
| int index = key.index; |
| if ((index >= 0) && (index < this.size)) { |
| DatabaseField field = this.fields.get(index); |
| if ((field == key) || field.equals(key)) { |
| this.values.set(index, value); |
| } |
| } |
| int fieldsIndex = this.fields.indexOf(key); |
| if (fieldsIndex >= 0) { |
| // PERF: If the fields index was not set, then set it. |
| if (index == -1) { |
| key.setIndex(fieldsIndex); |
| } |
| this.values.set(fieldsIndex, value); |
| } |
| } |
| |
| protected void setFields(Vector fields) { |
| this.fields = fields; |
| resetSize(); |
| } |
| |
| /** |
| * INTERNAL: |
| * Set the validForUpdateCallCacheCheck attribute to true if the row |
| * does not contain nulls, false otherwise |
| */ |
| public void setNullValueInFields(boolean nullValueInFields) { |
| this.nullValueInFields = nullValueInFields; |
| } |
| |
| protected void setValues(Vector values) { |
| this.values = values; |
| } |
| |
| /** |
| * PUBLIC: |
| * Return the number of field/value pairs in the row. |
| */ |
| @Override |
| public int size() { |
| return this.fields.size(); |
| } |
| |
| /** |
| * INTERNAL: |
| */ |
| @Override |
| public String toString() { |
| StringWriter writer = new StringWriter(); |
| writer.write(Helper.getShortClassName(getClass())); |
| writer.write("("); |
| |
| for (int index = 0; index < getFields().size(); index++) { |
| writer.write(Helper.cr()); |
| writer.write("\t"); |
| writer.write(String.valueOf((getFields().elementAt(index)))); |
| writer.write(" => "); |
| writer.write(String.valueOf((getValues().elementAt(index)))); |
| } |
| if (this.sopObject != null) { |
| writer.write(Helper.cr()); |
| writer.write(" sopObject = "); |
| writer.write(this.sopObject.toString()); |
| } |
| writer.write(")"); |
| |
| return writer.toString(); |
| } |
| |
| /** |
| * PUBLIC: |
| * Returns an collection of the values. |
| */ |
| @Override |
| public Collection values() { |
| return new ValuesSet(); |
| } |
| |
| /** |
| * INTERNAL: |
| */ |
| public boolean hasSopObject() { |
| return this.sopObject != null; |
| } |
| |
| /** |
| * INTERNAL: |
| */ |
| public Object getSopObject() { |
| return this.sopObject; |
| } |
| |
| /** |
| * INTERNAL: |
| */ |
| public void setSopObject(Object sopObject) { |
| this.sopObject = sopObject; |
| } |
| } |