Пример #1
0
/**
 * Class mapping for embedded objects.
 *
 * @author Abe White
 * @nojavadoc
 */
public class EmbeddedClassStrategy extends AbstractClassStrategy {

  private static final Localizer _loc = Localizer.forPackage(EmbeddedClassStrategy.class);

  public void map(boolean adapt) {
    ValueMapping vm = cls.getEmbeddingMapping();
    if (vm == null || vm.getType() != cls.getDescribedType())
      throw new MetaDataException(_loc.get("not-embed", cls));

    ClassMappingInfo info = cls.getMappingInfo();
    info.assertNoSchemaComponents(cls, true);

    ClassMapping owner = null;
    if (vm.getValueMappedByMapping() != null)
      owner = vm.getValueMappedByMapping().getDefiningMapping();
    else owner = vm.getFieldMapping().getDefiningMapping();
    cls.setIdentityType(owner.getIdentityType());
    cls.setObjectIdType(owner.getObjectIdType(), owner.isObjectIdTypeShared());
    cls.setTable(vm.getFieldMapping().getTable());
    cls.setPrimaryKeyColumns(owner.getPrimaryKeyColumns());
    cls.setColumnIO(owner.getColumnIO());
  }

  /** Return the proper synthetic null indicator value for the given instance. */
  public Object getNullIndicatorValue(OpenJPAStateManager sm) {
    Column[] cols = cls.getEmbeddingMapping().getColumns();
    if (cols.length != 1) return null;
    if (sm == null && !cols[0].isNotNull()) return null;
    if (sm == null) return JavaSQLTypes.getEmptyValue(cols[0].getJavaType());
    return JavaSQLTypes.getNonEmptyValue(cols[0].getJavaType());
  }

  /** Return whether the given null indicator value means the object is null. */
  public boolean indicatesNull(Object val) {
    Column[] cols = cls.getEmbeddingMapping().getColumns();
    if (cols.length != 1) return false;
    if (val == null) return true;
    if (cols[0].isNotNull() && val.equals(JavaSQLTypes.getEmptyValue(cols[0].getJavaType())))
      return true;
    if (cols[0].getDefaultString() != null && val.toString().equals(cols[0].getDefaultString()))
      return true;
    return false;
  }

  public boolean isPrimaryKeyObjectId(boolean hasAll) {
    return cls.getEmbeddingMapping()
        .getFieldMapping()
        .getDefiningMapping()
        .isPrimaryKeyObjectId(hasAll);
  }
}
Пример #2
0
/**
 * Expression factory implementation that can be used to execute queries via SQL.
 *
 * @author Abe White
 * @nojavadoc
 */
public class JDBCExpressionFactory implements ExpressionFactory, Serializable {

  private static final Val NULL = new Null();

  private static final Localizer _loc = Localizer.forPackage(JDBCExpressionFactory.class);

  private final ClassMapping _type;
  private final SelectConstructor _cons = new SelectConstructor();
  private int _getMapValueAlias = 0;

  private boolean _isBooleanLiteralAsNumeric = true;

  /** Constructor. Supply the type we're querying against. */
  public JDBCExpressionFactory(ClassMapping type) {
    _type = type;
  }

  public void setBooleanLiteralAsNumeric(boolean isBooleanLiteralAsNumeric) {
    _isBooleanLiteralAsNumeric = isBooleanLiteralAsNumeric;
  }

  /** Use to create SQL select. */
  public SelectConstructor getSelectConstructor() {
    return _cons;
  }

  public Expression emptyExpression() {
    return new EmptyExpression();
  }

  public Expression asExpression(Value v) {
    return equal(v, newLiteral(Boolean.TRUE, Literal.TYPE_BOOLEAN));
  }

  public Expression equal(Value v1, Value v2) {
    // if we're comparing an unaccessed bound variable, like in:
    // coll.contains (var) && var == x, then translate into:
    // coll.contains (x)
    if (v1 instanceof PCPath && ((PCPath) v1).isUnaccessedVariable()) return contains(v1, v2);
    if (v2 instanceof PCPath && ((PCPath) v2).isUnaccessedVariable()) return contains(v2, v1);
    if (v1 instanceof Type || v2 instanceof Type) {
      Value val = v1 instanceof Type ? v1 : v2;
      verifyTypeOperation(val, null, false);
      return new EqualTypeExpression((Val) v1, (Val) v2);
    }
    return new EqualExpression((Val) v1, (Val) v2);
  }

  private void verifyTypeOperation(Value val, Value param, boolean isNotEqual) {
    if (val.getPath() == null) return;
    PCPath path = (PCPath) val.getPath();
    Discriminator disc = ((Type) val).getDiscriminator();
    if (disc == null
        || !(val.getMetaData().getPCSuperclass() != null
            || val.getMetaData().getPCSubclasses().length > 0))
      throw new UserException(
          _loc.get(
              "invalid-type-argument",
              path.last() != null ? path.getPCPathString() : path.getSchemaAlias()));

    if (disc.getColumns().length == 0) {
      if (disc.getStrategy() instanceof NoneDiscriminatorStrategy) {
        // limited support for table per class inheritance hierarchy
        if (path.last() != null)
          throw new UserException(_loc.get("type-argument-unsupported", path.last().getName()));
        if (isNotEqual) {
          if (param != null && param instanceof Null)
            throw new UserException(
                _loc.get("type-in-expression-unsupported", path.getSchemaAlias()));
          else
            throw new UserException(_loc.get("type-not-equal-unsupported", path.getSchemaAlias()));
        }
      }
      if (param != null && param instanceof CollectionParam)
        throw new UserException(_loc.get("collection-param-unsupported"));
    }
  }

  public Expression notEqual(Value v1, Value v2) {
    if (v1 instanceof Type || v2 instanceof Type) {
      Value val = v1 instanceof Type ? v1 : v2;
      Value param = val == v1 ? (v2 instanceof Null ? v2 : null) : (v1 instanceof Null ? v1 : null);
      verifyTypeOperation(val, param, true);
      return new NotEqualTypeExpression((Val) v1, (Val) v2);
    }
    return new NotEqualExpression((Val) v1, (Val) v2);
  }

  public Expression lessThan(Value v1, Value v2) {
    return new CompareExpression((Val) v1, (Val) v2, CompareExpression.LESS);
  }

  public Expression greaterThan(Value v1, Value v2) {
    return new CompareExpression((Val) v1, (Val) v2, CompareExpression.GREATER);
  }

  public Expression lessThanEqual(Value v1, Value v2) {
    return new CompareExpression((Val) v1, (Val) v2, CompareExpression.LESS_EQUAL);
  }

  public Expression greaterThanEqual(Value v1, Value v2) {
    return new CompareExpression((Val) v1, (Val) v2, CompareExpression.GREATER_EQUAL);
  }

  public Expression isEmpty(Value val) {
    return new IsEmptyExpression((Val) val);
  }

  public Expression isNotEmpty(Value val) {
    return new IsNotEmptyExpression((Val) val);
  }

  public Expression contains(Value map, Value arg) {
    if (map instanceof Const) {
      if (arg instanceof Type) {
        // limited support for table per class inheritance
        verifyTypeOperation(arg, map, false);
        if (((ClassMapping) arg.getMetaData()).getDiscriminator().getColumns().length == 0)
          return new EqualTypeExpression((Val) arg, (Val) map);
      }

      return new InExpression((Val) arg, (Const) map);
    }
    if (map instanceof SubQ) return new InSubQExpression((Val) arg, (SubQ) map);
    return new ContainsExpression((Val) map, (Val) arg);
  }

  public Expression containsKey(Value map, Value arg) {
    if (map instanceof Const) return new InKeyExpression((Val) arg, (Const) map);
    return new ContainsKeyExpression((Val) map, (Val) arg);
  }

  public Expression containsValue(Value map, Value arg) {
    if (map instanceof Const) return new InValueExpression((Val) arg, (Const) map);
    return new ContainsExpression((Val) map, (Val) arg);
  }

  public Expression isInstance(Value val, Class c) {
    if (val instanceof Const) return new ConstInstanceofExpression((Const) val, c);
    return new InstanceofExpression((PCPath) val, c);
  }

  public Expression and(Expression exp1, Expression exp2) {
    if (exp1 instanceof BindVariableExpression)
      return new BindVariableAndExpression((BindVariableExpression) exp1, (Exp) exp2);
    if (exp2 instanceof BindVariableExpression)
      return new BindVariableAndExpression((BindVariableExpression) exp2, (Exp) exp1);
    return new AndExpression((Exp) exp1, (Exp) exp2);
  }

  public Expression or(Expression exp1, Expression exp2) {
    return new OrExpression((Exp) exp1, (Exp) exp2);
  }

  public Expression not(Expression exp) {
    if (!(exp instanceof IsNotEmptyExpression)
        && !(exp instanceof InSubQExpression)
        && HasContainsExpressionVisitor.hasContains(exp))
      return new NotContainsExpression((Exp) exp);
    return new NotExpression((Exp) exp);
  }

  public Expression bindVariable(Value var, Value val) {
    // handle the strange case of using a constant path to bind a
    // variable; in these cases the variable acts like an unbound
    // variable that we limit by using an IN clause on the constant
    // value collection
    if (val instanceof Const) {
      PCPath path = new PCPath(_type, (Variable) var);
      path.setMetaData(var.getMetaData());
      return new InExpression(path, (Const) val);
    }
    return new BindVariableExpression((Variable) var, (PCPath) val, false);
  }

  public Expression bindKeyVariable(Value var, Value val) {
    // handle the strange case of using a constant path to bind a
    // variable; in these cases the variable acts like an unbound
    // variable that we limit by using an IN clause on the constant
    // value collection
    if (val instanceof Const) {
      PCPath path = new PCPath(_type, (Variable) var);
      path.setMetaData(var.getMetaData());
      return new InKeyExpression(path, (Const) val);
    }
    return new BindVariableExpression((Variable) var, (PCPath) val, true);
  }

  public Expression bindValueVariable(Value var, Value val) {
    return bindVariable(var, val);
  }

  public Expression startsWith(Value v1, Value v2) {
    return new StartsWithExpression((Val) v1, (Val) v2);
  }

  public Expression endsWith(Value v1, Value v2) {
    return new EndsWithExpression((Val) v1, (Val) v2);
  }

  public Expression notMatches(Value v1, Value v2, String single, String multi, String esc) {
    return not(matches(v1, v2, single, multi, esc));
  }

  public Expression matches(Value v1, Value v2, String single, String multi, String esc) {
    if (!(v2 instanceof Const)) throw new UserException(_loc.get("const-only", "matches"));
    if (esc == null
        && _type.getMappingRepository().getDBDictionary().requiresSearchStringEscapeForLike
            == true) {
      esc = _type.getMappingRepository().getDBDictionary().searchStringEscape;
    }
    return new MatchesExpression((Val) v1, (Const) v2, single, multi, esc);
  }

  public Subquery newSubquery(ClassMetaData candidate, boolean subs, String alias) {
    DBDictionary dict = _type.getMappingRepository().getDBDictionary();
    dict.assertSupport(dict.supportsSubselect, "SupportsSubselect");
    return new SubQ((ClassMapping) candidate, subs, alias);
  }

  public Path newPath() {
    return new PCPath(_type);
  }

  public Path newPath(Value val) {
    if (val instanceof Const) return new ConstPath((Const) val);
    if (val instanceof SubQ) return new PCPath((SubQ) val);
    return new PCPath(_type, (Variable) val);
  }

  public Literal newLiteral(Object val, int ptype) {
    return new Lit(val, ptype);
  }

  public Literal newTypeLiteral(Object val, int ptype) {
    return new TypeLit(val, ptype);
  }

  public Value getThis() {
    return new PCPath(_type);
  }

  public Value getNull() {
    return NULL;
  }

  public <T extends Date> Value getCurrentDate(Class<T> dateType) {
    return new CurrentDate(dateType);
  }

  public <T extends Date> Value getCurrentTime(Class<T> dateType) {
    return new CurrentDate(dateType);
  }

  public <T extends Date> Value getCurrentTimestamp(Class<T> dateType) {
    return new CurrentDate(dateType);
  }

  public Parameter newParameter(Object name, Class type) {
    return new Param(name, type);
  }

  public Parameter newCollectionValuedParameter(Object key, Class type) {
    return new CollectionParam(key, type);
  }

  public Value newExtension(FilterListener listener, Value target, Value arg) {
    return new Extension((JDBCFilterListener) listener, (Val) target, (Val) arg, _type);
  }

  public Value newAggregate(AggregateListener listener, Value arg) {
    return new Aggregate((JDBCAggregateListener) listener, (Val) arg, _type);
  }

  public Arguments newArgumentList(Value v1, Value v2) {
    return new Args((Val) v1, (Val) v2);
  }

  public Arguments newArgumentList(Value... vs) {
    if (vs == null) return new Args(null);
    Val[] vals = new Val[vs.length];
    int i = 0;
    for (Value v : vs) {
      vals[i++] = (Val) v;
    }
    return new Args(vals);
  }

  public Value newUnboundVariable(String name, Class type) {
    return new Variable(name, type);
  }

  public Value newBoundVariable(String name, Class type) {
    return newUnboundVariable(name, type);
  }

  public Value cast(Value val, Class cls) {
    val.setImplicitType(cls);
    return val;
  }

  public Value add(Value v1, Value v2) {
    return new Math((Val) v1, (Val) v2, Math.ADD);
  }

  public Value subtract(Value v1, Value v2) {
    return new Math((Val) v1, (Val) v2, Math.SUBTRACT);
  }

  public Value multiply(Value v1, Value v2) {
    return new Math((Val) v1, (Val) v2, Math.MULTIPLY);
  }

  public Value divide(Value v1, Value v2) {
    return new Math((Val) v1, (Val) v2, Math.DIVIDE);
  }

  public Value mod(Value v1, Value v2) {
    return new Math((Val) v1, (Val) v2, Math.MOD);
  }

  public Value abs(Value val) {
    return new Abs((Val) val);
  }

  public Value indexOf(Value v1, Value v2) {
    return new IndexOf((Val) v1, (Val) v2);
  }

  public Value concat(Value v1, Value v2) {
    return new Concat((Val) v1, (Val) v2);
  }

  public Value stringLength(Value str) {
    return new StringLength((Val) str);
  }

  public Value trim(Value str, Value trimChar, Boolean where) {
    return new Trim((Val) str, (Val) trimChar, where);
  }

  public Value sqrt(Value val) {
    return new Sqrt((Val) val);
  }

  public Value substring(Value v1, Value v2) {
    return new Substring((Val) v1, (Val) v2);
  }

  public Value toUpperCase(Value val) {
    return new ToUpperCase((Val) val);
  }

  public Value toLowerCase(Value val) {
    return new ToLowerCase((Val) val);
  }

  public Value avg(Value val) {
    return new Avg((Val) val);
  }

  public Value count(Value val) {
    return new Count((Val) val);
  }

  public Value distinct(Value val) {
    return new Distinct((Val) val);
  }

  public Value max(Value val) {
    return new Max((Val) val);
  }

  public Value min(Value val) {
    return new Min((Val) val);
  }

  public Value sum(Value val) {
    return new Sum((Val) val);
  }

  public Value any(Value val) {
    return new Any((Val) val);
  }

  public Value all(Value val) {
    return new All((Val) val);
  }

  public Value size(Value val) {
    return new Size((Val) val);
  }

  public Value index(Value val) {
    ((PCPath) val).verifyIndexedField();
    return new Index((Val) val);
  }

  public Value type(Value val) {
    return new Type((Val) val);
  }

  public Value mapEntry(Value key, Value val) {
    return new MapEntry((Val) key, (Val) val);
  }

  public Value mapKey(Value key, Value val) {
    return new MapKey((Val) key);
  }

  public Value getKey(Value val) {
    ((PCPath) val).getKey();
    return val;
  }

  public Value getObjectId(Value val) {
    if (val instanceof Const) return new ConstGetObjectId((Const) val);
    return new GetObjectId((PCPath) val);
  }

  public Value getMapValue(Value map, Value arg) {
    return new GetMapValue((Val) map, (Val) arg, "gmv" + _getMapValueAlias++);
  }

  private Value getLiteralRawString(Value val) {
    if (val instanceof Lit) {
      Lit lit = (Lit) val;
      StringBuilder value = new StringBuilder();
      int pType = lit.getParseType();
      if (pType == Literal.TYPE_SQ_STRING || pType == Literal.TYPE_STRING)
        value.append("'").append(lit.getValue().toString()).append("'");
      else if (pType == Literal.TYPE_BOOLEAN) {
        Boolean boolVal = (Boolean) lit.getValue();
        if (_isBooleanLiteralAsNumeric) value.append(boolVal ? "1" : "0");
        else value.append(boolVal ? "true" : "false");
      } else if (pType == Literal.TYPE_ENUM) {
        lit.setRaw(true);
        return val;
      } else value.append(lit.getValue().toString());
      lit.setValue(new Raw(value.toString()));
      return lit;
    }
    return val;
  }

  public Value simpleCaseExpression(Value caseOperand, Expression[] exp, Value val1) {
    Exp[] exps = new Exp[exp.length];
    for (int i = 0; i < exp.length; i++) exps[i] = (Exp) exp[i];
    val1 = getLiteralRawString(val1);
    return new SimpleCaseExpression((Val) caseOperand, exps, (Val) val1);
  }

  public Value generalCaseExpression(Expression[] exp, Value val) {
    Exp[] exps = new Exp[exp.length];
    for (int i = 0; i < exp.length; i++) exps[i] = (Exp) exp[i];
    val = getLiteralRawString(val);
    return new GeneralCaseExpression(exps, (Val) val);
  }

  public Expression whenCondition(Expression exp, Value val) {
    val = getLiteralRawString(val);
    return new WhenCondition((Exp) exp, (Val) val);
  }

  public Expression whenScalar(Value val1, Value val2) {
    val1 = getLiteralRawString(val1);
    val2 = getLiteralRawString(val2);
    return new WhenScalar((Val) val1, (Val) val2);
  }

  public Value coalesceExpression(Value[] vals) {
    ;
    Object[] values = new Val[vals.length];
    for (int i = 0; i < vals.length; i++) {
      values[i] = getLiteralRawString(vals[i]);
    }
    return new CoalesceExpression((Val[]) values);
  }

  public Value nullIfExpression(Value val1, Value val2) {
    val1 = getLiteralRawString(val1);
    val2 = getLiteralRawString(val2);
    return new NullIfExpression((Val) val1, (Val) val2);
  }

  public Value newFunction(String functionName, Class<?> resultType, Value... args) {
    return new DatastoreFunction(functionName, resultType, newArgumentList(args));
  }

  public boolean isVerticalType(Value val) {
    if (!(val instanceof Type)) return false;
    ClassMapping cm = (ClassMapping) ((Type) val).getMetaData();
    String strat = cm.getMappingInfo().getHierarchyStrategy();
    if (strat != null && strat.equals(VerticalClassStrategy.ALIAS)) return true;
    return false;
  }
}
/**
 * Allows configuration and optimization of how objects are loaded from the data store.
 *
 * @since 0.3.0
 * @author Abe White
 * @author Pinaki Poddar
 * @nojavadoc
 */
@SuppressWarnings("serial")
public class FetchConfigurationImpl implements FetchConfiguration, Cloneable {

  private static final Localizer _loc = Localizer.forPackage(FetchConfigurationImpl.class);
  private static Map<String, Method> _hintSetters = new HashMap<String, Method>();

  /**
   * Registers hint keys that have a corresponding setter method. The hint keys are registered in
   * <code>openjpa.FetchPlan</code> and <code>openjpa</code> as prefix. Also some keys are
   * registered in <code>javax.persistence</code> namespace.
   */
  static {
    String[] prefixes = {"openjpa.FetchPlan", "openjpa"};
    Class<?> target = FetchConfiguration.class;
    populateHintSetter(target, "ExtendedPathLookup", boolean.class, prefixes);
    populateHintSetter(target, "FetchBatchSize", int.class, prefixes);
    populateHintSetter(target, "FlushBeforeQueries", int.class, prefixes);
    populateHintSetter(target, "LockScope", int.class, prefixes);
    populateHintSetter(target, "LockTimeout", int.class, prefixes);
    populateHintSetter(target, "setLockTimeout", "timeout", int.class, "javax.persistence.lock");
    populateHintSetter(target, "MaxFetchDepth", int.class, prefixes);
    populateHintSetter(target, "QueryTimeout", int.class, prefixes);
    populateHintSetter(target, "setQueryTimeout", "timeout", int.class, "javax.persistence.query");
    populateHintSetter(target, "ReadLockLevel", int.class, prefixes);
    populateHintSetter(target, "setReadLockLevel", "ReadLockMode", int.class, prefixes);
    populateHintSetter(target, "WriteLockLevel", int.class, prefixes);
    populateHintSetter(target, "setWriteLockLevel", "WriteLockMode", int.class, prefixes);
  }

  /**
   * Populate static registry of hints.
   *
   * @param target The name of the target class that will receive this hint.
   * @param hint the simple name of the hint without a prefix.
   * @param type the value argument type of the target setter method.
   * @param prefixes the prefixes will be added to the simple hint name.
   */
  protected static void populateHintSetter(
      Class<?> target, String hint, Class<?> type, String... prefixes) {
    populateHintSetter(target, "set" + hint, hint, type, prefixes);
  }

  /**
   * Populate static registry of hints.
   *
   * @param target The name of the target class that will receive this hint.
   * @param method The name of the method in the target class that will receive this hint.
   * @param hint the simple name of the hint without a prefix.
   * @param type the value argument type of the target setter method.
   * @param prefixes the prefixes will be added to the simple hint name.
   */
  protected static void populateHintSetter(
      Class<?> target, String method, String hint, Class<?> type, String... prefixes) {
    try {
      Method setter = target.getMethod(method, type);
      for (String prefix : prefixes) {
        _hintSetters.put(prefix + "." + hint, setter);
      }
    } catch (Exception e) {
      // should not reach
      throw new InternalException(
          "setter for " + hint + " with argument " + type + " does not exist");
    }
  }

  /** Configurable state shared throughout a traversal chain. */
  protected static class ConfigurationState implements Serializable {
    public transient StoreContext ctx = null;
    public int fetchBatchSize = 0;
    public int maxFetchDepth = 1;
    public boolean queryCache = true;
    public int flushQuery = 0;
    public int lockTimeout = -1;
    public int queryTimeout = -1;
    public int lockScope = LOCKSCOPE_NORMAL;
    public int readLockLevel = LOCK_NONE;
    public int writeLockLevel = LOCK_NONE;
    public Set<String> fetchGroups = null;
    public Set<String> fields = null;
    public Set<Class<?>> rootClasses;
    public Set<Object> rootInstances;
    public Map<String, Object> hints = null;
    public boolean fetchGroupContainsDefault = false;
    public boolean fetchGroupContainsAll = false;
    public boolean extendedPathLookup = false;
    public DataCacheRetrieveMode cacheRetrieveMode = DataCacheRetrieveMode.USE;
    public DataCacheStoreMode cacheStoreMode = DataCacheStoreMode.USE;
  }

  private final ConfigurationState _state;
  private FetchConfigurationImpl _parent;
  private String _fromField;
  private Class<?> _fromType;
  private String _directRelationOwner;
  private boolean _load = true;
  private int _availableRecursion;
  private int _availableDepth;

  public FetchConfigurationImpl() {
    this(null);
  }

  protected FetchConfigurationImpl(ConfigurationState state) {
    _state = (state == null) ? new ConfigurationState() : state;
    _availableDepth = _state.maxFetchDepth;
  }

  public StoreContext getContext() {
    return _state.ctx;
  }

  public void setContext(StoreContext ctx) {
    // can't reset non-null context to another context
    if (ctx != null && _state.ctx != null && ctx != _state.ctx) throw new InternalException();
    _state.ctx = ctx;
    if (ctx == null) return;

    // initialize to conf info
    OpenJPAConfiguration conf = ctx.getConfiguration();
    setFetchBatchSize(conf.getFetchBatchSize());
    setFlushBeforeQueries(conf.getFlushBeforeQueriesConstant());
    setLockTimeout(conf.getLockTimeout());
    setQueryTimeout(conf.getQueryTimeout());
    clearFetchGroups();
    addFetchGroups(Arrays.asList(conf.getFetchGroupsList()));
    setMaxFetchDepth(conf.getMaxFetchDepth());
  }

  /** Clone this instance. */
  public Object clone() {
    FetchConfigurationImpl clone = newInstance(null);
    clone._state.ctx = _state.ctx;
    clone._parent = _parent;
    clone._fromField = _fromField;
    clone._fromType = _fromType;
    clone._directRelationOwner = _directRelationOwner;
    clone._load = _load;
    clone._availableRecursion = _availableRecursion;
    clone._availableDepth = _availableDepth;
    clone.copy(this);
    return clone;
  }

  /** Return a new hollow instance. */
  protected FetchConfigurationImpl newInstance(ConfigurationState state) {
    return new FetchConfigurationImpl(state);
  }

  public void copy(FetchConfiguration fetch) {
    setFetchBatchSize(fetch.getFetchBatchSize());
    setMaxFetchDepth(fetch.getMaxFetchDepth());
    setQueryCacheEnabled(fetch.getQueryCacheEnabled());
    setFlushBeforeQueries(fetch.getFlushBeforeQueries());
    setExtendedPathLookup(fetch.getExtendedPathLookup());
    setLockTimeout(fetch.getLockTimeout());
    setQueryTimeout(fetch.getQueryTimeout());
    setLockScope(fetch.getLockScope());
    clearFetchGroups();
    addFetchGroups(fetch.getFetchGroups());
    clearFields();
    copyHints(fetch);
    setCacheRetrieveMode(fetch.getCacheRetrieveMode());
    setCacheStoreMode(fetch.getCacheStoreMode());
    addFields(fetch.getFields());

    // don't use setters because require active transaction
    _state.readLockLevel = fetch.getReadLockLevel();
    _state.writeLockLevel = fetch.getWriteLockLevel();
  }

  void copyHints(FetchConfiguration fetch) {
    if (fetch instanceof FetchConfigurationImpl == false) return;
    FetchConfigurationImpl from = (FetchConfigurationImpl) fetch;
    if (from._state == null || from._state.hints == null) return;
    if (this._state == null) return;
    if (this._state.hints == null) this._state.hints = new HashMap<String, Object>();
    this._state.hints.putAll(from._state.hints);
  }

  public int getFetchBatchSize() {
    return _state.fetchBatchSize;
  }

  public FetchConfiguration setFetchBatchSize(int fetchBatchSize) {
    if (fetchBatchSize == DEFAULT && _state.ctx != null)
      fetchBatchSize = _state.ctx.getConfiguration().getFetchBatchSize();
    if (fetchBatchSize != DEFAULT) _state.fetchBatchSize = fetchBatchSize;
    return this;
  }

  public int getMaxFetchDepth() {
    return _state.maxFetchDepth;
  }

  public FetchConfiguration setMaxFetchDepth(int depth) {
    if (depth == DEFAULT && _state.ctx != null)
      depth = _state.ctx.getConfiguration().getMaxFetchDepth();
    if (depth != DEFAULT) {
      _state.maxFetchDepth = depth;
      if (_parent == null) _availableDepth = depth;
    }
    return this;
  }

  public boolean getQueryCacheEnabled() {
    return _state.queryCache;
  }

  public FetchConfiguration setQueryCacheEnabled(boolean cache) {
    _state.queryCache = cache;
    return this;
  }

  public int getFlushBeforeQueries() {
    return _state.flushQuery;
  }

  public boolean getExtendedPathLookup() {
    return _state.extendedPathLookup;
  }

  public FetchConfiguration setExtendedPathLookup(boolean flag) {
    _state.extendedPathLookup = flag;
    return this;
  }

  public FetchConfiguration setFlushBeforeQueries(int flush) {
    if (flush != DEFAULT
        && flush != QueryFlushModes.FLUSH_TRUE
        && flush != QueryFlushModes.FLUSH_FALSE
        && flush != QueryFlushModes.FLUSH_WITH_CONNECTION)
      throw new IllegalArgumentException(
          _loc.get("bad-flush-before-queries", Integer.valueOf(flush)).getMessage());

    if (flush == DEFAULT && _state.ctx != null)
      _state.flushQuery = _state.ctx.getConfiguration().getFlushBeforeQueriesConstant();
    else if (flush != DEFAULT) _state.flushQuery = flush;
    return this;
  }

  public Set<String> getFetchGroups() {
    if (_state.fetchGroups == null) return Collections.emptySet();
    return _state.fetchGroups;
  }

  public boolean hasFetchGroup(String group) {
    return _state.fetchGroups != null
        && (_state.fetchGroups.contains(group) || _state.fetchGroups.contains(FetchGroup.NAME_ALL));
  }

  public boolean hasFetchGroupDefault() {
    // Fetch group All includes fetch group Default by definition
    return _state.fetchGroupContainsDefault || _state.fetchGroupContainsAll;
  }

  public boolean hasFetchGroupAll() {
    return _state.fetchGroupContainsAll;
  }

  public FetchConfiguration addFetchGroup(String name) {
    if (StringUtils.isEmpty(name)) throw new UserException(_loc.get("null-fg"));

    lock();
    try {
      if (_state.fetchGroups == null) _state.fetchGroups = new HashSet<String>();
      _state.fetchGroups.add(name);
      if (FetchGroup.NAME_ALL.equals(name)) _state.fetchGroupContainsAll = true;
      else if (FetchGroup.NAME_DEFAULT.equals(name)) _state.fetchGroupContainsDefault = true;
    } finally {
      unlock();
    }
    return this;
  }

  public FetchConfiguration addFetchGroups(Collection<String> groups) {
    if (groups == null || groups.isEmpty()) return this;
    for (String group : groups) addFetchGroup(group);
    return this;
  }

  public FetchConfiguration removeFetchGroup(String group) {
    lock();
    try {
      if (_state.fetchGroups != null) {
        _state.fetchGroups.remove(group);
        if (FetchGroup.NAME_ALL.equals(group)) _state.fetchGroupContainsAll = false;
        else if (FetchGroup.NAME_DEFAULT.equals(group)) _state.fetchGroupContainsDefault = false;
      }
    } finally {
      unlock();
    }
    return this;
  }

  public FetchConfiguration removeFetchGroups(Collection<String> groups) {
    lock();
    try {
      if (_state.fetchGroups != null && groups != null)
        for (String group : groups) removeFetchGroup(group);
    } finally {
      unlock();
    }
    return this;
  }

  public FetchConfiguration clearFetchGroups() {
    lock();
    try {
      if (_state.fetchGroups != null) {
        _state.fetchGroups.clear();
        _state.fetchGroupContainsAll = false;
        _state.fetchGroupContainsDefault = true;
      }
    } finally {
      unlock();
    }
    return this;
  }

  public FetchConfiguration resetFetchGroups() {
    clearFetchGroups();
    if (_state.ctx != null)
      addFetchGroups(Arrays.asList(_state.ctx.getConfiguration().getFetchGroupsList()));
    return this;
  }

  public Set<String> getFields() {
    if (_state.fields == null) return Collections.emptySet();
    return _state.fields;
  }

  public boolean hasField(String field) {
    return _state.fields != null && _state.fields.contains(field);
  }

  public FetchConfiguration addField(String field) {
    if (StringUtils.isEmpty(field)) throw new UserException(_loc.get("null-field"));

    lock();
    try {
      if (_state.fields == null) _state.fields = new HashSet<String>();
      _state.fields.add(field);
    } finally {
      unlock();
    }
    return this;
  }

  public FetchConfiguration addFields(Collection<String> fields) {
    if (fields == null || fields.isEmpty()) return this;

    lock();
    try {
      if (_state.fields == null) _state.fields = new HashSet<String>();
      _state.fields.addAll(fields);
    } finally {
      unlock();
    }
    return this;
  }

  public FetchConfiguration removeField(String field) {
    lock();
    try {
      if (_state.fields != null) _state.fields.remove(field);
    } finally {
      unlock();
    }
    return this;
  }

  public FetchConfiguration removeFields(Collection<String> fields) {
    lock();
    try {
      if (_state.fields != null) _state.fields.removeAll(fields);
    } finally {
      unlock();
    }
    return this;
  }

  public FetchConfiguration clearFields() {
    lock();
    try {
      if (_state.fields != null) _state.fields.clear();
    } finally {
      unlock();
    }
    return this;
  }

  public DataCacheRetrieveMode getCacheRetrieveMode() {
    return _state.cacheRetrieveMode;
  }

  public DataCacheStoreMode getCacheStoreMode() {
    return _state.cacheStoreMode;
  }

  public void setCacheRetrieveMode(DataCacheRetrieveMode mode) {
    _state.cacheRetrieveMode = mode;
  }

  public void setCacheStoreMode(DataCacheStoreMode mode) {
    _state.cacheStoreMode = mode;
  }

  public int getLockTimeout() {
    return _state.lockTimeout;
  }

  public FetchConfiguration setLockTimeout(int timeout) {
    if (timeout == DEFAULT && _state.ctx != null)
      _state.lockTimeout = _state.ctx.getConfiguration().getLockTimeout();
    else if (timeout != DEFAULT) {
      if (timeout < -1) {
        throw new IllegalArgumentException(_loc.get("invalid-timeout", timeout).getMessage());
      } else {
        _state.lockTimeout = timeout;
      }
    }
    return this;
  }

  public int getQueryTimeout() {
    return _state.queryTimeout;
  }

  public FetchConfiguration setQueryTimeout(int timeout) {
    if (timeout == DEFAULT && _state.ctx != null)
      _state.queryTimeout = _state.ctx.getConfiguration().getQueryTimeout();
    else if (timeout != DEFAULT) {
      if (timeout < -1) {
        throw new IllegalArgumentException(_loc.get("invalid-timeout", timeout).getMessage());
      } else {
        _state.queryTimeout = timeout;
      }
    }
    return this;
  }

  public int getLockScope() {
    return _state.lockScope;
  }

  public FetchConfiguration setLockScope(int scope) {
    if (scope != DEFAULT
        && scope != LockScopes.LOCKSCOPE_NORMAL
        && scope != LockScopes.LOCKSCOPE_EXTENDED)
      throw new IllegalArgumentException(
          _loc.get("bad-lock-scope", Integer.valueOf(scope)).getMessage());
    if (scope == DEFAULT) _state.lockScope = LOCKSCOPE_NORMAL;
    else _state.lockScope = scope;
    return this;
  }

  public int getReadLockLevel() {
    return _state.readLockLevel;
  }

  public FetchConfiguration setReadLockLevel(int level) {
    if (_state.ctx == null) return this;

    if (level != DEFAULT
        && level != MixedLockLevels.LOCK_NONE
        && level != MixedLockLevels.LOCK_READ
        && level != MixedLockLevels.LOCK_OPTIMISTIC
        && level != MixedLockLevels.LOCK_WRITE
        && level != MixedLockLevels.LOCK_OPTIMISTIC_FORCE_INCREMENT
        && level != MixedLockLevels.LOCK_PESSIMISTIC_READ
        && level != MixedLockLevels.LOCK_PESSIMISTIC_WRITE
        && level != MixedLockLevels.LOCK_PESSIMISTIC_FORCE_INCREMENT)
      throw new IllegalArgumentException(
          _loc.get("bad-lock-level", Integer.valueOf(level)).getMessage());

    lock();
    try {
      if (level != MixedLockLevels.LOCK_NONE) assertActiveTransaction();
      if (level == DEFAULT)
        _state.readLockLevel = _state.ctx.getConfiguration().getReadLockLevelConstant();
      else _state.readLockLevel = level;
    } finally {
      unlock();
    }
    return this;
  }

  public int getWriteLockLevel() {
    return _state.writeLockLevel;
  }

  public FetchConfiguration setWriteLockLevel(int level) {
    if (_state.ctx == null) return this;

    if (level != DEFAULT
        && level != MixedLockLevels.LOCK_NONE
        && level != MixedLockLevels.LOCK_READ
        && level != MixedLockLevels.LOCK_OPTIMISTIC
        && level != MixedLockLevels.LOCK_WRITE
        && level != MixedLockLevels.LOCK_OPTIMISTIC_FORCE_INCREMENT
        && level != MixedLockLevels.LOCK_PESSIMISTIC_READ
        && level != MixedLockLevels.LOCK_PESSIMISTIC_WRITE
        && level != MixedLockLevels.LOCK_PESSIMISTIC_FORCE_INCREMENT)
      throw new IllegalArgumentException(
          _loc.get("bad-lock-level", Integer.valueOf(level)).getMessage());

    lock();
    try {
      assertActiveTransaction();
      if (level == DEFAULT)
        _state.writeLockLevel = _state.ctx.getConfiguration().getWriteLockLevelConstant();
      else _state.writeLockLevel = level;
    } finally {
      unlock();
    }
    return this;
  }

  public ResultList<?> newResultList(ResultObjectProvider rop) {
    if (rop instanceof ListResultObjectProvider) return new SimpleResultList(rop);
    if (_state.fetchBatchSize < 0) return new EagerResultList(rop);
    if (rop.supportsRandomAccess()) return new SimpleResultList(rop);
    return new WindowResultList(rop);
  }

  /** Throw an exception if no transaction is active. */
  private void assertActiveTransaction() {
    if (!isActiveTransaction()) throw new NoTransactionException(_loc.get("not-active"));
  }

  private boolean isActiveTransaction() {
    return (_state.ctx != null && _state.ctx.isActive());
  }

  /**
   * Gets the current hints set on this receiver. The values designate the actual value specified by
   * the caller and not the values that may have been actually set on the state variables of this
   * receiver.
   */
  public Map<String, Object> getHints() {
    if (_state.hints == null) return Collections.emptyMap();
    return Collections.unmodifiableMap(_state.hints);
  }

  /** Affirms if the given key is set as a hint. */
  public boolean isHintSet(String key) {
    return _state.hints != null && _state.hints.containsKey(key);
  }

  /** Removes the given keys and their hint value. */
  public void removeHint(String... keys) {
    if (keys == null || _state.hints == null) return;
    for (String key : keys) {
      _state.hints.remove(key);
    }
  }

  public Collection<String> getSupportedHints() {
    return _hintSetters.keySet();
  }

  /**
   * Same as <code>setHint(key, value, value)</code>.
   *
   * @see #setHint(String, Object, Object)
   */
  public void setHint(String key, Object value) {
    setHint(key, value, value);
  }

  /**
   * Sets the hint to the given value. If the key corresponds to a known key, then that value is set
   * via the setter method. Otherwise it is put into opaque hints map. <br>
   * In either case, the original value is put in the hints map. So essential difference between
   * setting a value directly by a setter and via a hint is the memory of this original value. <br>
   * The other important difference is setting lock levels. Setting of lock level via setter method
   * needs active transaction. But setting via hint does not.
   *
   * @param key a hint key. If it is one of the statically registered hint key then the setter is
   *     called.
   * @param value to be set. The given value type must match the argument type of the setter, if one
   *     exists.
   * @param original value as specified by the caller. This value is put in the hints map.
   * @exception IllegalArgumentException if the given value is not acceptable by the setter method,
   *     if one exists corresponds the given hint key.
   */
  public void setHint(String key, Object value, Object original) {
    if (key == null) return;
    if (_hintSetters.containsKey(key)) {
      Method setter = _hintSetters.get(key);
      String methodName = setter.getName();
      try {
        if ("setReadLockLevel".equals(methodName) && !isActiveTransaction()) {
          _state.readLockLevel = (Integer) value;
        } else if ("setWriteLockLevel".equals(methodName) && !isActiveTransaction()) {
          _state.writeLockLevel = (Integer) value;
        } else {
          setter.invoke(this, Filters.convertToMatchMethodArgument(value, setter));
        }
      } catch (Exception e) {
        String message =
            _loc.get("bad-hint-value", key, toString(value), toString(original)).getMessage();
        if (e instanceof IllegalArgumentException) {
          throw new IllegalArgumentException(message);
        }
        throw new IllegalArgumentException(message, e);
      }
    }
    addHint(key, original);
  }

  private void addHint(String name, Object value) {
    lock();
    try {
      if (_state.hints == null) _state.hints = new HashMap<String, Object>();
      _state.hints.put(name, value);
    } finally {
      unlock();
    }
  }

  public Object getHint(String name) {
    return (_state.hints == null) ? null : _state.hints.get(name);
  }

  public Object removeHint(String name) {
    return (_state.hints == null) ? null : _state.hints.remove(name);
  }

  public Set<Class<?>> getRootClasses() {
    if (_state.rootClasses == null) return Collections.emptySet();
    return _state.rootClasses;
  }

  public FetchConfiguration setRootClasses(Collection<Class<?>> classes) {
    lock();
    try {
      if (_state.rootClasses != null) _state.rootClasses.clear();
      if (classes != null && !classes.isEmpty()) {
        if (_state.rootClasses == null) _state.rootClasses = new HashSet<Class<?>>(classes);
        else _state.rootClasses.addAll(classes);
      }
    } finally {
      unlock();
    }
    return this;
  }

  public Set<Object> getRootInstances() {
    if (_state.rootInstances == null) return Collections.emptySet();
    return _state.rootInstances;
  }

  public FetchConfiguration setRootInstances(Collection<?> instances) {
    lock();
    try {
      if (_state.rootInstances != null) _state.rootInstances.clear();
      if (instances != null && !instances.isEmpty()) {
        if (_state.rootInstances == null) {
          _state.rootInstances = new HashSet<Object>(instances);
        } else {
          _state.rootInstances.addAll(instances);
        }
      }
    } finally {
      unlock();
    }
    return this;
  }

  public void lock() {
    if (_state.ctx != null) _state.ctx.lock();
  }

  public void unlock() {
    if (_state.ctx != null) _state.ctx.unlock();
  }

  /////////////
  // Traversal
  /////////////

  public int requiresFetch(FieldMetaData fm) {
    if (!includes(fm)) return FETCH_NONE;

    Class<?> type = fm.getRelationType();
    if (type == null) return FETCH_LOAD;
    if (_availableDepth == 0) return FETCH_NONE;

    // we can skip calculating recursion depth if this is a top-level conf:
    // the field is in our fetch groups, so can't possibly not select
    if (_parent == null) return FETCH_LOAD;

    String fieldName = fm.getFullName(false);
    int rdepth = getAvailableRecursionDepth(fm, type, fieldName, false);
    if (rdepth != FetchGroup.DEPTH_INFINITE && rdepth <= 0) return FETCH_NONE;

    if (StringUtils.equals(_directRelationOwner, fieldName)) return FETCH_REF;
    return FETCH_LOAD;
  }

  public boolean requiresLoad() {
    return _load;
  }

  public FetchConfiguration traverse(FieldMetaData fm) {
    Class<?> type = fm.getRelationType();
    if (type == null) return this;

    FetchConfigurationImpl clone = newInstance(_state);
    clone._parent = this;
    clone._availableDepth = reduce(_availableDepth);
    clone._fromField = fm.getFullName(false);
    clone._fromType = type;
    clone._availableRecursion = getAvailableRecursionDepth(fm, type, fm.getFullName(false), true);
    if (StringUtils.equals(_directRelationOwner, fm.getFullName(false))) clone._load = false;
    else clone._load = _load;

    FieldMetaData owner = fm.getMappedByMetaData();
    if (owner != null && owner.getTypeCode() == JavaTypes.PC)
      clone._directRelationOwner = owner.getFullName(false);

    return clone;
  }

  /** Whether our configuration state includes the given field. */
  private boolean includes(FieldMetaData fmd) {
    if ((hasFetchGroupDefault() && fmd.isInDefaultFetchGroup())
        || hasFetchGroupAll()
        || hasField(fmd.getFullName(false))
        || hasExtendedLookupPath(fmd)) return true;
    String[] fgs = fmd.getCustomFetchGroups();
    for (int i = 0; i < fgs.length; i++) if (hasFetchGroup(fgs[i])) return true;
    return false;
  }

  private boolean hasExtendedLookupPath(FieldMetaData fmd) {
    return getExtendedPathLookup()
        && (hasField(fmd.getRealName())
            || (_fromField != null && hasField(_fromField + "." + fmd.getName())));
  }

  /**
   * Return the available recursion depth via the given field for the given type.
   *
   * @param traverse whether we're traversing the field
   */
  private int getAvailableRecursionDepth(
      FieldMetaData fm, Class<?> type, String fromField, boolean traverse) {
    // see if there's a previous limit
    int avail = Integer.MIN_VALUE;
    for (FetchConfigurationImpl f = this; f != null; f = f._parent) {
      if (StringUtils.equals(f._fromField, fromField)
          && ImplHelper.isAssignable(f._fromType, type)) {
        avail = f._availableRecursion;
        if (traverse) avail = reduce(avail);
        break;
      }
    }
    if (avail == 0) return 0;

    // calculate fetch groups max
    ClassMetaData meta = fm.getDefiningMetaData();
    int max = Integer.MIN_VALUE;
    if (fm.isInDefaultFetchGroup())
      max = meta.getFetchGroup(FetchGroup.NAME_DEFAULT).getRecursionDepth(fm);
    String[] groups = fm.getCustomFetchGroups();
    int cur;
    for (int i = 0; max != FetchGroup.DEPTH_INFINITE && i < groups.length; i++) {
      // ignore custom groups that are inactive in this configuration
      if (!this.hasFetchGroup(groups[i])) continue;
      cur = meta.getFetchGroup(groups[i]).getRecursionDepth(fm);
      if (cur == FetchGroup.DEPTH_INFINITE || cur > max) max = cur;
    }
    // reduce max if we're traversing a self-type relation
    if (traverse
        && max != Integer.MIN_VALUE
        && ImplHelper.isAssignable(meta.getDescribedType(), type)) max = reduce(max);

    // take min/defined of previous avail and fetch group max
    if (avail == Integer.MIN_VALUE && max == Integer.MIN_VALUE) {
      int def = FetchGroup.RECURSION_DEPTH_DEFAULT;
      return (traverse && ImplHelper.isAssignable(meta.getDescribedType(), type)) ? def - 1 : def;
    }
    if (avail == Integer.MIN_VALUE || avail == FetchGroup.DEPTH_INFINITE) return max;
    if (max == Integer.MIN_VALUE || max == FetchGroup.DEPTH_INFINITE) return avail;
    return Math.min(max, avail);
  }

  /** Reduce the given logical depth by 1. */
  private static int reduce(int d) {
    if (d == 0) return 0;
    if (d != FetchGroup.DEPTH_INFINITE) d--;
    return d;
  }

  /////////////////
  // Debug methods
  /////////////////

  FetchConfiguration getParent() {
    return _parent;
  }

  boolean isRoot() {
    return _parent == null;
  }

  FetchConfiguration getRoot() {
    return (isRoot()) ? this : _parent.getRoot();
  }

  int getAvailableFetchDepth() {
    return _availableDepth;
  }

  int getAvailableRecursionDepth() {
    return _availableRecursion;
  }

  String getTraversedFromField() {
    return _fromField;
  }

  Class<?> getTraversedFromType() {
    return _fromType;
  }

  List<FetchConfigurationImpl> getPath() {
    if (isRoot()) return Collections.emptyList();
    return trackPath(new ArrayList<FetchConfigurationImpl>());
  }

  List<FetchConfigurationImpl> trackPath(List<FetchConfigurationImpl> path) {
    if (_parent != null) _parent.trackPath(path);
    path.add(this);
    return path;
  }

  public String toString() {
    return "FetchConfiguration@"
        + System.identityHashCode(this)
        + " ("
        + _availableDepth
        + ")"
        + getPathString();
  }

  private String getPathString() {
    List<FetchConfigurationImpl> path = getPath();
    if (path.isEmpty()) return "";
    StringBuilder buf = new StringBuilder().append(": ");
    for (Iterator<FetchConfigurationImpl> itr = path.iterator(); itr.hasNext(); ) {
      buf.append(itr.next().getTraversedFromField());
      if (itr.hasNext()) buf.append("->");
    }
    return buf.toString();
  }

  protected String toString(Object o) {
    return o == null ? "null" : o.toString() + "[" + o.getClass().getName() + "]";
  }
}
/**
 * Maps a relation to a set of other objects using an inverse foreign key in the related object
 * table.
 *
 * @author Abe White
 */
public abstract class RelationToManyInverseKeyFieldStrategy extends StoreCollectionFieldStrategy {

  private static final Localizer _loc =
      Localizer.forPackage(RelationToManyInverseKeyFieldStrategy.class);

  private boolean _orderInsert = false;
  private boolean _orderUpdate = false;
  private boolean _uni1MFK = false;

  protected ClassMapping[] getIndependentElementMappings(boolean traverse) {
    return field.getElementMapping().getIndependentTypeMappings();
  }

  protected ForeignKey getJoinForeignKey(ClassMapping elem) {
    return field.getElementMapping().getForeignKey(elem);
  }

  protected void selectElement(
      Select sel,
      ClassMapping elem,
      JDBCStore store,
      JDBCFetchConfiguration fetch,
      int eagerMode,
      Joins joins) {
    sel.select(
        elem, field.getElementMapping().getSelectSubclasses(), store, fetch, eagerMode, joins);
  }

  protected Object loadElement(
      OpenJPAStateManager sm,
      JDBCStore store,
      JDBCFetchConfiguration fetch,
      Result res,
      Joins joins)
      throws SQLException {
    ClassMapping elem = res.getBaseMapping();
    if (elem == null) elem = field.getElementMapping().getIndependentTypeMappings()[0];
    return res.load(elem, store, fetch, joins);
  }

  protected Joins join(Joins joins, ClassMapping elem) {
    ValueMapping vm = field.getElementMapping();
    ForeignKey fk = vm.getForeignKey(elem);
    ClassMapping owner = field.getDefiningMapping();
    while (fk.getPrimaryKeyTable() != owner.getTable()) {
      joins = owner.joinSuperclass(joins, false);
      owner = owner.getJoinablePCSuperclassMapping();
      if (owner == null) throw new InternalException();
    }
    return joins.joinRelation(field.getName(), fk, elem, vm.getSelectSubclasses(), true, true);
  }

  protected Joins joinElementRelation(Joins joins, ClassMapping elem) {
    return joinRelation(joins, false, false);
  }

  public void map(boolean adapt) {
    OpenJPAConfiguration conf = field.getRepository().getConfiguration();
    boolean isNonDefaultMappingAllowed =
        field.getRepository().getMetaDataFactory().getDefaults().isNonDefaultMappingAllowed(conf);
    FieldMapping mapped = field.getMappedByMapping();

    // JPA 2.0 allows non-default mapping: Uni-/1-M/@JoinColumn ==> foreign key strategy
    // Bi-/1-M/@JoinColumn should result in exception
    if (!isNonDefaultMappingAllowed || mapped != null) {
      field.getValueInfo().assertNoSchemaComponents(field, !adapt);
    }
    field.getKeyMapping().getValueInfo().assertNoSchemaComponents(field.getKey(), !adapt);

    ValueMapping elem = field.getElementMapping();
    if (elem.getTypeCode() != JavaTypes.PC
        || elem.isEmbeddedPC()
        || !elem.getTypeMapping().isMapped())
      throw new MetaDataException(_loc.get("not-elem-relation", field));

    // check for named inverse
    FieldMappingInfo finfo = field.getMappingInfo();
    ValueMappingInfo vinfo = elem.getValueInfo();
    boolean criteria = vinfo.getUseClassCriteria();
    if (mapped != null) {
      mapped.resolve(mapped.MODE_META | mapped.MODE_MAPPING);
      if (!(mapped.getStrategy() instanceof RelationFieldStrategy
          || mapped.getHandler() instanceof UntypedPCValueHandler))
        throw new MetaDataException(_loc.get("not-inv-relation", field, mapped));
      vinfo.assertNoSchemaComponents(elem, !adapt);
      elem.setForeignKey(mapped.getForeignKey(field.getDefiningMapping()));
      elem.setColumns(mapped.getDefiningMapping().getPrimaryKeyColumns());
      elem.setJoinDirection(ValueMapping.JOIN_EXPECTED_INVERSE);
      elem.setUseClassCriteria(criteria);

      ForeignKey fk = mapped.getForeignKey();
      /**
       * Foreign key may be null if declared type of the mapped field is abstract and under
       * table-per-class inheritance strategy will have no mapped table.
       */
      if (fk != null) {
        field.setOrderColumn(finfo.getOrderColumn(field, fk.getTable(), adapt));
        field.setOrderColumnIO(finfo.getColumnIO());
      }
      return;
    } else {
      if (field.getValueInfo().getColumns().size() > 0
          && field.getAccessType() == FieldMetaData.ONE_TO_MANY) {
        _uni1MFK = true;
      }
    }

    // map inverse foreign key in related table
    ForeignKey fk = vinfo.getInverseTypeJoin(elem, field.getName(), adapt);
    if (_uni1MFK) {
      Column[] locals = fk.getColumns();
      for (int i = 0; i < locals.length; i++) locals[i].setUni1MFK(true);
    }
    elem.setForeignKey(fk);
    elem.setColumnIO(vinfo.getColumnIO());
    elem.setColumns(elem.getTypeMapping().getPrimaryKeyColumns());
    elem.setJoinDirection(ValueMapping.JOIN_EXPECTED_INVERSE);
    elem.setUseClassCriteria(criteria);
    elem.mapConstraints(field.getName(), adapt);

    field.setOrderColumn(finfo.getOrderColumn(field, fk.getTable(), adapt));
    field.setOrderColumnIO(finfo.getColumnIO());
  }

  public void initialize() {
    Column order = field.getOrderColumn();
    _orderInsert = field.getOrderColumnIO().isInsertable(order, false);
    _orderUpdate = field.getOrderColumnIO().isUpdatable(order, false);

    ValueMapping elem = field.getElementMapping();
    Log log = field.getRepository().getLog();
    if (field.getMappedBy() == null && elem.getUseClassCriteria() && log.isWarnEnabled()) {
      ForeignKey fk = elem.getForeignKey();
      if (elem.getColumnIO().isAnyUpdatable(fk, false))
        log.warn(_loc.get("class-crit-owner", field));
    }
  }

  public void insert(OpenJPAStateManager sm, JDBCStore store, RowManager rm) throws SQLException {
    if (field.getMappedBy() == null || _orderInsert || _orderUpdate)
      insert(sm, rm, sm.fetchObject(field.getIndex()));
  }

  private void insert(OpenJPAStateManager sm, RowManager rm, Object vals) throws SQLException {
    if (field.getMappedBy() != null && !_orderInsert && !_orderUpdate) return;
    Collection coll = toCollection(vals);
    if (coll == null || coll.isEmpty()) return;

    ClassMapping rel = field.getElementMapping().getTypeMapping();
    int idx = 0;
    for (Iterator itr = coll.iterator(); itr.hasNext(); idx++)
      updateInverse(sm.getContext(), itr.next(), rel, rm, sm, idx);
  }

  public void update(OpenJPAStateManager sm, JDBCStore store, RowManager rm) throws SQLException {
    if (field.getMappedBy() != null && !_orderInsert && !_orderUpdate) return;

    Object obj = sm.fetchObject(field.getIndex());
    ChangeTracker ct = null;
    if (obj instanceof Proxy) {
      Proxy proxy = (Proxy) obj;
      if (Proxies.isOwner(proxy, sm, field.getIndex())) ct = proxy.getChangeTracker();
    }
    Column order = field.getOrderColumn();

    // if no fine-grained change tracking then just delete and reinsert
    // if no fine-grained change tracking or if an item was removed
    // from an ordered collection, delete and reinsert
    if (ct == null || !ct.isTracking() || (order != null && !ct.getRemoved().isEmpty())) {
      delete(sm, store, rm);
      insert(sm, rm, obj);
      return;
    }

    // null inverse columns for deletes and update them with our oid for
    // inserts
    ClassMapping rel = field.getElementMapping().getTypeMapping();
    StoreContext ctx = store.getContext();
    if (field.getMappedBy() == null) {
      Collection rem = ct.getRemoved();
      for (Iterator itr = rem.iterator(); itr.hasNext(); )
        updateInverse(ctx, itr.next(), rel, rm, null, 0);
    }

    Collection add = ct.getAdded();
    int seq = ct.getNextSequence();
    for (Iterator itr = add.iterator(); itr.hasNext(); seq++)
      updateInverse(ctx, itr.next(), rel, rm, sm, seq);
    if (order != null) ct.setNextSequence(seq);
  }

  public void delete(OpenJPAStateManager sm, JDBCStore store, RowManager rm) throws SQLException {
    if (field.getMappedBy() != null) return;

    // if nullable, null any existing inverse columns that refer to this obj
    ValueMapping elem = field.getElementMapping();
    ColumnIO io = elem.getColumnIO();
    ForeignKey fk = elem.getForeignKey();
    if (!elem.getUseClassCriteria() && io.isAnyUpdatable(fk, true)) {
      assertInversable();
      Row row = rm.getAllRows(fk.getTable(), Row.ACTION_UPDATE);
      row.setForeignKey(fk, io, null);
      row.whereForeignKey(fk, sm);
      rm.flushAllRows(row);
      return;
    }

    if (!sm.getLoaded().get(field.getIndex())) return;

    // update fk on each field value row
    ClassMapping rel = field.getElementMapping().getTypeMapping();
    StoreContext ctx = store.getContext();
    Collection objs = toCollection(sm.fetchObject(field.getIndex()));
    if (objs != null && !objs.isEmpty())
      for (Iterator itr = objs.iterator(); itr.hasNext(); )
        updateInverse(ctx, itr.next(), rel, rm, sm, 0);
  }

  /** This method updates the inverse columns of a 1-M related object with the given oid. */
  private void updateInverse(
      StoreContext ctx,
      Object inverse,
      ClassMapping rel,
      RowManager rm,
      OpenJPAStateManager sm,
      int idx)
      throws SQLException {
    OpenJPAStateManager invsm = RelationStrategies.getStateManager(inverse, ctx);
    if (invsm == null) return;

    ValueMapping elem = field.getElementMapping();
    ForeignKey fk = elem.getForeignKey();
    ColumnIO io = elem.getColumnIO();
    Column order = field.getOrderColumn();

    int action;
    boolean writeable;
    boolean orderWriteable;
    if (invsm.isNew() && !invsm.isFlushed()) {
      // no need to null inverse columns of new instance
      if (sm == null || sm.isDeleted()) return;
      writeable = io.isAnyInsertable(fk, false);
      orderWriteable = _orderInsert;
      action = Row.ACTION_INSERT;
    } else if (invsm.isDeleted()) {
      // no need to null inverse columns of deleted instance
      if (invsm.isFlushed() || sm == null || !sm.isDeleted()) return;
      writeable = true;
      orderWriteable = false;
      action = Row.ACTION_DELETE;
    } else {
      if (sm != null && sm.isDeleted()) sm = null;
      writeable = io.isAnyUpdatable(fk, sm == null);
      orderWriteable = field.getOrderColumnIO().isUpdatable(order, sm == null);
      action = Row.ACTION_UPDATE;
    }
    if (!writeable && !orderWriteable) return;

    assertInversable();

    // if this is an update, this might be the only mod to the row, so
    // make sure the where condition is set
    Row row = rm.getRow(fk.getTable(), action, invsm, true);
    if (action == Row.ACTION_UPDATE) row.wherePrimaryKey(invsm);

    // update the inverse pointer with our oid value
    if (writeable) row.setForeignKey(fk, io, sm);
    if (orderWriteable) row.setInt(order, idx);
  }

  public Object toDataStoreValue(Object val, JDBCStore store) {
    ClassMapping cm = field.getElementMapping().getTypeMapping();
    return cm.toDataStoreValue(val, cm.getPrimaryKeyColumns(), store);
  }

  public Joins join(Joins joins, boolean forceOuter) {
    ValueMapping elem = field.getElementMapping();
    ClassMapping[] clss = elem.getIndependentTypeMappings();
    if (clss.length != 1) throw RelationStrategies.unjoinable(elem);
    if (forceOuter)
      return joins.outerJoinRelation(
          field.getName(),
          elem.getForeignKey(clss[0]),
          clss[0],
          elem.getSelectSubclasses(),
          true,
          true);
    return joins.joinRelation(
        field.getName(),
        elem.getForeignKey(clss[0]),
        clss[0],
        elem.getSelectSubclasses(),
        true,
        true);
  }

  private void assertInversable() {
    ValueMapping elem = field.getElementMapping();
    if (elem.getIndependentTypeMappings().length != 1) throw RelationStrategies.uninversable(elem);
  }
}