All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.openjpa.kernel.FetchConfigurationImpl Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openjpa.kernel;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.lib.rop.EagerResultList;
import org.apache.openjpa.lib.rop.ListResultObjectProvider;
import org.apache.openjpa.lib.rop.ResultList;
import org.apache.openjpa.lib.rop.ResultObjectProvider;
import org.apache.openjpa.lib.rop.SimpleResultList;
import org.apache.openjpa.lib.rop.WindowResultList;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FetchGroup;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.NoTransactionException;
import org.apache.openjpa.util.UserException;

/**
 * Allows configuration and optimization of how objects are loaded from
 * the data store.
 *
 * @since 0.3.0
 * @author Abe White
 * @author Pinaki Poddar
 */
public class FetchConfigurationImpl
    implements FetchConfiguration, Cloneable {

    private static final long serialVersionUID = 1L;
    private static final Localizer _loc = Localizer.forPackage(FetchConfigurationImpl.class);
    private static Map _hintSetters = new HashMap<>();

    /**
     * Registers hint keys that have a corresponding setter method.
     * The hint keys are registered in openjpa.FetchPlan and openjpa as prefix.
     * Also some keys are registered in jakarta.persistence 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, "jakarta.persistence.lock");
            populateHintSetter(target, "MaxFetchDepth", int.class, prefixes);
            populateHintSetter(target, "QueryTimeout", int.class, prefixes);
            populateHintSetter(target, "setQueryTimeout", "timeout", int.class, "jakarta.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
    {
        
        private static final long serialVersionUID = 1L;
        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 fetchGroups = null;
        public Set fields = null;
        public Set> rootClasses;
        public Set rootInstances;
        public Map hints = null;
        public boolean fetchGroupContainsDefault = false;
        public boolean fetchGroupContainsAll = false;
        public boolean fetchGroupIsPUDefault = false;
        public boolean extendedPathLookup = false;
        public DataCacheRetrieveMode cacheRetrieveMode = DataCacheRetrieveMode.USE;
        public DataCacheStoreMode cacheStoreMode = DataCacheStoreMode.USE;
        public boolean cacheNonDefaultFetchPlanQueries = false;
    }

    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;
    }

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

    @Override
    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());

        String[] fetchGroupList = conf.getFetchGroupsList();
        clearFetchGroups((fetchGroupList == null || fetchGroupList.length == 0));

        addFetchGroups(Arrays.asList(fetchGroupList));
        setMaxFetchDepth(conf.getMaxFetchDepth());

        _state.cacheNonDefaultFetchPlanQueries = conf.getCompatibilityInstance().getCacheNonDefaultFetchPlanQueries();
    }

    /**
     * Clone this instance.
     */
    @Override
    public Object clone() {
        FetchConfigurationImpl clone = newInstance(null);
        clone._state.ctx = _state.ctx;
        clone._state.cacheNonDefaultFetchPlanQueries = _state.cacheNonDefaultFetchPlanQueries;
        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);
    }

    @Override
    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(false);
        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))
            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<>();
        this._state.hints.putAll(from._state.hints);
    }

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

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

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

    @Override
    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;
    }

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

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

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

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

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

    @Override
    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", flush).getMessage());

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

    @Override
    public Set getFetchGroups() {
        if (_state.fetchGroups == null) return Collections.emptySet();
        return _state.fetchGroups;
    }

    @Override
    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;
     }

    @Override
    public FetchConfiguration addFetchGroup(String name) {
        return addFetchGroup(name, true);
    }

    private FetchConfiguration addFetchGroup(String name, boolean recomputeIsDefault) {
        if (StringUtil.isEmpty(name))
            throw new UserException(_loc.get("null-fg"));

        lock();
        try {
            if (_state.fetchGroups == null)
                _state.fetchGroups = new HashSet<>();
            _state.fetchGroups.add(name);

            if (FetchGroup.NAME_ALL.equals(name))
                _state.fetchGroupContainsAll = true;
            else if (FetchGroup.NAME_DEFAULT.equals(name))
                _state.fetchGroupContainsDefault = true;
        } finally {
            if (recomputeIsDefault) {
                verifyDefaultPUFetchGroups();
            }
            unlock();
        }
        return this;
    }

    @Override
    public FetchConfiguration addFetchGroups(Collection groups) {
        if (groups == null || groups.isEmpty())
            return this;
        for (String group : groups)
            addFetchGroup(group, false);

        verifyDefaultPUFetchGroups();
        return this;
    }

    @Override
    public FetchConfiguration removeFetchGroup(String group) {
        return removeFetchGroup(group, true);
    }

    private FetchConfiguration removeFetchGroup(String group, boolean recomputeIsDefault) {
        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 {
            if (recomputeIsDefault) {
                verifyDefaultPUFetchGroups();
            }
            unlock();
        }
        return this;
    }

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

    @Override
    public FetchConfiguration clearFetchGroups() {
        return clearFetchGroups(true);
    }

    private FetchConfiguration clearFetchGroups(boolean restoresDefault) {
        lock();
        try {
            if (_state.fetchGroups != null) {
                _state.fetchGroups.clear();
            } else {
                _state.fetchGroups = new HashSet<>();
            }

            _state.fetchGroupContainsAll = false;

            if (restoresDefault) {
                _state.fetchGroupContainsDefault = true;
                _state.fetchGroups.add(FetchGroup.NAME_DEFAULT); // OPENJPA-2413
            }
        } finally {
            verifyDefaultPUFetchGroups();
            unlock();
        }
        return this;
    }

    @Override
    public FetchConfiguration resetFetchGroups() {
        String[] fetchGroupList = _state.ctx.getConfiguration().getFetchGroupsList();
        clearFetchGroups((fetchGroupList == null || fetchGroupList.length == 0));

        if (_state.ctx != null)
            addFetchGroups(Arrays.asList(fetchGroupList));

        verifyDefaultPUFetchGroups();

        return this;
    }

    /**
     * Determine if the current selection of FetchGroups is equivalent to the Configuration's default FetchGroups
     */
    private void verifyDefaultPUFetchGroups() {
        _state.fetchGroupIsPUDefault = false;

        if (_state.fields != null && !_state.fields.isEmpty()) {
            return;
        }

        if (_state.fetchGroups != null && _state.ctx != null) {
            List defaultPUFetchGroups = Arrays.asList(_state.ctx.getConfiguration().getFetchGroupsList());
            if (_state.fetchGroups.size() != defaultPUFetchGroups.size()) {
                return;
            }

            for (String fetchGroupName : defaultPUFetchGroups) {
                if (!_state.fetchGroups.contains(fetchGroupName)) {
                    return;
                }
            }

            _state.fetchGroupIsPUDefault = true;
        }
    }

    @Override
    public boolean isDefaultPUFetchGroupConfigurationOnly() {
        return _state.fetchGroupIsPUDefault;
    }

    @Override
    public boolean isFetchConfigurationSQLCacheAdmissible() {
        if (_state == null || _state.cacheNonDefaultFetchPlanQueries) {
            return false;
        } else {
            // Only pctx-default matching FetchConfiguration generated SQL is cache permissible
            return _state.fetchGroupIsPUDefault;
        }
    }

    @Override
    public Set getFields() {
        if (_state.fields == null) return Collections.emptySet();
        return _state.fields;
    }

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

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

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

    @Override
    public FetchConfiguration addFields(Collection fields) {
        if (fields == null || fields.isEmpty())
            return this;

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

    @Override
    public FetchConfiguration removeField(String field) {
        lock();
        try {
            if (_state.fields != null) {
                _state.fields.remove(field);

                if (_state.fields.size() == 0) {
                    verifyDefaultPUFetchGroups();
                }
            }
        } finally {
            unlock();
        }
        return this;
    }

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

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

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

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

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

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

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

    @Override
    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;
    }

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

    @Override
    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;
    }

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

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

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

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

        if (level != DEFAULT
            && level != LockLevels.LOCK_NONE
            && level != LockLevels.LOCK_READ
            && level != MixedLockLevels.LOCK_OPTIMISTIC
            && level != LockLevels.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", level).getMessage());

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

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

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

        if (level != DEFAULT
            && level != LockLevels.LOCK_NONE
            && level != LockLevels.LOCK_READ
            && level != MixedLockLevels.LOCK_OPTIMISTIC
            && level != LockLevels.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", level).getMessage());

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

    @Override
    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.
     *
     */
    @Override
    public Map getHints() {
        if (_state.hints == null)
            return Collections.emptyMap();
        return Collections.unmodifiableMap(_state.hints);
    }

    /**
     * Affirms if the given key is set as a hint.
     */
    @Override
    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 getSupportedHints() {
        return _hintSetters.keySet();
    }

    /**
     * Same as setHint(key, value, value).
     *
     * @see #setHint(String, Object, Object)
     */
    @Override
    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.
     * 
* 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. *
* 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. */ @Override 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<>(); _state.hints.put(name, value); } finally { unlock(); } } @Override 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); } @Override public Set> getRootClasses() { if (_state.rootClasses == null) return Collections.emptySet(); return _state.rootClasses; } @Override public FetchConfiguration setRootClasses(Collection> classes) { lock(); try { if (_state.rootClasses != null) _state.rootClasses.clear(); if (classes != null && !classes.isEmpty()) { if (_state.rootClasses == null) _state.rootClasses = new HashSet<>(classes); else _state.rootClasses.addAll(classes); } } finally { unlock(); } return this; } @Override public Set getRootInstances() { if (_state.rootInstances == null) return Collections.emptySet(); return _state.rootInstances; } @Override 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<>(instances); } else { _state.rootInstances.addAll(instances); } } } finally { unlock(); } return this; } @Override public void lock() { if (_state.ctx != null) _state.ctx.lock(); } @Override public void unlock() { if (_state.ctx != null) _state.ctx.unlock(); } ///////////// // Traversal ///////////// @Override 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 (Objects.equals(_directRelationOwner, fieldName)) return FETCH_REF; return FETCH_LOAD; } @Override public boolean requiresLoad() { return _load; } @Override 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 (Objects.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 (String fg : fgs) if (hasFetchGroup(fg)) 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 (Objects.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 getPath() { if (isRoot()) return Collections.emptyList(); return trackPath(new ArrayList<>()); } List trackPath(List path) { if (_parent != null) _parent.trackPath(path); path.add(this); return path; } @Override public String toString() { return "FetchConfiguration@" + System.identityHashCode(this) + " (" + _availableDepth + ")" + getPathString(); } private String getPathString() { List path = getPath(); if (path.isEmpty()) return ""; StringBuilder buf = new StringBuilder().append (": "); for (Iterator 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() + "]"; } }