Package com.tll.common.model

Source Code of com.tll.common.model.Model$ChangesPredicate

/**
* The Logic Lab
* @author jpk Aug 31, 2007
*/
package com.tll.common.model;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import com.allen_sauer.gwt.log.client.Log;
import com.tll.IDescriptorProvider;
import com.tll.IMarshalable;
import com.tll.common.bind.IBindable;
import com.tll.common.bind.IPropertyChangeListener;
import com.tll.common.model.CopyCriteria.CopyMode;
import com.tll.model.schema.IPropertyMetadataProvider;
import com.tll.model.schema.PropertyMetadata;
import com.tll.model.schema.PropertyType;
import com.tll.util.Binding;
import com.tll.util.BindingRefSet;
import com.tll.util.PropertyPath;
import com.tll.util.RefSet;
import com.tll.util.StringUtil;

/**
* Model - Encapsulates a set of {@link IModelProperty}s. This construct serves
* to represent an entity instance object graph on the client.
* @author jpk
*/
public final class Model implements IMarshalable, IBindable, IPropertyMetadataProvider, IEntityTypeProvider, IDescriptorProvider, Iterable<IModelProperty> {

  @SuppressWarnings("serial")
  static class ModelPropSet extends LinkedHashSet<IModelProperty> {

    /**
     * Ensures the given model prop is non-null and unique by name against the
     * existing child props (non-hierarchically).
     */
    @Override
    public boolean add(IModelProperty mp) {
      if(mp == null) return false;
      if(mp.getPropertyName() == null) throw new IllegalArgumentException();
      // we need to ensure the name is unique among the other model props (but
      // not hierarchically)
      for(final IModelProperty emp : this) {
        if(emp.getPropertyName().equals(mp.getPropertyName())) {
          return false;
        }
      }
      return super.add(mp);
    }

    /**
     * Sets the given {@link IPropertyValue}. If an existing model prop has the
     * same property name, it is replaced by the one given.
     * @param mp The {@link IModelProperty} to set
     */
    public void set(IModelProperty mp) {
      if(mp == null) return;
      if(mp.getPropertyName() == null) throw new IllegalArgumentException();
      final IModelProperty prop = get(mp.getPropertyName());
      if(prop != mp) {
        if(prop != null) {
          remove(prop);
        }
        add(mp);
      }
    }

    /**
     * Retrieves a model prop by property name
     * @param propName
     * @return The found model property or <code>null<code> if not found
     */
    public IModelProperty get(String propName) {
      for(final IModelProperty m : this) {
        if(m.getPropertyName().equals(propName)) return m;
      }
      return null;
    }

  } // ModelPropSet

  /**
   * CopyPredicate - One impl is defined for each declared {@link CopyMode}.
   * @author jpk
   */
  static interface ICopyPredicate {

    /**
     * Evaluates at the {@link Model} level the source and corresponding copy.
     * The copy is available for modification as well.
     * @param source the source model
     * @param copy the being copied to
     * @return <code>true</code> if the given source model's properties should
     *         be iterated for copy.
     */
    boolean evaluateSourceAndCopy(Model source, Model copy);

    /**
     * Evaluatea a source model and its corresponding root model relative path
     * for copying.
     * @param srcProp the source model prop ref
     * @param rootRelPath the root relative path of the given source model
     *        property
     * @return <code>true</code> if the given model property should be copied.
     */
    boolean evaluateProperty(IModelProperty srcProp, String rootRelPath);
  } // ICopyPredicate

  /**
   * WhitelistElement
   * @author jpk
   */
  static class WhitelistElement {

    IModelProperty srcProp;
    String rootRelPath, nearestParentRefPath;

    /**
     * Constructor
     * @param srcProp
     * @param rootRelPath
     * @param nearestParentRefPath
     */
    WhitelistElement(IModelProperty srcProp, String rootRelPath, String nearestParentRefPath) {
      super();
      this.srcProp = srcProp;
      this.rootRelPath = rootRelPath;
      this.nearestParentRefPath = nearestParentRefPath;
      //Log.debug("ChangesPredicate elm: " + this);
    }

    @Override
    public String toString() {
      return "srcProp: " + srcProp + ", rootRelPath: " + rootRelPath + ", nearestParentRefPath: " + nearestParentRefPath;
    }
  } // WhitelistElement

  /**
   * AllPropsPredicate
   * @author jpk
   */
  static class AllPropsPredicate implements ICopyPredicate {

    @Override
    public boolean evaluateSourceAndCopy(Model source, Model copy) {
      copy.markedDeleted = source.markedDeleted;
      return true; // no-op
    }

    @Override
    public boolean evaluateProperty(IModelProperty srcProp, String rootRelPath) {
      return true; // default
    }

  } // AllPropsPredicate

  /**
   * NoReferencesPredicate
   * @author jpk
   */
  static class NoReferencesPredicate extends AllPropsPredicate {

    @Override
    public boolean evaluateProperty(IModelProperty srcProp, String rootRelPath) {
      if(srcProp instanceof IModelRefProperty) {
        return !((IModelRefProperty) srcProp).isReference();
      }
      return true; // default
    }

  } // NoReferencesPredicate

  /**
   * SubsetPredicate
   * @author jpk
   */
  @SuppressWarnings("serial")
  class SubsetPredicate extends HashSet<WhitelistElement> implements ICopyPredicate {

    /**
     * Constructor
     * @param whitelistModelProps
     */
    public SubsetPredicate(Set<IModelProperty> whitelistModelProps) {
      super();
      // create whitelist elements needed for the main copy routine
      if(whitelistModelProps != null) {
        for(final IModelProperty mp : whitelistModelProps) {
          final String rootRelPath = getRelPath(mp);
          String nearestParentRefPath;
          if(mp instanceof IModelRefProperty || mp instanceof IndexedProperty) {
            nearestParentRefPath = rootRelPath;
          }
          else {
            // resolve the nearest parent (relational or indexed prop)
            final PropertyPath pp = new PropertyPath(rootRelPath);
            if(pp.depth() > 1) {
              nearestParentRefPath = pp.trim(1);
            }
            else {
              nearestParentRefPath = "";
            }
          }
          add(new WhitelistElement(mp, rootRelPath, nearestParentRefPath));
        }
      }
    }

    @Override
    public boolean evaluateSourceAndCopy(Model source, Model copy) {
      // core model props to always have
      copy.markedDeleted = source.markedDeleted;
      copy.setId(source.getId());
      copy.setVersion(source.getVersion());
      return true;
    }

    /**
     * We copy relational props if the are parent to targeted white list props
     * @param rootRelPath the current root relative path to check for copying
     * @return true/false
     */
    public boolean evaluateProperty(IModelProperty srcProp, String rootRelPath) {
      if(size() > 0) {
        final boolean relational = srcProp.getType().isRelational();
        final boolean indexed = srcProp.getType() == PropertyType.INDEXED;
        for(final WhitelistElement wle : this) {
          if(relational || indexed) {
            if(wle.nearestParentRefPath.indexOf(rootRelPath) == 0) {
              Log.debug("Allowing relational/indexed [" + rootRelPath + "]");
              return true;
            }
          }
          else {
            if(wle.rootRelPath.equals(rootRelPath) || rootRelPath.endsWith(ID_PROPERTY)
                || rootRelPath.endsWith(VERSION_PROPERTY)) {
              Log.debug("Allowing value/nested [" + rootRelPath + "]");
              return true;
            }
          }
        }
      }
      return false;
    }

  } // SubsetPredicate

  @SuppressWarnings("serial")
  class ChangesPredicate extends SubsetPredicate {

    /**
     * Constructor
     * @param whitelistModelProps
     */
    public ChangesPredicate(Set<IModelProperty> whitelistModelProps) {
      super(whitelistModelProps);
    }

    @Override
    public boolean evaluateSourceAndCopy(Model source, Model copy) {
      super.evaluateSourceAndCopy(source, copy);
      // when in change mode, only copy id and version when a model is marked as deleted
      if(source.markedDeleted) {
        return false;
      }
      return true;
    }

    /**
     * We copy relational props if the are parent to targeted white list props
     * @param rootRelPath the current root relative path to check for copying
     * @return true/false
     */
    @Override
    public boolean evaluateProperty(IModelProperty srcProp, String rootRelPath) {
      if(srcProp instanceof IModelRefProperty) {
        final Model m = ((IModelRefProperty) srcProp).getModel();
        if(m != null && m.isMarkedDeleted()) {
          //Log.debug("Allowing model ref [" + rootRelPath + "]");
          return true;
        }
      }
      if(srcProp instanceof IPropertyValue) {
        final PropertyMetadata metadata = ((IPropertyValue) srcProp).getMetadata();
        if(metadata != null && metadata.isManaged()) return false;
      }
      return super.evaluateProperty(srcProp, rootRelPath);
    }

  } // ChangesPredicate

  /**
   * Entity id property name
   */
  public static final String ID_PROPERTY = "id";

  /**
   * Entity version property name
   */
  public static final String VERSION_PROPERTY = "version";

  /**
   * Entity name property name. May not be set for entity definitions without a
   * name property.
   */
  public static final String NAME_PROPERTY = "name";

  /**
   * Entity data created property name. May not be set for entity definitions
   * without a date created property.
   */
  public static final String DATE_CREATED_PROPERTY = "dateCreated";

  /**
   * Entity data modified property name. May not be set for entity definitions
   * without a date modified property.
   */
  public static final String DATE_MODIFIED_PROPERTY = "dateModified";

  /**
   * Resolves the root relative property path of a given model property
   * descendant.
   * @param descendant
   * @param current
   * @param parentPath
   * @param visited
   * @return the root relative path or <code>null</code> if not a descendant.
   */
  private static String resolveModelProperty(final IModelProperty descendant, IModelProperty current,
      String parentPath, RefSet<Model> visited) {

    if(current.getType().isModelRef()) {
      final Model m = ((IModelRefProperty) current).getModel();
      if(visited.exists(m)) return null;
      if(!visited.add(m)) throw new IllegalStateException();
    }

    final String cpp = PropertyPath.getPropertyPath(parentPath, current.getPropertyName());
    //Log.debug("resolveModelProperty() current prop path: " + cpp);

    if(descendant == current) return cpp;

    if(current instanceof RelatedOneProperty) {
      final RelatedOneProperty rop = (RelatedOneProperty) current;
      final Model m = rop.getModel();
      if(m != null) {
        for(final IModelProperty mp : m) {
          final String path = resolveModelProperty(descendant, mp, cpp, visited);
          if(path != null) return path;
        }
      }
    }
    else if(current instanceof RelatedManyProperty) {
      final RelatedManyProperty rmp = (RelatedManyProperty) current;
      for(final IndexedProperty ip : rmp) {
        final String ipath = PropertyPath.index(cpp, ip.getIndex());
        if(ip == descendant) return ipath;
        for(final IModelProperty mp : ip.getModel()) {
          final String path = resolveModelProperty(descendant, mp, ipath, visited);
          if(path != null) return path;
        }
      }
    }

    // not a descendant!
    return null;
  }

  /**
   * Recursive copy routine to guard against re-copying entities.
   * <p>
   * <b>NOTE: </b>A model prop whitelist trumps the references flag!
   * @param parentPropPath the parent property path relative to the "root" model
   * @param source The model to be copied
   * @param cp the copy predicate that filters what is copied
   * @param visited list of {@link Binding} where the binding source is the
   *        model source and the target is the model copy
   * @return the copied model
   */
  private static Model copy(String parentPropPath, final Model source, ICopyPredicate cp,
      final BindingRefSet<Model, Model> visited) {

    if(source == null) return null;

    Model copy = null;

    // check visited
    Binding<Model, Model> b = visited.findBindingBySource(source);
    if(b != null) {
      //Log.debug("Already visited target: " + b.tgt);
      return b.tgt;
    }
    copy = new Model(source.entityType);
    b = new Binding<Model, Model>(source, copy);
    if(!visited.add(b)) throw new IllegalStateException();

    if(!cp.evaluateSourceAndCopy(source, copy)) {
      return copy;
    }

    for(final IModelProperty srcprop : source.props) {
      assert srcprop != null;

      final String crntPropPath = PropertyPath.getPropertyPath(parentPropPath, srcprop.getPropertyName());
      //Log.debug("copy() Checking prop: " + crntPropPath);

      if(!cp.evaluateProperty(srcprop, crntPropPath)) {
        continue;
      }

      // related one
      if(srcprop instanceof RelatedOneProperty) {
        final IModelRefProperty mrp = (IModelRefProperty) srcprop;
        final Model srcModel = mrp.getModel();
        final Model cpyModel = srcModel == null ? null : copy(crntPropPath, srcModel, cp, visited);
        copy.set(new RelatedOneProperty(mrp.getRelatedType(), cpyModel, mrp.getPropertyName(), mrp.isReference()));
      }

      // related many
      else if(srcprop instanceof RelatedManyProperty) {
        final RelatedManyProperty rmp = (RelatedManyProperty) srcprop;
        final ArrayList<Model> clist = new ArrayList<Model>(rmp.size());
        for(final IndexedProperty ip : rmp) {
          final String ipath = PropertyPath.index(crntPropPath, ip.getIndex());
          if(cp.evaluateProperty(ip, ipath)) {
            final Model im = ip.getModel();
            final Model cim = copy(ipath, im, cp, visited);
            if(cim != null) clist.add(cim);
          }
        }
        copy.set(new RelatedManyProperty(rmp.getRelatedType(), rmp.getPropertyName(), rmp.isReference(), clist));
      }

      // value or nested..
      else {
        assert srcprop.getType().isValue() || srcprop.getType().isNested();
        copy.set(((IPropertyValue) srcprop).copy());
      }
    } // loop

    return copy;
  }

  /**
   * Clears all nested property values of the given model.
   * @param model The group to be cleared
   * @param clearReferences Clear valus held in related models marked as a
   *        reference?
   * @param visited
   */
  private static void clearProps(Model model, final boolean clearReferences, final boolean retainIdAndVersion,
      RefSet<Model> visited) {

    if(model == null) return;

    // check visited
    if(visited.exists(model)) return;
    if(!visited.add(model)) throw new IllegalStateException();

    model.markedDeleted = false;

    for(final IModelProperty prop : model.props) {
      assert prop != null;

      // model prop (relational) val...
      if(prop instanceof IModelRefProperty) {
        final IModelRefProperty gpv = (IModelRefProperty) prop;
        if(clearReferences || !gpv.isReference()) {
          clearProps(gpv.getModel(), clearReferences, retainIdAndVersion, visited);
        }
      }

      // model list (relational) prop val...
      else if(prop instanceof RelatedManyProperty) {
        final RelatedManyProperty rmp = (RelatedManyProperty) prop;
        if(clearReferences || !rmp.isReference()) {
          final List<Model> list = rmp.getModelList();
          if(list != null) {
            for(final Model m : list) {
              clearProps(m, clearReferences, retainIdAndVersion, visited);
            }
          }
        }
      }

      // property values...
      else {
        if(!retainIdAndVersion
            || (retainIdAndVersion && (!ID_PROPERTY.equals(prop.getPropertyName()) && !VERSION_PROPERTY.equals(prop
                .getPropertyName())))) ((IPropertyValue) prop).clear();
      }
    }
  }

  /**
   * The set of model properties. <br>
   * NOTE: can't mark as final for GWT RPC compatibility
   */
  private/*final*/ModelPropSet props = new ModelPropSet();

  /**
   * The bound entity type.
   */
  private IEntityType entityType;

  /**
   * The marked deleted flag. When <code>true</code>, this indicates this model
   * data is scheduled for deletion.
   */
  boolean markedDeleted;

  /**
   * Self-reference expressed as an {@link IModelRefProperty} which is
   * instantiated and cached upon demand.
   */
  RelatedOneProperty selfRef;

  /**
   * Constructor
   */
  public Model() {
    super();
  }

  /**
   * Constructor
   * @param entityType required since we depend on the model key for equality!
   */
  public Model(IEntityType entityType) {
    super();
    this.entityType = entityType;
  }

  /**
   * @return the ref type
   */
  public IEntityType getEntityType() {
    return entityType;
  }

  /**
   * Is this model entity new?
   * @return true/false
   */
  public boolean isNew() {
    return getVersion() == null;
  }

  /**
   * @return the {@link #ID_PROPERTY} value which may be <code>null</code> if
   *         this is scalar data.
   */
  public String getId() {
    final StringPropertyValue prop = (StringPropertyValue) get(ID_PROPERTY);
    return prop == null ? null : prop.getString();
  }

  /**
   * Sets the id property creating it if not present.
   * @param id
   */
  public void setId(String id) {
    setPropertyNoPropertyPathException(ID_PROPERTY, id, PropertyType.STRING);
  }

  /**
   * @return the {@link #NAME_PROPERTY} value which may be <code>null</code>.
   */
  public String getName() {
    final StringPropertyValue prop = (StringPropertyValue) get(NAME_PROPERTY);
    return prop == null ? null : prop.getString();
  }

  /**
   * Sets the name property creating it if not present.
   * @param name
   */
  public void setName(String name) {
    setPropertyNoPropertyPathException(NAME_PROPERTY, name, PropertyType.STRING);
  }

  /**
   * @return The date created. May be <code>null</code> for entity types that
   *         don't support this property.
   */
  public Date getDateCreated() {
    final DatePropertyValue prop = (DatePropertyValue) get(DATE_CREATED_PROPERTY);
    return prop == null ? null : prop.getDate();
  }

  /**
   * @return The date last modified. May be <code>null</code> for entity types
   *         that don't support this property.
   */
  public Date getDateModified() {
    final DatePropertyValue prop = (DatePropertyValue) get(DATE_MODIFIED_PROPERTY);
    return prop == null ? null : prop.getDate();
  }

  /**
   * @return the {@link #VERSION_PROPERTY} value which may be <code>null</code>
   *         if this is scalar data.
   */
  public Integer getVersion() {
    final IntPropertyValue prop = (IntPropertyValue) get(VERSION_PROPERTY);
    return prop == null ? null : prop.getInteger();
  }

  public void setVersion(Integer version) {
    setPropertyNoPropertyPathException(VERSION_PROPERTY, version, PropertyType.INT);
  }

  /**
   * @return the {@link ModelKey} for this instance.
   */
  public ModelKey getKey() {
    return new ModelKey(entityType, getId(), getName());
  }

  /**
   * Finds a property value in this model's collection of property values given
   * a non-path property name. (No property path resolution is performed.)
   * <p>
   * @param name The non-path property name (i.e. no dots)
   * @return The found {@link IPropertyValue} or <code>null</code> if not
   *         present.
   */
  public IModelProperty get(String name) {
    return props.get(name);
  }

  /**
   * Sets the given {@link IPropertyValue} as a child to this model with no
   * property path resolution preformed. If an existing prop val is currently
   * mapped to the ascribed property name, it is replaced by the one given.
   * <p>
   * <em><b>IMPT:</b> This method does not fire property change events.</em>
   * @param mprop The replacing {@link IPropertyValue}
   */
  public void set(IModelProperty mprop) {
    props.set(mprop);
  }

  /**
   * Provides the property value in String form
   * <em>for self-formatting types only</em>.
   * @param propPath The property path
   * @return The property value as a String or <code>null</code> when the
   *         property does not exist.
   * @throws IllegalArgumentException When the targeted property is relational
   *         or is not self-formatting.
   */
  public String asString(String propPath) throws IllegalArgumentException {
    IModelProperty prop = null;
    try {
      prop = getModelProperty(propPath);
    }
    catch(final PropertyPathException e) {
      throw new IllegalArgumentException(e.getMessage());
    }
    assert prop != null;
    if(prop instanceof ISelfFormattingPropertyValue == false) {
      throw new IllegalArgumentException("Non self-formatting property: " + propPath);
    }
    return ((ISelfFormattingPropertyValue) prop).asString();
  }

  public Object getProperty(String propPath) throws PropertyPathException {
    return getModelProperty(propPath).getValue();
  }

  public void setProperty(String propPath, Object value) throws PropertyPathException, IllegalArgumentException {
    setProperty(propPath, value, null);
  }

  /**
   * Same as {@link #setProperty(String, Object, PropertyType)} but when a
   * @param propPath
   * @param value
   * @param ptype if specified, the property will be automatically created if it
   *        doesn't exist {@link PropertyPathException} occurs, it is converted
   *        into an {@link IllegalStateException}.
   * @throws IllegalArgumentException
   * @throws IllegalStateException
   */
  public void setPropertyNoPropertyPathException(String propPath, Object value, PropertyType ptype)
  throws IllegalArgumentException, IllegalStateException {
    try {
      setProperty(propPath, value, ptype);
    }
    catch(final PropertyPathException e) {
      // shouldn't happen
      throw new IllegalStateException(e);
    }
  }

  /**
   * Does a property exist?
   * @param propPath The property path to test
   * @return true/false
   */
  public boolean propertyExists(String propPath) {
    try {
      return getModelProperty(propPath) != null;
    }
    catch(final PropertyPathException e) {
      return false;
    }
  }

  /**
   * Resolves a property path to the nested {@link IModelProperty}. This is a
   * generic way to obtain a defined model property.
   * <p>
   * <strong>NOTE: </strong>When a property path element having no associated
   * property is encoutered before reaching the end of the given property path
   * or when a given index is found out of range for an indexable property in
   * the given property path, <code>null</code> is returned.
   * @param propPath The property path. When <code>null</code> or empty, a
   *        {@link RelatedOneProperty} that references <em>this</em> model is
   *        returned.
   * @return The resolved non-<code>null</code> model property
   * @throws PropertyPathException When the model property can't be resolved.
   */
  public IModelProperty getModelProperty(String propPath) throws PropertyPathException {
    return StringUtil.isEmpty(propPath) ? getSelfRef() : resolvePropertyPath(propPath);
  }

  /**
   * Retrieves a non-relational property value from this {@link Model} given a
   * property path. This method is behaves like
   * {@link #getModelProperty(String)} with a filter that targets
   * {@link IPropertyValue}s only.
   * @param propPath Points to the desired model property
   * @return The resolved non-<code>null</code> {@link IPropertyValue}
   * @throws PropPathNodeMismatchException When the given property path does not
   *         resolve to a property value.
   * @throws PropertyPathException When the property path is mal-formed or
   *         doesn't point to an existing model property.
   */
  public IPropertyValue getPropertyValue(String propPath) throws PropPathNodeMismatchException, PropertyPathException {
    final IModelProperty prop = getModelProperty(propPath);
    if(prop == null) return null;
    if(!prop.getType().isValue()) {
      throw new PropPathNodeMismatchException(propPath, prop.getPropertyName(), prop.getType().toString(), "value");
    }
    return (IPropertyValue) prop;
  }

  /**
   * Extracts a nested Model from a targeted {@link IModelRefProperty}.
   * @param propPath The property path that points to the desired model ref
   *        property. If <code>null</code> or empty, this {@link Model} is
   *        returned.
   * @return The resolved {@link Model} or <code>null</code> if the model
   *         property doesn't exist or it does and the held model ref is
   *         <code>null</code>
   * @throws IllegalArgumentException When the given property path can't be
   *         resolved or does not map to an {@link IModelRefProperty}.
   */
  public Model nestedModel(String propPath) throws IllegalArgumentException {
    try {
      final IModelProperty prop = getModelProperty(propPath);
      assert prop != null;
      if(!prop.getType().isModelRef()) {
        throw new PropPathNodeMismatchException(propPath, prop.getPropertyName(), prop.getType().toString(),
        "model reference");
      }
      return ((IModelRefProperty) prop).getModel();
    }
    catch(final UnsetPropertyException e) {
      return null;
    }
    catch(final NullNodeInPropPathException e) {
      return null;
    }
    catch(final PropertyPathException e) {
      throw new IllegalArgumentException(e);
    }
  }

  /**
   * Retrieves a related one property value from the model given a property
   * path.
   * @param propPath The property path (E.g.: "root.relatedModelPropName")
   * @return The resolved related one property or <code>null</code> if it doesn't exist
   * @throws IllegalArgumentException When the given property path can't be
   *         resolved or does not map to related one property.
   */
  public RelatedOneProperty relatedOne(String propPath) throws IllegalArgumentException {
    try {
      final IModelProperty prop = getModelProperty(propPath);
      assert prop != null;
      if(prop.getType() != PropertyType.RELATED_ONE) {
        throw new PropPathNodeMismatchException(propPath, prop.getPropertyName(), prop.getType().toString(),
        "related one");
      }
      return (RelatedOneProperty) prop;
    }
    catch(final UnsetPropertyException e) {
      return null;
    }
    catch(final NullNodeInPropPathException e) {
      return null;
    }
    catch(final PropertyPathException e) {
      throw new IllegalArgumentException(e);
    }
  }

  /**
   * Retrieves a related many property value from the model given a property
   * path.
   * @param propPath The property path (E.g.: "root.listProperty")
   * @return The resolved related many property or <code>null</code> if it doesn't exist
   * @throws IllegalArgumentException When the given property path can't be
   *         resolved or does not map to a related many property.
   */
  public RelatedManyProperty relatedMany(String propPath) throws IllegalArgumentException {
    try {
      final IModelProperty prop = getModelProperty(propPath);
      assert prop != null;
      if(prop.getType() != PropertyType.RELATED_MANY) {
        throw new PropPathNodeMismatchException(propPath, prop.getPropertyName(), prop.getType().toString(),
        "related many");
      }
      return (RelatedManyProperty) prop;
    }
    catch(final UnsetPropertyException e) {
      return null;
    }
    catch(final NullNodeInPropPathException e) {
      return null;
    }
    catch(final PropertyPathException e) {
      throw new IllegalArgumentException(e);
    }
  }

  /**
   * Retrieves an indexed property value from the model given a property path.
   * This is a property value that wraps a nested Model that is a child of a
   * related many property.
   * @param propPath The property path. (E.g.: "root.listProperty[1]")
   * @return The resolved indexed property or <code>null</code> if it doesn't exist.
   * @throws IllegalArgumentException When the given property path can't be
   *         resolved or does not map to an indexed property.
   */
  public IndexedProperty indexed(String propPath) throws IllegalArgumentException {
    try {
      final IModelProperty prop = getModelProperty(propPath);
      assert prop != null;
      if(prop.getType() != PropertyType.INDEXED) {
        throw new PropPathNodeMismatchException(propPath, prop.getPropertyName(), prop.getType().toString(), "indexed");
      }
      return (IndexedProperty) prop;
    }
    catch(final UnsetPropertyException e) {
      return null;
    }
    catch(final NullNodeInPropPathException e) {
      return null;
    }
    catch(final PropertyPathException e) {
      throw new IllegalArgumentException(e);
    }
  }

  public PropertyMetadata getPropertyMetadata(String propPath) {
    try {
      return getPropertyValue(propPath).getMetadata();
    }
    catch(final PropertyPathException e) {
      return null;
    }
  }

  /**
   * Calculates the property path of the given descendant model property.
   * @param descendant required model prop ref
   * @return the non-<code>null</code> property path that resolves to the given
   *         descendant. An empty string is returned if the given model property
   *         refers to *this* model instance.
   * @throws IllegalArgumentException When the given model property is not a
   *         descendant of this model instance.
   */
  public String getRelPath(IModelProperty descendant) throws IllegalArgumentException {
    if(descendant  == null) throw new IllegalArgumentException("Null descendant arg");
    if(descendant == selfRef) return "";
    if(descendant instanceof IModelRefProperty) {
      if(((IModelRefProperty) descendant).getModel() == this) return "";
    }
    final String s =
      resolveModelProperty(descendant, getSelfRef(), "", new RefSet<Model>());
    if(s == null)
      throw new IllegalArgumentException("[" + descendant + "] is not a descendant of model: [" + this + "]");
    return s;
  }

  /**
   * Deep copies this instance creating a new distinct model instance containing
   * a sub-set of properties that match the given criteria.
   * @param criteria the copy criteria
   * @return Clone of this instance or <code>null</code> if this model is marked
   *         as deleted and the given <code>copyMarkedDeleted</code> param is
   *         <code>true</code>.
   */
  public Model copy(final CopyCriteria criteria) {
    ICopyPredicate cp;
    switch(criteria.getMode()) {
    case SUBSET:
      cp = new SubsetPredicate(criteria.getWhitelistProps());
      break;
    case CHANGES:
      cp = new ChangesPredicate(criteria.getWhitelistProps());
      break;
    case NO_REFERENCES:
      cp = new NoReferencesPredicate();
      break;
    default:
    case ALL:
      cp = new AllPropsPredicate();
      break;
    }
    return copy(null, this, cp, new BindingRefSet<Model, Model>());
  }

  /**
   * Clears a single property value.
   * @param propPath Identifies the property value to clear
   * @throws PropertyPathException
   */
  public void clearPropertyValue(String propPath) throws PropertyPathException {
    getPropertyValue(propPath).clear();
  }

  /**
   * Walks the held collection of {@link IPropertyValue}s clearing then
   * recursing as necessary to ensure all have been visited.
   * @param clearReferences Clear valus held in related models marked as a
   *        reference?
   * @param retainIdAndVersion
   */
  public void clearPropertyValues(boolean clearReferences, boolean retainIdAndVersion) {
    clearProps(this, clearReferences, retainIdAndVersion, new RefSet<Model>());
  }

  /**
   * Non-hierarchical iteration.
   * @return An iterator traversing only the immediate child model propertis of
   *         this intance.
   */
  public Iterator<IModelProperty> iterator() {
    return props.iterator();
  }

  /**
   * @return <code>true</code> if this group is marked for deletion.
   */
  public boolean isMarkedDeleted() {
    return markedDeleted;
  }

  /**
   * Sets the makred for deletion flag.
   * @param markedDeleted
   */
  public void setMarkedDeleted(boolean markedDeleted) {
    this.markedDeleted = markedDeleted;
  }

  public void addPropertyChangeListener(IPropertyChangeListener listener) {
    throw new UnsupportedOperationException();
  }

  public void addPropertyChangeListener(String propertyName, IPropertyChangeListener listener) {
    try {
      resolvePropertyPath(propertyName).addPropertyChangeListener(listener);
    }
    catch(final PropertyPathException e) {
      throw new IllegalArgumentException("Unable to add property change listener", e);
    }
  }

  public IPropertyChangeListener[] getPropertyChangeListeners() {
    throw new UnsupportedOperationException();
  }

  public void removePropertyChangeListener(IPropertyChangeListener listener) {
    throw new UnsupportedOperationException();
  }

  public void removePropertyChangeListener(String propertyName, IPropertyChangeListener listener) {
    try {
      resolvePropertyPath(propertyName).removePropertyChangeListener(listener);
    }
    catch(final PropertyPathException e) {
      throw new IllegalArgumentException("Unable to remove property change listener", e);
    }
  }

  @Override
  public String descriptor() {
    return getKey().descriptor();
  }

  /**
   * @return The number of immediate (non-hierarchical) child properties.
   */
  public int size() {
    return props.size();
  }

  // Don't rely on logical equals since we want to support scalar data in a Model instance!
  // so we need to be careful and do manual checking for model collections when searching for a particular instance
  /*
  @Override
  public boolean equals(Object obj) {
    if(this == obj) return true;
    if(!(obj instanceof Model)) return false;
    return getKey().equals(((Model) obj).getKey());
  }

  @Override
  public int hashCode() {
    return getKey().hashCode();
  }
   */

  @Override
  public String toString() {
    return getKey().toString()/* + " [" + ((Object) this).hashCode() + ']'*/;
  }

  private IModelRefProperty getSelfRef() {
    if(selfRef == null) {
      selfRef = new RelatedOneProperty(entityType, this, null, true);
    }
    return selfRef;
  }

  /**
   * Sets a model properties' value conditionally creating the wrapping model
   * property if not present.
   * @param propPath
   * @param value
   * @param ptype if specified, the property will be automatically created if it
   *        doesn't exist
   * @throws PropertyPathException
   * @throws IllegalArgumentException
   */
  private void setProperty(String propPath, Object value, PropertyType ptype) throws PropertyPathException,
  IllegalArgumentException {
    try {
      IModelProperty mprop = null;
      if(PropertyPath.isIndexed(propPath)) {
        // divert to the "physical" related many property as indexed properties
        // are "virtual"
        mprop = relatedMany(PropertyPath.deIndex(propPath));
      }
      else {
        mprop = getModelProperty(propPath);
      }
      mprop.setProperty(propPath, value);
    }
    catch(final UnsetPropertyException e) {
      if(ptype != null) {
        // create it
        final IPropertyValue pv = AbstractPropertyValue.create(ptype, e.parentProperty, null);
        pv.setValue(value);
        e.parentModel.set(pv);
      }
    }
  }

  /**
   * Resolves a given property path against the hierarchy of this model throwing
   * a specific {@link PropertyPathException} when an error occurs.
   * @param propPath The property path
   * @return The non-<code>null</code> resolved model property
   * @throws PropertyPathException When an error occurrs whilst resolving the
   *         property path or when the given property path does not resolve to
   *         an existant property
   */
  private IModelProperty resolvePropertyPath(final String propPath) throws PropertyPathException {
    if(StringUtil.isEmpty(propPath)) {
      throw new MalformedPropPathException("No property path specified.");
    }

    final PropertyPath pp = new PropertyPath(propPath);
    IModelProperty prop = null;
    Model model = this;
    final int len = pp.depth();
    for(int i = 0; i < len; i++) {
      final String pname = pp.nameAt(i);
      final int index;
      try {
        index = pp.indexAt(i);
      }
      catch(final IllegalArgumentException e) {
        throw new MalformedPropPathException(e.getMessage());
      }
      final boolean indexed = (index >= 0);
      final boolean atEnd = (i == len - 1);

      // find the prop val under current model
      prop = model.get(pname);
      if(prop == null) {
        if(atEnd) {
          throw new UnsetPropertyException(pp.toString(), model, pname);
        }
        throw new NullNodeInPropPathException(pp.toString(), pname);
      }

      // get the bound prop val type for this prop path element
      final PropertyType pvType = prop.getType();

      // non-relational prop val
      if(!pvType.isRelational()) {
        if(!atEnd) {
          throw new PropPathNodeMismatchException(pp.toString(), pname, pvType.toString(), "Relational");
        }
        return prop;
      }

      // related one prop val
      else if(pvType == PropertyType.RELATED_ONE) {
        if(indexed) {
          throw new PropPathNodeMismatchException(pp.toString(), pname, pvType.toString(), PropertyType.RELATED_MANY
              .toString());
        }
        final IModelRefProperty mrp = (IModelRefProperty) prop;
        if(atEnd) {
          return mrp;
        }
        // get the nested group...
        final Model ng = mrp.getModel();
        if(ng == null) {
          throw new NullNodeInPropPathException(pp.toString(), pname);
        }
        // reset for next path
        model = ng;
      }

      // related many prop val
      else if(pvType == PropertyType.RELATED_MANY) {
        final RelatedManyProperty rmp = (RelatedManyProperty) prop;
        if(!indexed) {
          if(atEnd) {
            return rmp;
          }
          // an index is expected if we're not at the end
          throw new MalformedPropPathException(pp.toString());
        }
        else if(indexed) {
          // get the nested group prop val list...
          if(index >= rmp.size()) {
            throw new IndexOutOfRangeInPropPathException(pp.toString(), pname, index);
          }
          if(atEnd) {
            return rmp.getIndexedProperty(index);
          }
          // reset for next path
          model = rmp.getIndexedProperty(index).getModel();
        }
      }
    }
    throw new MalformedPropPathException(propPath);
  }

}
TOP

Related Classes of com.tll.common.model.Model$ChangesPredicate

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.