/*****************************************************************
* 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.cayenne.map;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.reflect.ClassDescriptor;
import org.apache.cayenne.reflect.ClassDescriptorMap;
import org.apache.cayenne.reflect.FaultFactory;
import org.apache.cayenne.reflect.LifecycleCallbackRegistry;
import org.apache.cayenne.reflect.SingletonFaultFactory;
import org.apache.cayenne.reflect.generic.DataObjectDescriptorFactory;
import org.apache.cayenne.reflect.valueholder.ValueHolderDescriptorFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicLong;
/**
* Represents a virtual shared namespace for zero or more DataMaps. Unlike
* DataMap, EntityResolver is intended to work as a runtime container of
* mapping. DataMaps can be added or removed dynamically at runtime.
* <p>
* EntityResolver is thread-safe.
* </p>
*
* @since 1.1
*/
public class EntityResolver implements MappingNamespace, Serializable {
protected static final Logger logger = LoggerFactory.getLogger(EntityResolver.class);
protected static AtomicLong incrementer = new AtomicLong();
@Deprecated
protected boolean indexedByClass;
protected Collection<DataMap> maps;
protected transient MappingNamespace mappingCache;
protected EntityResolver clientEntityResolver;
// must be transient, as resolver may get deserialized in another VM, and
// descriptor recompilation will be desired.
protected transient volatile ClassDescriptorMap classDescriptorMap;
// callbacks are not serializable
protected transient LifecycleCallbackRegistry callbackRegistry;
protected transient ValueObjectTypeRegistry valueObjectTypeRegistry;
/**
* Creates new empty EntityResolver.
*/
public EntityResolver() {
this(Collections.<DataMap> emptyList());
}
/**
* Creates new EntityResolver that indexes a collection of DataMaps.
*/
public EntityResolver(Collection<DataMap> dataMaps) {
this.maps = new ArrayList<>(dataMaps);
refreshMappingCache();
}
/**
* Updates missing mapping artifacts that can be guessed from other mapping
* information. This implementation creates missing reverse relationships,
* marking newly created relationships as "runtime".
*
* @since 3.0
*/
public void applyDBLayerDefaults() {
// connect DB layer
for (DataMap map : getDataMaps()) {
for (DbEntity entity : map.getDbEntities()) {
// iterate by copy to avoid concurrency modification errors on
// reflexive relationships
DbRelationship[] relationships = entity.getRelationships().toArray(
new DbRelationship[entity.getRelationships().size()]);
for (DbRelationship relationship : relationships) {
if (relationship.getReverseRelationship() == null) {
DbRelationship reverse = relationship.createReverseRelationship();
Entity targetEntity = reverse.getSourceEntity();
reverse.setName(getUniqueRelationshipName(targetEntity));
reverse.setRuntime(true);
targetEntity.addRelationship(reverse);
logger.info("added runtime complimentary DbRelationship from " + targetEntity.getName()
+ " to " + reverse.getTargetEntityName());
}
}
}
}
}
/**
* @since 3.0
* @deprecated since 4.0 does nothing. Previously it used to create runtime
* ObjRelationships, that broke a lot of things.
*/
@Deprecated
public void applyObjectLayerDefaults() {
// noop
}
private String getUniqueRelationshipName(Entity entity) {
String name;
do {
name = "runtimeRelationship" + incrementer.getAndIncrement();
} while(entity.getRelationship(name) != null);
return name;
}
/**
* Compiles internal callback registry.
*/
synchronized void initCallbacks() {
if (callbackRegistry == null) {
LifecycleCallbackRegistry callbackRegistry = new LifecycleCallbackRegistry(this);
// load entity callbacks
for (ObjEntity entity : getObjEntities()) {
Class<?> entityClass = entity.getJavaClass();
CallbackDescriptor[] callbacks = entity.getCallbackMap().getCallbacks();
for (CallbackDescriptor callback : callbacks) {
for (String method : callback.getCallbackMethods()) {
callbackRegistry.addCallback(callback.getCallbackType(), entityClass, method);
}
}
}
this.callbackRegistry = callbackRegistry;
}
}
/**
* Returns a {@link LifecycleCallbackRegistry} for handling callbacks.
* Registry is lazily initialized on first call.
*
* @since 3.0
*/
public LifecycleCallbackRegistry getCallbackRegistry() {
if (callbackRegistry == null) {
initCallbacks();
}
return callbackRegistry;
}
/**
* Sets a lifecycle callbacks registry of the EntityResolver. Users rarely
* if ever need to call this method as Cayenne would instantiate a registry
* itself as needed based on mapped configuration.
*
* @since 3.0
*/
public void setCallbackRegistry(LifecycleCallbackRegistry callbackRegistry) {
this.callbackRegistry = callbackRegistry;
}
/**
* Returns ClientEntityResolver with mapping information that only includes
* entities available on CWS Client Tier.
*
* @since 1.2
*/
public EntityResolver getClientEntityResolver() {
if (clientEntityResolver == null) {
synchronized (this) {
if (clientEntityResolver == null) {
EntityResolver resolver = new ClientEntityResolver();
// translate to client DataMaps
for (DataMap map : getDataMaps()) {
DataMap clientMap = map.getClientDataMap(this);
if (clientMap != null) {
resolver.addDataMap(clientMap);
}
}
clientEntityResolver = resolver;
}
}
}
return clientEntityResolver;
}
/**
* Returns all DbEntities.
*/
public Collection<DbEntity> getDbEntities() {
checkMappingCache();
return mappingCache.getDbEntities();
}
public Collection<ObjEntity> getObjEntities() {
checkMappingCache();
return mappingCache.getObjEntities();
}
/**
* @since 3.0
*/
public Collection<Embeddable> getEmbeddables() {
checkMappingCache();
return mappingCache.getEmbeddables();
}
/**
* @deprecated since 4.0 use {@link #getResults()}.
*/
@Deprecated
public Collection<SQLResult> getResultSets() {
return getResults();
}
/**
* @since 4.0
*/
public Collection<SQLResult> getResults() {
checkMappingCache();
return mappingCache.getResults();
}
public Collection<Procedure> getProcedures() {
checkMappingCache();
return mappingCache.getProcedures();
}
public Collection<QueryDescriptor> getQueryDescriptors() {
checkMappingCache();
return mappingCache.getQueryDescriptors();
}
public DbEntity getDbEntity(String name) {
checkMappingCache();
DbEntity result = mappingCache.getDbEntity(name);
if (result == null) {
// reconstruct cache just in case some of the datamaps
// have changed and now contain the required information
refreshMappingCache();
result = mappingCache.getDbEntity(name);
}
return result;
}
public ObjEntity getObjEntity(String name) {
checkMappingCache();
ObjEntity result = mappingCache.getObjEntity(name);
if (result == null) {
// reconstruct cache just in case some of the datamaps
// have changed and now contain the required information
refreshMappingCache();
result = mappingCache.getObjEntity(name);
}
return result;
}
public Procedure getProcedure(String procedureName) {
checkMappingCache();
Procedure result = mappingCache.getProcedure(procedureName);
if (result == null) {
// reconstruct cache just in case some of the datamaps
// have changed and now contain the required information
refreshMappingCache();
result = mappingCache.getProcedure(procedureName);
}
return result;
}
/**
* Returns a named query or null if no query exists for a given name.
*/
public QueryDescriptor getQueryDescriptor(String name) {
checkMappingCache();
QueryDescriptor result = mappingCache.getQueryDescriptor(name);
if (result == null) {
// reconstruct cache just in case some of the datamaps
// have changed and now contain the required information
refreshMappingCache();
result = mappingCache.getQueryDescriptor(name);
}
return result;
}
/**
* @since 3.0
*/
public Embeddable getEmbeddable(String className) {
checkMappingCache();
Embeddable result = mappingCache.getEmbeddable(className);
if (result == null) {
// reconstruct cache just in case some of the datamaps
// have changed and now contain the required information
refreshMappingCache();
result = mappingCache.getEmbeddable(className);
}
return result;
}
/**
* @since 3.0
*/
public SQLResult getResult(String name) {
checkMappingCache();
SQLResult result = mappingCache.getResult(name);
if (result == null) {
// reconstruct cache just in case some of the datamaps
// have changed and now contain the required information
refreshMappingCache();
result = mappingCache.getResult(name);
}
return result;
}
/**
* Returns ClassDescriptor for the ObjEntity matching the name. Returns null
* if no matching entity exists.
*
* @since 1.2
*/
public ClassDescriptor getClassDescriptor(String entityName) {
if (entityName == null) {
throw new IllegalArgumentException("Null entityName");
}
return getClassDescriptorMap().getDescriptor(entityName);
}
public synchronized void addDataMap(DataMap map) {
if (!maps.contains(map)) {
maps.add(map);
map.setNamespace(this);
refreshMappingCache();
}
}
/**
* Removes all entity mappings from the cache.
*
* @deprecated since 4.0 in favor of {@link #refreshMappingCache()}.
*/
@Deprecated
public void clearCache() {
refreshMappingCache();
}
private void checkMappingCache() {
if (mappingCache == null) {
refreshMappingCache();
}
}
/**
* Refreshes entity cache to reflect the current state of the DataMaps in
* the EntityResolver.
*
* @since 4.0
*/
public void refreshMappingCache() {
mappingCache = new ProxiedMappingNamespace() {
@Override
protected MappingCache createDelegate() {
return new MappingCache(maps);
}
};
clientEntityResolver = null;
}
/**
* Returns a DataMap matching the name.
*/
public DataMap getDataMap(String mapName) {
if (mapName == null) {
return null;
}
for (DataMap map : maps) {
if (mapName.equals(map.getName())) {
return map;
}
}
return null;
}
public synchronized void setDataMaps(Collection<DataMap> maps) {
this.maps.clear();
this.maps.addAll(maps);
refreshMappingCache();
}
/**
* Returns an unmodifiable collection of DataMaps.
*/
public Collection<DataMap> getDataMaps() {
return Collections.unmodifiableCollection(maps);
}
/**
* @since 4.0
*/
public EntityInheritanceTree getInheritanceTree(String entityName) {
checkMappingCache();
EntityInheritanceTree tree = mappingCache.getInheritanceTree(entityName);
if (tree == null) {
// since we keep inheritance trees for all entities, null means
// unknown entity...
// rebuild cache just in case some of the datamaps
// have changed and now contain the required information
refreshMappingCache();
tree = mappingCache.getInheritanceTree(entityName);
}
return tree;
}
/**
* @deprecated since 4.0 use {@link #getInheritanceTree(String)}.
*/
@Deprecated
public EntityInheritanceTree lookupInheritanceTree(String entityName) {
return getInheritanceTree(entityName);
}
/**
* Looks in the DataMap's that this object was created with for the
* ObjEntity that maps to the services the specified class
*
* @return the required ObjEntity or null if there is none that matches the
* specifier
*
* @since 4.0
*/
public ObjEntity getObjEntity(Class<?> entityClass) {
checkMappingCache();
ObjEntity result = mappingCache.getObjEntity(entityClass);
if (result == null) {
// reconstruct cache just in case some of the datamaps
// have changed and now contain the required information
refreshMappingCache();
result = mappingCache.getObjEntity(entityClass);
}
return result;
}
/**
* <p>
* Looks in the DataMap's that this object was created with for the
* ObjEntity that maps to the services the specified class, with option to
* fallback to search by name with client resolver in case entity not found.
* </p>
* <p>
* This method can be used where entity class can be received from client.
* </p>
*
* @param entityClass entity class to search
* @param lookupClientResolver flag to fallback to client resolver
* @return the required ObjEntity or null if there is none that matches the
* specifier
*
* @since 4.0
*/
public ObjEntity getObjEntity(Class<?> entityClass, boolean lookupClientResolver) {
ObjEntity entity = getObjEntity(entityClass);
if(entity != null || !lookupClientResolver) {
return entity;
}
EntityResolver clientResolver = getClientEntityResolver();
if (clientResolver != this) {
ObjEntity clientEntity = clientResolver.getObjEntity(entityClass);
entity = clientEntity == null ? null : getObjEntity(clientEntity.getName());
}
return entity;
}
/**
* @deprecated since 4.0, use {@link #getObjEntity(Class)}.
*/
public ObjEntity lookupObjEntity(Class<?> entityClass) {
return getObjEntity(entityClass);
}
public ObjEntity getObjEntity(Persistent object) {
checkMappingCache();
return mappingCache.getObjEntity(object);
}
/**
* Looks in the DataMap's that this object was created with for the
* ObjEntity that services the specified data Object
*
* @return the required ObjEntity, or null if none matches the specifier
* @since 4.0 a corresponding getObjEntity method should be used.
*/
@Deprecated
public ObjEntity lookupObjEntity(Object object) {
if (object instanceof ObjEntity) {
return (ObjEntity) object;
}
if (object instanceof Persistent) {
ObjectId id = ((Persistent) object).getObjectId();
if (id != null) {
return getObjEntity(id.getEntityName());
}
} else if (object instanceof Class) {
return getObjEntity((Class<?>) object);
}
return getObjEntity(object.getClass());
}
/**
* @deprecated since 4.0. Use q.getMetaData(resolver).getProcedure()
*/
@Deprecated
public Procedure lookupProcedure(Query q) {
return q.getMetaData(this).getProcedure();
}
/**
* @deprecated since 4.0 use {@link #getProcedure(String)}.
*/
@Deprecated
public Procedure lookupProcedure(String procedureName) {
return getProcedure(procedureName);
}
public synchronized void removeDataMap(DataMap map) {
if (maps.remove(map)) {
refreshMappingCache();
}
}
/**
* @deprecated since 4.0. There's no replacement. This property is
* meaningless and is no longer respected by the code.
*/
@Deprecated
public boolean isIndexedByClass() {
return indexedByClass;
}
/**
* @deprecated since 4.0. There's no replacement. This property is
* meaningless.
*/
public void setIndexedByClass(boolean b) {
indexedByClass = b;
}
/**
* Returns an object that compiles and stores {@link ClassDescriptor}
* instances for all entities.
*
* @since 3.0
*/
public ClassDescriptorMap getClassDescriptorMap() {
if (classDescriptorMap == null) {
synchronized (this) {
if (classDescriptorMap == null) {
ClassDescriptorMap classDescriptorMap = new ClassDescriptorMap(this);
FaultFactory faultFactory = new SingletonFaultFactory();
// add factories in reverse of the desired chain order
classDescriptorMap.addFactory(new ValueHolderDescriptorFactory(classDescriptorMap));
classDescriptorMap.addFactory(new DataObjectDescriptorFactory(classDescriptorMap, faultFactory));
// since ClassDescriptorMap is not synchronized, we need to
// prefill
// it with entity proxies here.
for (DataMap map : maps) {
for (String entityName : map.getObjEntityMap().keySet()) {
classDescriptorMap.getDescriptor(entityName);
}
}
this.classDescriptorMap = classDescriptorMap;
}
}
}
return classDescriptorMap;
}
/**
* @since 3.0
* @deprecated since 4.0 this method does nothing, as EntityResolver no
* longer loads listeners from its DataMaps.
*/
@Deprecated
public void setEntityListenerFactory(EntityListenerFactory entityListenerFactory) {
// noop
}
/**
* Java default deserialization seems not to invoke constructor by default -
* invoking it manually
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
refreshMappingCache();
}
public ValueObjectTypeRegistry getValueObjectTypeRegistry() {
return valueObjectTypeRegistry;
}
public void setValueObjectTypeRegistry(ValueObjectTypeRegistry valueObjectTypeRegistry) {
this.valueObjectTypeRegistry = valueObjectTypeRegistry;
}
}