Package com.orientechnologies.orient.core.record.impl

Source Code of com.orientechnologies.orient.core.record.impl.ODocument

/*
* Copyright 1999-2010 Luca Garulli (l.garulli--at--orientechnologies.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.orientechnologies.orient.core.record.impl;

import java.lang.ref.WeakReference;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.core.config.OStorageConfiguration;
import com.orientechnologies.orient.core.db.record.ODatabaseRecord;
import com.orientechnologies.orient.core.db.record.ORecordElement;
import com.orientechnologies.orient.core.db.record.ORecordLazyList;
import com.orientechnologies.orient.core.db.record.ORecordLazyMap;
import com.orientechnologies.orient.core.db.record.ORecordLazyMultiValue;
import com.orientechnologies.orient.core.db.record.ORecordLazySet;
import com.orientechnologies.orient.core.db.record.ORecordTrackedList;
import com.orientechnologies.orient.core.db.record.ORecordTrackedSet;
import com.orientechnologies.orient.core.db.record.OTrackedList;
import com.orientechnologies.orient.core.db.record.OTrackedMap;
import com.orientechnologies.orient.core.db.record.OTrackedSet;
import com.orientechnologies.orient.core.exception.ODatabaseException;
import com.orientechnologies.orient.core.exception.OQueryParsingException;
import com.orientechnologies.orient.core.exception.ORecordNotFoundException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.index.OPropertyIndexManager;
import com.orientechnologies.orient.core.iterator.OEmptyIterator;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OClassImpl;
import com.orientechnologies.orient.core.metadata.schema.OProperty;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.ORecordAbstract;
import com.orientechnologies.orient.core.record.ORecordSchemaAwareAbstract;
import com.orientechnologies.orient.core.serialization.OBase64Utils;
import com.orientechnologies.orient.core.serialization.OBinaryProtocol;
import com.orientechnologies.orient.core.serialization.serializer.record.ORecordSerializerFactory;
import com.orientechnologies.orient.core.serialization.serializer.record.string.ORecordSerializerSchemaAware2CSV;

/**
* Document representation to handle values dynamically. Can be used in schema-less, schema-mixed and schema-full modes. Fields can
* be added at run-time. Instances can be reused across calls by using the reset() before to re-use.
*/
@SuppressWarnings({ "unchecked", "serial" })
public class ODocument extends ORecordSchemaAwareAbstract<Object> implements Iterable<Entry<String, Object>> {
  public static final byte                      RECORD_TYPE        = 'd';
  protected Map<String, Object>                  _fieldValues;
  protected Map<String, Object>                  _fieldOriginalValues;
  protected Map<String, OType>                  _fieldTypes;
  protected boolean                              _trackingChanges  = true;
  protected boolean                              _ordered          = true;
  protected boolean                              _lazyLoad          = true;

  protected List<WeakReference<ORecordElement>>  _owners            = null;

  private static final String[]                  EMPTY_STRINGS      = new String[] {};

  /**
   * Internal constructor used on unmarshalling.
   */
  public ODocument() {
    setup();
  }

  /**
   * Creates a new instance by the raw stream usually read from the database. New instances are not persistent until {@link #save()}
   * is called.
   *
   * @param iSource
   *          Raw stream
   */
  public ODocument(final byte[] iSource) {
    _source = iSource;
    setup();
  }

  /**
   * Creates a new instance and binds to the specified database. New instances are not persistent until {@link #save()} is called.
   *
   * @param iDatabase
   *          Database instance
   */
  public ODocument(final ODatabaseRecord iDatabase) {
    super(iDatabase);
    setup();
  }

  /**
   * Creates a new instance in memory linked by the Record Id to the persistent one. New instances are not persistent until
   * {@link #save()} is called.
   *
   * @param iDatabase
   *          Database instance
   * @param iRID
   *          Record Id
   */
  public ODocument(final ODatabaseRecord iDatabase, final ORID iRID) {
    this(iDatabase);
    _recordId = (ORecordId) iRID;
    _status = STATUS.NOT_LOADED;
    _dirty = false;
  }

  /**
   * Creates a new instance in memory of the specified class, linked by the Record Id to the persistent one. New instances are not
   * persistent until {@link #save()} is called.
   *
   * @param iDatabase
   *          Database instance
   * @param iClassName
   *          Class name
   * @param iRID
   *          Record Id
   */
  public ODocument(final ODatabaseRecord iDatabase, final String iClassName, final ORID iRID) {
    this(iDatabase, iClassName);
    _recordId = (ORecordId) iRID;
    _dirty = false;
    _status = STATUS.NOT_LOADED;
  }

  /**
   * Creates a new instance in memory of the specified class. New instances are not persistent until {@link #save()} is called.
   *
   * @param iDatabase
   *          Database instance
   * @param iClassName
   *          Class name
   */
  public ODocument(final ODatabaseRecord iDatabase, final String iClassName) {
    super(iDatabase);
    setClassName(iClassName);
    setup();
  }

  /**
   * Creates a new instance in memory of the specified schema class. New instances are not persistent until {@link #save()} is
   * called. The database reference is taken by the OClass instance received.
   *
   * @param iClass
   *          OClass instance
   */
  public ODocument(final OClass iClass) {
    super(((OClassImpl) iClass).getDocument().getDatabase());
    setup();
    _clazz = iClass;
  }

  /**
   * Fills a document passing the field array in form of pairs of field name and value.
   *
   * @param iFields
   *          Array of field pairs
   */
  public ODocument(final Object[] iFields) {
    _recordId = new ORecordId();
    if (iFields != null && iFields.length > 0)
      for (int i = 0; i < iFields.length; i += 2) {
        field(iFields[i].toString(), iFields[i + 1]);
      }
  }

  /**
   * Fills a document passing the field names/values pair, where the first pair is mandatory.
   *
   */
  public ODocument(final String iFieldName, final Object iFieldValue, final Object... iFields) {
    this(iFields);
    field(iFieldName, iFieldValue);
  }

  /**
   * Copies the current instance to a new one. Hasn't been choose the clone() to let ODocument return type. Once copied the new
   * instance has the same identity and values but all the internal structure are totally independent by the source.
   */
  public ODocument copy() {
    final ODocument cloned = (ODocument) copyTo(new ODocument());
    cloned._ordered = _ordered;
    cloned._clazz = _clazz;
    cloned._trackingChanges = _trackingChanges;

    if (_fieldValues != null) {
      cloned._fieldValues = _fieldValues instanceof LinkedHashMap ? new LinkedHashMap<String, Object>()
          : new HashMap<String, Object>();
      for (Entry<String, Object> entry : _fieldValues.entrySet())
        copyFieldValue(cloned, entry);
    }

    if (_fieldTypes != null)
      cloned._fieldTypes = new HashMap<String, OType>(_fieldTypes);

    cloned._fieldOriginalValues = null;

    return cloned;
  }

  @Override
  public ODocument flatCopy() {
    if (isDirty())
      throw new IllegalStateException("Can't execute a flat copy of a dirty record");

    final ODocument cloned = new ODocument();
    cloned.fill(_database, _recordId, _version, _source, false);
    return cloned;
  }

  /**
   * Returns an empty record as place-holder of the current. Used when a record is requested, but only the identity is needed.
   *
   * @return
   */
  public ORecord<?> placeholder() {
    final ODocument cloned = new ODocument();
    cloned._source = null;
    cloned._database = _database;
    cloned._recordId = _recordId.copy();
    cloned._status = STATUS.NOT_LOADED;
    return cloned;
  }

  public boolean detach() {
    _database = null;
    boolean fullyDetached = true;

    if (_fieldValues != null) {
      Object fieldValue;
      for (Map.Entry<String, Object> entry : _fieldValues.entrySet()) {
        fieldValue = entry.getValue();

        if (fieldValue instanceof ORecord<?>)
          if (((ORecord<?>) fieldValue).getIdentity().isNew())
            fullyDetached = false;
          else
            _fieldValues.put(entry.getKey(), ((ORecord<?>) fieldValue).getIdentity());
        else if (fieldValue instanceof ORecordLazyMultiValue) {
          if (!((ORecordLazyMultiValue) fieldValue).convertRecords2Links())
            fullyDetached = false;
        }
      }
    }

    return fullyDetached;
  }

  /**
   * Loads the record using a fetch plan. Example:
   * <p>
   * <code>doc.load( "*:3" ); // LOAD THE DOCUMENT BY EARLY FETCHING UP TO 3rd LEVEL OF CONNECTIONS</code>
   * </p>
   *
   * @param iFetchPlan
   *          Fetch plan to use
   */
  public ODocument load(final String iFetchPlan) {
    return load(iFetchPlan, false);
  }

  /**
   * Loads the record using a fetch plan. Example:
   * <p>
   * <code>doc.load( "*:3", true ); // LOAD THE DOCUMENT BY EARLY FETCHING UP TO 3rd LEVEL OF CONNECTIONS IGNORING THE CACHE</code>
   * </p>
   *
   * @param iIgnoreCache
   *          Ignore the cache or use it
   */
  public ODocument load(final String iFetchPlan, boolean iIgnoreCache) {
    if (_database == null)
      throw new ODatabaseException("No database assigned to current record");

    Object result = null;
    try {
      result = _database.load(this, iFetchPlan, iIgnoreCache);
    } catch (Exception e) {
      throw new ORecordNotFoundException("The record with id '" + getIdentity() + "' was not found", e);
    }

    if (result == null)
      throw new ORecordNotFoundException("The record with id '" + getIdentity() + "' was not found");

    return (ODocument) result;
  }

  /**
   * Makes a deep comparison field by field to check if the passed ODocument instance is identical in the content to the current
   * one. Instead equals() just checks if the RID are the same.
   *
   * @param iOther
   *          ODocument instance
   * @return true if the two document are identical, otherwise false
   * @see #equals(Object);
   */
  public boolean hasSameContentOf(ODocument iOther) {
    if (iOther == null)
      return false;

    if (!equals(iOther) && _recordId.isValid())
      return false;

    if (_status == STATUS.NOT_LOADED)
      reload();
    if (iOther._status == STATUS.NOT_LOADED)
      iOther = (ODocument) iOther.load();

    checkForFields();

    iOther.checkForFields();

    if (_fieldValues.size() != iOther._fieldValues.size())
      return false;

    // CHECK FIELD-BY-FIELD
    Object myFieldValue;
    Object otherFieldValue;
    for (Entry<String, Object> f : _fieldValues.entrySet()) {
      myFieldValue = f.getValue();
      otherFieldValue = iOther._fieldValues.get(f.getKey());

      // CHECK FOR NULLS
      if (myFieldValue == null) {
        if (otherFieldValue != null)
          return false;
      } else if (otherFieldValue == null)
        return false;

      if (myFieldValue != null && otherFieldValue != null)
        if (myFieldValue instanceof Collection && otherFieldValue instanceof Collection) {
          final Collection<?> myCollection = (Collection<?>) myFieldValue;
          final Collection<?> otherCollection = (Collection<?>) otherFieldValue;

          if (myCollection.size() != otherCollection.size())
            return false;

          Iterator<?> myIterator = myCollection.iterator();
          Iterator<?> otherIterator = otherCollection.iterator();

          while (myIterator.hasNext()) {
            hasSameContentItem(myIterator.next(), otherIterator.next());
          }
        } else if (myFieldValue instanceof Map && otherFieldValue instanceof Map) {
          // CHECK IF THE ORDER IS RESPECTED
          final Map<?, ?> myMap = (Map<?, ?>) myFieldValue;
          final Map<?, ?> otherMap = (Map<?, ?>) otherFieldValue;

          if (myMap.size() != otherMap.size())
            return false;

          for (Entry<?, ?> myEntry : myMap.entrySet()) {
            if (!otherMap.containsKey(myEntry.getKey()))
              return false;

            if (myEntry.getValue() instanceof ODocument) {
              if (!((ODocument) myEntry.getValue()).hasSameContentOf((ODocument) otherMap.get(myEntry.getKey())))
                return false;
            } else if (!myEntry.getValue().equals(otherMap.get(myEntry.getKey())))
              return false;
          }
        } else {
          if (!myFieldValue.equals(otherFieldValue))
            return false;
        }
    }

    return true;
  }

  private boolean hasSameContentItem(final Object my, final Object other) {
    if (my instanceof ODocument) {
      if (other instanceof ORID) {
        if (!((ODocument) my).isDirty()) {
          if (!((ODocument) my).getIdentity().equals(other))
            return false;
        } else {
          ODocument otherDoc = (ODocument) getDatabase().load((ORID) other);
          if (!((ODocument) my).hasSameContentOf(otherDoc))
            return false;
        }
      } else if (!((ODocument) my).hasSameContentOf((ODocument) other))
        return false;
    } else if (!my.equals(other))
      return false;
    return true;
  }

  /**
   * Dumps the instance as string.
   */
  @Override
  public String toString() {
    final boolean saveDirtyStatus = _dirty;

    final StringBuilder buffer = new StringBuilder();

    try {
      checkForFields();
      if (_clazz != null)
        buffer.append(_clazz.getStreamableName());

      if (_recordId != null) {
        if (_recordId.isValid())
          buffer.append(_recordId);
      }

      boolean first = true;
      ORecord<?> record;
      for (Entry<String, Object> f : _fieldValues.entrySet()) {
        buffer.append(first ? '{' : ',');
        buffer.append(f.getKey());
        buffer.append(':');
        if (f.getValue() instanceof Collection<?>) {
          buffer.append('[');
          buffer.append(((Collection<?>) f.getValue()).size());
          buffer.append(']');
        } else if (f.getValue() instanceof ORecord<?>) {
          record = (ORecord<?>) f.getValue();

          if (record.getIdentity() != null)
            record.getIdentity().toString(buffer);
          else
            buffer.append(record.toString());
        } else
          buffer.append(f.getValue());

        if (first)
          first = false;
      }
      if (!first)
        buffer.append('}');

      buffer.append(" v");
      buffer.append(_version);

    } finally {
      _dirty = saveDirtyStatus;
    }

    return buffer.toString();
  }

  /**
   * Fills the ODocument directly with the string representation of the document itself. Use it for faster insertion but pay
   * attention to respect the OrientDB record format.
   * <p>
   * <code>
   * record.reset();<br/>
   * record.setClassName("Account");<br/>
   * record.fromString(new String("Account@id:" + data.getCyclesDone() + ",name:'Luca',surname:'Garulli',birthDate:" + date.getTime()<br/>
   *     + ",salary:" + 3000f + i));<br/>
   * record.save();<br/>
</code>
   * </p>
   *
   * @param iValue
   */
  public void fromString(final String iValue) {
    _dirty = true;
    _source = OBinaryProtocol.string2bytes(iValue);
    _fieldOriginalValues = null;
    _fieldTypes = null;
    _fieldValues = null;
  }

  /**
   * Returns the set of field names.
   */
  public String[] fieldNames() {
    checkForLoading();
    checkForFields();

    if (_fieldValues == null || _fieldValues.size() == 0)
      return EMPTY_STRINGS;

    return _fieldValues.keySet().toArray(new String[_fieldValues.keySet().size()]);
  }

  /**
   * Returns the array of field values.
   */
  public Object[] fieldValues() {
    checkForLoading();
    checkForFields();

    return _fieldValues.values().toArray(new Object[_fieldValues.values().size()]);
  }

  public <RET> RET rawField(final String iFieldName) {
    checkForLoading();
    checkForFields();

    final int separatorPos = iFieldName.indexOf('.');
    if (separatorPos > -1) {
      // GET THE LINKED OBJECT IF ANY
      final String fieldName = iFieldName.substring(0, separatorPos);
      final Object linkedObject = _fieldValues.get(fieldName);

      if (linkedObject == null || !(linkedObject instanceof ODocument))
        // IGNORE IT BY RETURNING NULL
        return null;

      final ODocument linkedRecord = (ODocument) linkedObject;
      if (linkedRecord.getInternalStatus() == STATUS.NOT_LOADED)
        // LAZY LOAD IT
        linkedRecord.reload();

      // CALL MYSELF RECURSIVELY BY CROSSING ALL THE OBJECTS
      return (RET) linkedRecord.field(iFieldName.substring(separatorPos + 1));
    }

    return (RET) _fieldValues.get(iFieldName);
  }

  /**
   * Reads the field value.
   *
   * @param iFieldName
   *          field name
   * @return field value if defined, otherwise null
   */
  public <RET> RET field(final String iFieldName) {
    RET value = this.<RET> rawField(iFieldName);

    final OType t = fieldType(iFieldName);

    if (_lazyLoad && value instanceof ORID && t != OType.LINK && _database != null) {
      // CREATE THE DOCUMENT OBJECT IN LAZY WAY
      value = (RET) _database.load((ORID) value);
      _fieldValues.put(iFieldName, value);
    }

    // CHECK FOR CONVERSION
    if (t != null) {
      Object newValue = null;

      if (t == OType.BINARY && value instanceof String)
        newValue = OBase64Utils.decode((String) value);
      else if ((t == OType.DATE || t == OType.DATE) && value instanceof Long)
        newValue = (RET) new Date(((Long) value).longValue());

      if (newValue != null) {
        // VALUE CHANGED: SET THE NEW ONE
        _fieldValues.put(iFieldName, newValue);
        value = (RET) newValue;
      }
    }

    return value;
  }

  /**
   * Reads the field value forcing the return type. Use this method to force return of ORID instead of the entire document by
   * passing ORID.class as iFieldType.
   *
   * @param iFieldName
   *          field name
   * @param iFieldType
   *          Forced type.
   * @return field value if defined, otherwise null
   */
  public <RET> RET field(final String iFieldName, final Class<?> iFieldType) {
    RET value = this.<RET> rawField(iFieldName);

    if (value != null)
      value = (RET) convertField(iFieldName, iFieldType, value);

    return value;
  }

  /**
   * Reads the field value forcing the return type. Use this method to force return of binary data.
   *
   * @param iFieldName
   *          field name
   * @param iFieldType
   *          Forced type.
   * @return field value if defined, otherwise null
   */
  public <RET> RET field(final String iFieldName, final OType iFieldType) {
    setFieldType(iFieldName, iFieldType);
    return (RET) field(iFieldName);
  }

  /**
   * Writes the field value.
   *
   * @param iFieldName
   *          field name
   * @param iPropertyValue
   *          field value
   * @return The Record instance itself giving a "fluent interface". Useful to call multiple methods in chain.
   */
  public ODocument field(final String iFieldName, Object iPropertyValue) {
    return field(iFieldName, iPropertyValue, null);
  }

  /**
   * Fill a document passing the field names/values
   *
   */
  public ODocument fields(final String iFieldName, final Object iFieldValue, final Object... iFields) {
    field(iFieldName, iFieldValue);
    if (iFields != null && iFields.length > 0)
      for (int i = 0; i < iFields.length; i += 2) {
        field(iFields[i].toString(), iFields[i + 1]);
      }
    return this;
  }

  /**
   * Writes the field value forcing the type.
   *
   * @param iFieldName
   *          field name
   * @param iPropertyValue
   *          field value
   * @param iFieldType
   *          Forced type (not auto-determined)
   * @return The Record instance itself giving a "fluent interface". Useful to call multiple methods in chain.
   */
  public ODocument field(String iFieldName, Object iPropertyValue, OType iFieldType) {
    iFieldName = checkFieldName(iFieldName);

    checkForLoading();
    checkForFields();

    _source = null;

    final boolean knownProperty = _fieldValues.containsKey(iFieldName);
    final Object oldValue = _fieldValues.get(iFieldName);

    if (knownProperty)
      // CHECK IF IS REALLY CHANGED
      if (iPropertyValue == null) {
        if (oldValue == null)
          // BOTH NULL: UNCHANGED
          return this;
      } else {
        try {
          if (iPropertyValue == oldValue) {
            if (!(iPropertyValue instanceof ORecordElement))
              // SAME BUT NOT TRACKABLE: SET THE RECORD AS DIRTY TO BE SURE IT'S SAVED
              setDirty();

            // SAVE VALUE: UNCHANGED
            return this;
          }
        } catch (Exception e) {
          OLogManager.instance().warn(this, "Error on checking the value of property %s against the record %s", e, iFieldName,
              getIdentity());
        }
      }

    if (iFieldType != null)
      setFieldType(iFieldName, iFieldType);
    else if (_clazz != null) {
      // SCHEMAFULL?
      final OProperty prop = _clazz.getProperty(iFieldName);
      if (prop != null)
        iFieldType = prop.getType();
    }

    if (iPropertyValue == null) {
      _fieldValues.put(iFieldName, iPropertyValue);
    } else {
      if (iFieldType != null) {
        iPropertyValue = convertField(iFieldName, iFieldType.getDefaultJavaType(), iPropertyValue);
      } else if (iPropertyValue instanceof Enum) {
        iPropertyValue = iPropertyValue.toString();
      }

      if (_status != STATUS.UNMARSHALLING) {
        setDirty();

        if (_trackingChanges && _recordId.isValid()) {
          // SAVE THE OLD VALUE IN A SEPARATE MAP ONLY IF TRACKING IS ACTIVE AND THE RECORD IS NOT NEW
          if (_fieldOriginalValues == null)
            _fieldOriginalValues = new HashMap<String, Object>();

          // INSERT IT ONLY IF NOT EXISTS TO AVOID LOOSE OF THE ORIGINAL VALUE (FUNDAMENTAL FOR INDEX HOOK)
          if (!_fieldOriginalValues.containsKey(iFieldName))
            _fieldOriginalValues.put(iFieldName, oldValue);
        }
      }

      _fieldValues.put(iFieldName, iPropertyValue);
    }

    return this;
  }

  /**
   * Removes a field.
   */
  public Object removeField(final String iFieldName) {
    checkForLoading();
    checkForFields();

    final boolean knownProperty = _fieldValues.containsKey(iFieldName);
    final Object oldValue = _fieldValues.get(iFieldName);

    if (knownProperty && _trackingChanges) {
      // SAVE THE OLD VALUE IN A SEPARATE MAP
      if (_fieldOriginalValues == null)
        _fieldOriginalValues = new HashMap<String, Object>();

      // INSERT IT ONLY IF NOT EXISTS TO AVOID LOOSE OF THE ORIGINAL VALUE (FUNDAMENTAL FOR INDEX HOOK)
      if (!_fieldOriginalValues.containsKey(iFieldName))
        _fieldOriginalValues.put(iFieldName, oldValue);
    }

    _fieldValues.remove(iFieldName);
    _source = null;

    setDirty();
    return oldValue;
  }

  /**
   * Merge current document with the document passed as parameter. If the field already exists then the conflicts are managed based
   * on the value of the parameter 'iConflictsOtherWins'.
   *
   * @param iOther
   *          Other ODocument instance to merge
   * @param iConflictsOtherWins
   *          if true, the other document wins in case of conflicts, otherwise the current document wins
   * @param iMergeSingleItemsOfMultiValueFields
   * @return
   */
  public ODocument merge(final ODocument iOther, boolean iConflictsOtherWins, boolean iMergeSingleItemsOfMultiValueFields) {
    iOther.checkForLoading();
    iOther.checkForFields();

    if (_clazz == null && iOther.getSchemaClass() != null)
      _clazz = iOther.getSchemaClass();

    return merge(iOther._fieldValues, iConflictsOtherWins, iMergeSingleItemsOfMultiValueFields);
  }

  /**
   * Merge current document with the document passed as parameter. If the field already exists then the conflicts are managed based
   * on the value of the parameter 'iConflictsOtherWins'.
   *
   * @param iOther
   *          Other ODocument instance to merge
   * @param iAddOnlyMode
   *          if true, the other document properties will be always added. If false, the missed propertie in the "other" document
   *          will be removed by original too
   * @param iMergeSingleItemsOfMultiValueFields
   * @return
   */
  public ODocument merge(final Map<String, Object> iOther, final boolean iAddOnlyMode, boolean iMergeSingleItemsOfMultiValueFields) {
    checkForLoading();
    checkForFields();

    _source = null;

    for (String f : iOther.keySet()) {
      if (containsField(f) && iMergeSingleItemsOfMultiValueFields) {
        Object field = field(f);
        if (field instanceof Map<?, ?>) {
          final Map<String, Object> map = (Map<String, Object>) field;
          final Map<String, Object> otherMap = (Map<String, Object>) iOther.get(f);

          for (Entry<String, Object> entry : otherMap.entrySet()) {
            map.put(entry.getKey(), entry.getValue());
          }
          continue;
        } else if (field instanceof Collection<?>) {
          final Collection<Object> coll = (Collection<Object>) field;
          final Collection<Object> otherColl = (Collection<Object>) iOther.get(f);

          for (Object item : otherColl) {
            if (!coll.contains(item))
              coll.add(item);
          }

          // JUMP RAW REPLACE
          continue;
        }
      }

      // RAW SET/REPLACE
      field(f, iOther.get(f));
    }

    if (!iAddOnlyMode) {
      // REMOVE PROPERTIES NOT FOUND IN OTHER DOC
      for (String f : fieldNames())
        if (!iOther.containsKey(f))
          removeField(f);
    }

    return this;
  }

  /**
   * Returns the original value of a field before it has been changed.
   *
   * @param iFieldName
   *          Property name to retrieve the original value
   */
  public String[] getDirtyFields() {
    if (_fieldOriginalValues == null || _fieldOriginalValues.size() == 0)
      return EMPTY_STRINGS;

    return _fieldOriginalValues.keySet().toArray(new String[_fieldOriginalValues.keySet().size()]);
  }

  /**
   * Returns the original value of a field before it has been changed.
   *
   * @param iFieldName
   *          Property name to retrieve the original value
   */
  public Object getOriginalValue(final String iFieldName) {
    return _fieldOriginalValues != null ? _fieldOriginalValues.get(iFieldName) : null;
  }

  /**
   * Returns the iterator against only the changed fields if tracking was enabled
   */
  public Iterator<Entry<String, Object>> iterator() {
    checkForLoading();
    checkForFields();

    if (_fieldValues == null)
      return OEmptyIterator.INSTANCE;

    return _fieldValues.entrySet().iterator();
  }

  @Override
  public boolean setDatabase(final ODatabaseRecord iDatabase) {
    if (super.setDatabase(iDatabase)) {
      if (_fieldValues != null)
        for (Object f : _fieldValues.values()) {
          if (f instanceof ORecordElement)
            ((ORecordElement) f).setDatabase(iDatabase);
        }
      return true;
    }
    return false;
  }

  /**
   * Checks if a field exists.
   *
   * @return True if exists, otherwise false.
   */
  public boolean containsField(final String iFieldName) {
    if (iFieldName == null)
      return false;

    checkForLoading();
    checkForFields();
    return _fieldValues.containsKey(iFieldName);
  }

  /**
   * Internal.
   */
  public byte getRecordType() {
    return RECORD_TYPE;
  }

  /**
   * Returns true if the record has some owner.
   */
  public boolean hasOwners() {
    return _owners != null && !_owners.isEmpty();
  }

  /**
   * Internal.
   *
   * @return
   */
  public ODocument addOwner(final ORecordElement iOwner) {
    if (_owners == null)
      _owners = new ArrayList<WeakReference<ORecordElement>>();
    this._owners.add(new WeakReference<ORecordElement>(iOwner));
    return this;
  }

  public ODocument removeOwner(final ORecordElement iRecordElement) {
    if (_owners != null) {
      // PROPAGATES TO THE OWNER
      ORecordElement e;
      for (int i = 0; i < _owners.size(); ++i) {
        e = _owners.get(i).get();
        if (e == iRecordElement) {
          _owners.remove(i);
          break;
        }
      }
    }
    return this;
  }

  /**
   * Propagates the dirty status to the owner, if any. This happens when the object is embedded in another one.
   */
  @Override
  public ORecordAbstract<Object> setDirty() {
    if (_owners != null) {
      // PROPAGATES TO THE OWNER
      ORecordElement e;
      for (WeakReference<ORecordElement> o : _owners) {
        e = o.get();
        if (e != null)
          e.setDirty();
      }
    }
    // THIS IS IMPORTANT TO BE SURE THAT FIELDS ARE LOADED BEFORE IT'S TOO LATE AND THE RECORD _SOURCE IS NULL
    checkForFields();

    return super.setDirty();
  }

  @Override
  public void onBeforeIdentityChanged(final ORID iRID) {
    if (_owners != null) {
      final List<WeakReference<ORecordElement>> temp = new ArrayList<WeakReference<ORecordElement>>(_owners);

      ORecordElement e;
      for (WeakReference<ORecordElement> o : temp) {
        e = o.get();
        if (e != null)
          e.onBeforeIdentityChanged(iRID);
      }
    }
  }

  @Override
  public void onAfterIdentityChanged(final ORecord<?> iRecord) {
    if (_owners != null) {
      final List<WeakReference<ORecordElement>> temp = new ArrayList<WeakReference<ORecordElement>>(_owners);

      ORecordElement e;
      for (WeakReference<ORecordElement> o : temp) {
        e = o.get();
        if (e != null)
          e.onAfterIdentityChanged(iRecord);
      }
    }
  }

  @Override
  public ODocument fromStream(byte[] iRecordBuffer) {
    _fieldValues = null;
    _fieldTypes = null;
    _fieldOriginalValues = null;
    return (ODocument) super.fromStream(iRecordBuffer);
  }

  @Override
  public void unsetDirty() {
    _fieldOriginalValues = null;
    super.unsetDirty();
  }

  /**
   * Returns the forced field type if any.
   *
   * @param iFieldName
   */
  public OType fieldType(final String iFieldName) {
    return _fieldTypes != null ? _fieldTypes.get(iFieldName) : null;
  }

  @Override
  public ODocument unload() {
    super.unload();
    if (_fieldValues != null)
      _fieldValues.clear();
    return this;
  }

  /**
   * Clear all the field values and types.
   */
  @Override
  public ODocument clear() {
    super.clear();
    if (_fieldValues != null)
      _fieldValues.clear();
    return this;
  }

  /**
   * Reset the record values and class type to being reused.
   */
  @Override
  public ODocument reset() {
    super.reset();
    if (_fieldValues != null)
      _fieldValues.clear();
    return this;
  }

  public boolean isLazyLoad() {
    return _lazyLoad;
  }

  public void setLazyLoad(final boolean iLazyLoad) {
    this._lazyLoad = iLazyLoad;
  }

  public boolean isTrackingChanges() {
    return _trackingChanges;
  }

  /**
   * Enabled or disabled the tracking of changes in the document. This is needed by some triggers like {@link OPropertyIndexManager}
   * to determine what fields are changed to update indexes.
   *
   * @param iTrackingChanges
   *          True to enable it, otherwise false
   * @return
   */
  public ODocument setTrackingChanges(final boolean iTrackingChanges) {
    this._trackingChanges = iTrackingChanges;
    if (!iTrackingChanges)
      // FREE RESOURCES
      this._fieldOriginalValues = null;
    return this;
  }

  public boolean isOrdered() {
    return _ordered;
  }

  public ODocument setOrdered(final boolean iOrdered) {
    this._ordered = iOrdered;
    return this;
  }

  @Override
  public boolean equals(Object obj) {
    if (!super.equals(obj))
      return false;

    return this == obj || _recordId.isValid();
  }

  /**
   * Returns the number of fields in memory.
   */
  public int fields() {
    return _fieldValues == null ? 0 : _fieldValues.size();
  }

  public boolean isEmpty() {
    return _fieldValues == null || _fieldValues.isEmpty();
  }

  @Override
  protected void checkForFields() {
    if (_fieldValues == null)
      _fieldValues = _ordered ? new LinkedHashMap<String, Object>() : new HashMap<String, Object>();

    if (_status == ORecordElement.STATUS.LOADED && fields() == 0)
      // POPULATE FIELDS LAZY
      deserializeFields();
  }

  /**
   * Internal.
   */
  @Override
  protected void setup() {
    super.setup();
    _recordFormat = ORecordSerializerFactory.instance().getFormat(ORecordSerializerSchemaAware2CSV.NAME);
  }

  private <RET> RET convertField(final String iFieldName, final Class<?> iFieldType, Object iValue) {
    if (iFieldType == null)
      return (RET) iValue;

    if (ORID.class.isAssignableFrom(iFieldType)) {
      if (iValue instanceof ORID) {
        return (RET) iValue;
      } else if (iValue instanceof String) {
        return (RET) new ORecordId((String) iValue);
      } else if (iValue instanceof ORecord<?>) {
        return (RET) ((ORecord<?>) iValue).getIdentity();
      }
    } else if (ORecord.class.isAssignableFrom(iFieldType)) {
      if (iValue instanceof ORID || iValue instanceof ORecord<?>) {
        return (RET) iValue;
      } else if (iValue instanceof String) {
        return (RET) new ORecordId((String) iValue);
      }
    } else if (Set.class.isAssignableFrom(iFieldType)) {
      if (!(iValue instanceof Set)) {
        // CONVERT IT TO SET
        final Collection<?> newValue;

        if (iValue instanceof ORecordLazyList || iValue instanceof ORecordLazyMap)
          newValue = new ORecordLazySet(this);
        else
          newValue = new OTrackedSet<Object>(this);

        if (iValue instanceof Collection<?>) {
          ((Collection<Object>) newValue).addAll((Collection<Object>) iValue);
          return (RET) newValue;
        } else if (iValue instanceof Map) {
          ((Collection<Object>) newValue).addAll(((Map<String, Object>) iValue).values());
          return (RET) newValue;
        } else if (iValue instanceof String) {
          final String stringValue = (String) iValue;

          if (stringValue != null && stringValue.length() > 0) {
            final String[] items = stringValue.split(",");
            for (String s : items) {
              ((Collection<Object>) newValue).add(s);
            }
          }
          return (RET) newValue;
        }
      } else {
        return (RET) iValue;
      }
    } else if (List.class.isAssignableFrom(iFieldType)) {
      if (!(iValue instanceof List)) {
        // CONVERT IT TO LIST
        final Collection<?> newValue;

        if (iValue instanceof ORecordLazySet || iValue instanceof ORecordLazyMap)
          newValue = new ORecordLazyList(this);
        else
          newValue = new OTrackedList<Object>(this);

        if (iValue instanceof Collection) {
          ((Collection<Object>) newValue).addAll((Collection<Object>) iValue);
          return (RET) newValue;
        } else if (iValue instanceof Map) {
          ((Collection<Object>) newValue).addAll(((Map<String, Object>) iValue).values());
          return (RET) newValue;
        } else if (iValue instanceof String) {
          final String stringValue = (String) iValue;

          if (stringValue != null && stringValue.length() > 0) {
            final String[] items = stringValue.split(",");
            for (String s : items) {
              ((Collection<Object>) newValue).add(s);
            }
          }
          return (RET) newValue;
        }
      } else {
        return (RET) iValue;
      }
    } else if (iValue instanceof Enum) {
      // ENUM
      if (Number.class.isAssignableFrom(iFieldType))
        iValue = ((Enum<?>) iValue).ordinal();
      else
        iValue = iValue.toString();
      if (!(iValue instanceof String) && !iFieldType.isAssignableFrom(iValue.getClass()))
        throw new IllegalArgumentException("Property '" + iFieldName + "' of type '" + iFieldType
            + "' can't accept value of type: " + iValue.getClass());
    } else if (Date.class.isAssignableFrom(iFieldType)) {
      if (iValue instanceof String && _database != null) {
        final OStorageConfiguration config = _database.getStorage().getConfiguration();

        DateFormat formatter = config.getDateFormatInstance();

        if (((String) iValue).length() > config.dateFormat.length()) {
          // ASSUMES YOU'RE USING THE DATE-TIME FORMATTE
          formatter = config.getDateTimeFormatInstance();
        }

        try {
          Date newValue = formatter.parse((String) iValue);
          // _fieldValues.put(iFieldName, newValue);
          return (RET) newValue;
        } catch (ParseException pe) {
          final String dateFormat = ((String) iValue).length() > config.dateFormat.length() ? config.dateTimeFormat
              : config.dateFormat;
          throw new OQueryParsingException("Error on conversion of date '" + iValue + "' using the format: " + dateFormat);
        }
      }
    }

    iValue = OType.convert(iValue, iFieldType);

    return (RET) iValue;
  }

  /**
   * Sets the field type. This overrides the schema property settings if any.
   *
   * @param iFieldName
   *          Field name
   * @param iFieldType
   *          Type to set between OType enumaration values
   */
  public ODocument setFieldType(final String iFieldName, final OType iFieldType) {
    if (iFieldType != null) {
      // SET THE FORCED TYPE
      if (_fieldTypes == null)
        _fieldTypes = new HashMap<String, OType>();
      _fieldTypes.put(iFieldName, iFieldType);
    } else if (_fieldTypes != null) {
      // REMOVE THE FIELD TYPE
      _fieldTypes.remove(iFieldName);
      if (_fieldTypes.size() == 0)
        // EMPTY: OPTIMIZE IT BY REMOVING THE ENTIRE MAP
        _fieldTypes = null;
    }
    return this;
  }

  private void copyFieldValue(final ODocument iCloned, final Entry<String, Object> iEntry) {
    final Object fieldValue = iEntry.getValue();

    if (fieldValue != null)
      // LISTS
      if (fieldValue instanceof ORecordLazyList) {
        iCloned._fieldValues.put(iEntry.getKey(), ((ORecordLazyList) fieldValue).copy(iCloned));

      } else if (fieldValue instanceof ORecordTrackedList) {
        final ORecordTrackedList newList = new ORecordTrackedList(iCloned);
        newList.addAll((ORecordTrackedList) fieldValue);
        iCloned._fieldValues.put(iEntry.getKey(), newList);

      } else if (fieldValue instanceof OTrackedList<?>) {
        final OTrackedList<Object> newList = new OTrackedList<Object>(iCloned);
        newList.addAll((OTrackedList<Object>) fieldValue);
        iCloned._fieldValues.put(iEntry.getKey(), newList);

      } else if (fieldValue instanceof List<?>) {
        iCloned._fieldValues.put(iEntry.getKey(), new ArrayList<Object>((List<Object>) fieldValue));

        // SETS
      } else if (fieldValue instanceof ORecordLazySet) {
        iCloned._fieldValues.put(iEntry.getKey(), ((ORecordLazySet) fieldValue).copy(iCloned));

      } else if (fieldValue instanceof ORecordTrackedSet) {
        final ORecordTrackedSet newList = new ORecordTrackedSet(iCloned);
        newList.addAll((ORecordTrackedSet) fieldValue);
        iCloned._fieldValues.put(iEntry.getKey(), newList);

      } else if (fieldValue instanceof OTrackedSet<?>) {
        final OTrackedSet<Object> newList = new OTrackedSet<Object>(iCloned);
        newList.addAll((OTrackedSet<Object>) fieldValue);
        iCloned._fieldValues.put(iEntry.getKey(), newList);

      } else if (fieldValue instanceof Set<?>) {
        iCloned._fieldValues.put(iEntry.getKey(), new HashSet<Object>((Set<Object>) fieldValue));

        // MAPS
      } else if (fieldValue instanceof ORecordLazyMap) {
        final ORecordLazyMap newMap = new ORecordLazyMap(iCloned, ((ORecordLazyMap) fieldValue).getRecordType());
        newMap.putAll((ORecordLazyMap) fieldValue);
        iCloned._fieldValues.put(iEntry.getKey(), newMap);

      } else if (fieldValue instanceof OTrackedMap) {
        final OTrackedMap<Object> newMap = new OTrackedMap<Object>(iCloned);
        newMap.putAll((OTrackedMap<Object>) fieldValue);
        iCloned._fieldValues.put(iEntry.getKey(), newMap);

      } else if (fieldValue instanceof Map<?, ?>) {
        iCloned._fieldValues.put(iEntry.getKey(), new LinkedHashMap<String, Object>((Map<String, Object>) fieldValue));
      } else
        iCloned._fieldValues.put(iEntry.getKey(), fieldValue);
  }

  protected String checkFieldName(String iFieldName) {
    if (iFieldName == null)
      throw new IllegalArgumentException("Field name is null");

    iFieldName = iFieldName.trim();

    if (iFieldName.length() == 0)
      throw new IllegalArgumentException("Field name is empty");

    for (int i = 0; i < iFieldName.length(); ++i) {
      final char c = iFieldName.charAt(i);
      if (c == ':' || c == ',')
        throw new IllegalArgumentException("Invalid field name '" + iFieldName + "'");
    }

    // if (!Character.isJavaIdentifierStart(iFieldName.charAt(0)))
    // throw new IllegalArgumentException("Invalid property name");
    //
    // for (int i = 1; i < iFieldName.length(); ++i)
    // if (!Character.isJavaIdentifierPart(iFieldName.charAt(i)))
    // throw new IllegalArgumentException("Invalid property name");

    return iFieldName;
  }
}
TOP

Related Classes of com.orientechnologies.orient.core.record.impl.ODocument

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.