| /* |
| * 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.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.Set; |
| |
| import org.apache.commons.lang.StringUtils; |
| 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.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 |
| * @nojavadoc |
| */ |
| public class FetchConfigurationImpl |
| implements FetchConfiguration, Cloneable { |
| |
| private static final Localizer _loc = Localizer.forPackage |
| (FetchConfigurationImpl.class); |
| |
| /** |
| * 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 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; |
| } |
| |
| 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()); |
| 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()); |
| setLockTimeout(fetch.getLockTimeout()); |
| clearFetchGroups(); |
| addFetchGroups(fetch.getFetchGroups()); |
| clearFields(); |
| addFields(fetch.getFields()); |
| |
| // don't use setters because require active transaction |
| _state.readLockLevel = fetch.getReadLockLevel(); |
| _state.writeLockLevel = fetch.getWriteLockLevel(); |
| } |
| |
| 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 FetchConfiguration setFlushBeforeQueries(int flush) { |
| if (flush == DEFAULT && _state.ctx != null) |
| _state.flushQuery = _state.ctx.getConfiguration(). |
| getFlushBeforeQueriesConstant(); |
| else if (flush != DEFAULT) |
| _state.flushQuery = flush; |
| return this; |
| } |
| |
| public Set getFetchGroups() { |
| return (_state.fetchGroups == null) ? Collections.EMPTY_SET |
| : _state.fetchGroups; |
| } |
| |
| public boolean hasFetchGroup(String group) { |
| return _state.fetchGroups != null |
| && (hasFetchGroupAll() |
| || _state.fetchGroups.contains(group)); |
| } |
| |
| 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(); |
| _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 groups) { |
| if (groups == null || groups.isEmpty()) |
| return this; |
| for (Iterator itr = groups.iterator(); itr.hasNext();) |
| addFetchGroup((String) itr.next()); |
| 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 groups) { |
| lock(); |
| try { |
| if (_state.fetchGroups != null && groups != null) |
| for (Object group : groups) |
| removeFetchGroup(group.toString()); |
| } 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 getFields() { |
| return (_state.fields == null) ? Collections.EMPTY_SET : _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(); |
| _state.fields.add(field); |
| } finally { |
| unlock(); |
| } |
| return this; |
| } |
| |
| 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 { |
| 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 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 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) |
| _state.lockTimeout = timeout; |
| return this; |
| } |
| |
| public int getReadLockLevel() { |
| return _state.readLockLevel; |
| } |
| |
| public FetchConfiguration setReadLockLevel(int level) { |
| if (_state.ctx == null) |
| return this; |
| |
| lock(); |
| try { |
| 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; |
| |
| 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 (_state.ctx != null && !_state.ctx.isActive()) |
| throw new NoTransactionException(_loc.get("not-active")); |
| } |
| |
| public void setHint(String name, Object value) { |
| lock(); |
| try { |
| if (_state.hints == null) |
| _state.hints = new HashMap(); |
| _state.hints.put(name, value); |
| } finally { |
| unlock(); |
| } |
| } |
| |
| public Object getHint(String name) { |
| return (_state.hints == null) ? null : _state.hints.get(name); |
| } |
| |
| public Set getRootClasses() { |
| return (_state.rootClasses == null) ? Collections.EMPTY_SET |
| : _state.rootClasses; |
| } |
| |
| 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; |
| } |
| |
| public Set getRootInstances() { |
| return (_state.rootInstances == null) ? Collections.EMPTY_SET |
| : _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(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 = getRelationType(fm); |
| 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; |
| |
| int rdepth = getAvailableRecursionDepth(fm, type, false); |
| if (rdepth != FetchGroup.DEPTH_INFINITE && rdepth <= 0) |
| return FETCH_NONE; |
| |
| if (StringUtils.equals(_directRelationOwner, fm.getFullName())) |
| return FETCH_REF; |
| return FETCH_LOAD; |
| } |
| |
| public boolean requiresLoad() { |
| return _load; |
| } |
| |
| public FetchConfiguration traverse(FieldMetaData fm) { |
| Class type = getRelationType(fm); |
| 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, true); |
| if (StringUtils.equals(_directRelationOwner, fm.getFullName())) |
| clone._load = false; |
| else |
| clone._load = _load; |
| |
| FieldMetaData owner = fm.getMappedByMetaData(); |
| if (owner != null && owner.getTypeCode() == JavaTypes.PC) |
| clone._directRelationOwner = owner.getFullName(); |
| |
| return clone; |
| } |
| |
| /** |
| * Whether our configuration state includes the given field. |
| */ |
| private boolean includes(FieldMetaData fmd) { |
| if (hasFetchGroupAll() |
| || (fmd.isInDefaultFetchGroup() |
| && hasFetchGroupDefault()) |
| || hasField(fmd.getFullName(false))) |
| return true; |
| String[] fgs = fmd.getCustomFetchGroups(); |
| for (int i = 0; i < fgs.length; i++) |
| if (hasFetchGroup(fgs[i])) |
| return true; |
| return false; |
| } |
| |
| /** |
| * 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, |
| boolean traverse) { |
| // see if there's a previous limit |
| int avail = Integer.MIN_VALUE; |
| for (FetchConfigurationImpl f = this; f != null; f = f._parent) { |
| if (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); |
| } |
| |
| /** |
| * Return the relation type of the given field. |
| */ |
| private static Class getRelationType(FieldMetaData fm) { |
| if (fm.isDeclaredTypePC()) |
| return fm.getDeclaredType(); |
| if (fm.getElement().isDeclaredTypePC()) |
| return fm.getElement().getDeclaredType(); |
| if (fm.getKey().isDeclaredTypePC()) |
| return fm.getKey().getDeclaredType(); |
| return null; |
| } |
| |
| /** |
| * 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.EMPTY_LIST; |
| return trackPath(new ArrayList()); |
| } |
| |
| List trackPath(List 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 path = getPath(); |
| if (path.isEmpty()) |
| return ""; |
| StringBuffer buf = new StringBuffer().append (": "); |
| for (Iterator itr = path.iterator(); itr.hasNext();) { |
| buf.append(((FetchConfigurationImpl) itr.next()). |
| getTraversedFromField()); |
| if (itr.hasNext()) |
| buf.append("->"); |
| } |
| return buf.toString(); |
| } |
| } |