content: /*****************************************************************
   *   Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you 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
   *
   *    https://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 org.apache.cayenne.util;
  
  import java.util.Collection;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  import org.apache.cayenne.CayenneRuntimeException;
  import org.apache.cayenne.Persistent;
  import org.apache.cayenne.ValueHolder;
  import org.apache.cayenne.reflect.Accessor;
  
  /**
   * @since 3.0
   */
  public class PersistentObjectMap extends RelationshipFault implements Map, ValueHolder {
  
      protected Map objectMap;
      protected Accessor mapKeyAccessor;
  
      // exists for the benefit of manual serialization schemes such as the one in Hessian.
      private PersistentObjectMap() {
      }
  
      /**
       * Creates PersistentObjectList initializing it with list owner persistent object and
       * relationship name that this list maps to.
       * 
       * @param relationshipOwner persistent object that owns this list.
       * @param relationshipName a query used to resolve the list
       * @param mapKeyAccessor an accessor that can read a map key from an object.
       */
      public PersistentObjectMap(Persistent relationshipOwner, String relationshipName,
              Accessor mapKeyAccessor) {
          super(relationshipOwner, relationshipName);
          this.mapKeyAccessor = mapKeyAccessor;
      }
  
      public Object getValue() throws CayenneRuntimeException {
          return resolvedObjectMap();
      }
  
      public Object getValueDirectly() throws CayenneRuntimeException {
          return objectMap;
      }
  
      public void invalidate() {
          setObjectMap(null);
      }
      
      @Override
      protected void mergeLocalChanges(List resolved) {
          // TODO implement
      }
  
      public boolean isFault() {
  
          if (objectMap != null) {
              return false;
          }
  
          // resolve on the fly if owner is transient... Can't do it in constructor, as
          // object may be in an inconsistent state during construction time
          // synchronize??
          else if (isTransientParent()) {
              objectMap = new HashMap();
              return false;
          }
          else {
              return true;
          }
      }
  
      public Object setValue(Object value) throws CayenneRuntimeException {
          resolvedObjectMap();
          return setValueDirectly(value);
      }
  
      public Object setValueDirectly(Object value) throws CayenneRuntimeException {
          Object old = this.objectMap;
  
          if (value == null || value instanceof Map) {
              setObjectMap((Map) value);
          } else if (value instanceof Collection) {
              // we can index collections on the fly - this is needed for prefetch handling...
              // although it seems to be breaking the contract for 'setValueDirectly' ???
              setObjectMap(indexCollection((Collection<Object>) value));
          } else {
              throw new CayenneRuntimeException("Value must be a Map, a Collection or null, got: %s"
                              , value.getClass().getName());
          }
  
          return old;
      }
  
      public void setObjectMap(Map objectMap) {
          this.objectMap = objectMap;
      }
  
      // ====================================================
      // Tracking map modifications, and resolving it
      // on demand
      // ====================================================
  
      /**
       * Returns internal objects list resolving it if needed.
       */
      protected Map resolvedObjectMap() {
          if (isFault()) {
  
              synchronized (this) {
  
                  // now that we obtained the lock, check
                  // if another thread just resolved the map
                  if (isFault()) {
                      List localList = resolveFromDB();
  
                      // map objects by property
                      Map localMap = indexCollection(localList);
  
                      // TODO: andrus 8/20/2007 implement merging local changes like
                      // PersistentObjectList does
                      // mergeLocalChanges(localList);
  
                      this.objectMap = localMap;
                  }
              }
          }
  
          return objectMap;
      }
  
      /**
       * Converts a collection into a map indexed by map key.
       */
      protected Map indexCollection(Collection<Object> collection) {
          // map objects by property
          Map<Object, Object> map = new HashMap<>((int) (collection.size() * 1.33d) + 1);
  
          if (collection.size() > 0) {
  
              for (Object next : collection) {
                  Object key = mapKeyAccessor.getValue(next);
                  Object previous = map.put(key, next);
                  if (previous != null && previous != next) {
                      throw new CayenneRuntimeException("Duplicate key '%s' in relationship map. "
                              + "Relationship: %s, source object: %s"
                              , key, relationshipName, relationshipOwner.getObjectId());
                  }
              }
          }
  
          return map;
      }
  
      @Override
      public String toString() {
          return (objectMap != null) ? objectMap.toString() : "{<unresolved>}";
      }
  
      protected void postprocessAdd(Object addedObject) {
  
          // notify ObjectContext
          if (relationshipOwner.getObjectContext() != null) {
              relationshipOwner.getObjectContext().propertyChanged(
                      relationshipOwner,
                      relationshipName,
                      null,
                      addedObject);
              
              if (addedObject instanceof Persistent) {
                  Util.setReverse(relationshipOwner, relationshipName,
                          (Persistent) addedObject);
              }
          }
      }
  
      protected void postprocessAdd(Collection collection) {
          Iterator it = collection.iterator();
          while (it.hasNext()) {
              postprocessAdd(it.next());
          }
      }
  
      protected void postprocessRemove(Object removedObject) {
  
          // notify ObjectContext
          if (relationshipOwner.getObjectContext() != null) {
              relationshipOwner.getObjectContext().propertyChanged(
                      relationshipOwner,
                      relationshipName,
                      removedObject,
                      null);
              if (removedObject instanceof Persistent) {
                  Util.unsetReverse(relationshipOwner, relationshipName,
                          (Persistent) removedObject);
              }
          }
      }
  
      protected void postprocessRemove(Collection collection) {
          Iterator it = collection.iterator();
          while (it.hasNext()) {
              postprocessRemove(it.next());
          }
      }
  
      // ====================================================
      // Standard Map Methods.
      // ====================================================
  
      public void clear() {
          Map resolved = resolvedObjectMap();
          postprocessRemove(resolved.values());
          resolved.clear();
      }
  
      public boolean containsKey(Object key) {
          return resolvedObjectMap().containsKey(key);
      }
  
      public boolean containsValue(Object value) {
          return resolvedObjectMap().containsValue(value);
      }
  
      public Set entrySet() {
          return resolvedObjectMap().entrySet();
      }
  
      public Object get(Object key) {
          return resolvedObjectMap().get(key);
      }
  
      public boolean isEmpty() {
          return resolvedObjectMap().isEmpty();
      }
  
      public Set keySet() {
          return resolvedObjectMap().keySet();
      }
  
      public Object put(Object key, Object value) {
          // TODO: andrus 8/20/2007 implement handling local changes without faulting like
          // PersistentObjectList does
  
          Object oldValue = resolvedObjectMap().put(key, value);
  
          postprocessAdd(value);
          postprocessRemove(oldValue);
  
          return oldValue;
      }
  
      public void putAll(Map map) {
          resolvedObjectMap().putAll(map);
          postprocessAdd(map.values());
      }
  
      public Object remove(Object key) {
          // TODO: andrus 8/20/2007 implement handling local changes without faulting like
          // PersistentObjectList does
  
          Object removed = resolvedObjectMap().remove(key);
          postprocessRemove(removed);
          return removed;
      }
  
      public int size() {
          return resolvedObjectMap().size();
      }
  
      public Collection values() {
          return resolvedObjectMap().values();
      }
      
      public void putDirectly(Object key, Object value) {
          //TODO: should not resolve manually
          resolvedObjectMap().put(key, value);
      }
      
      public void removeDirectly(Object key) {
          //TODO: should not resolve manually
          resolvedObjectMap().remove(key);
      }
  }
  