/*
* Copyright 2006-2014 University of Dundee. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.tools.hibernate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.hibernate.EntityMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.type.CollectionType;
import org.hibernate.type.ComponentType;
import org.hibernate.type.EmbeddedComponentType;
import org.hibernate.type.EntityType;
import org.hibernate.type.Type;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.ContextRefreshedEvent;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import ome.conditions.ApiUsageException;
import ome.conditions.InternalException;
import ome.model.IAnnotated;
import ome.model.IGlobal;
import ome.model.IObject;
import ome.model.annotations.Annotation;
import ome.model.internal.Permissions;
import ome.tools.spring.OnContextRefreshedEventListener;
/**
* extension of the model metadata provided by {@link SessionFactory}. During
* construction, the metadata is created and cached for later use.
*
* @author Josh Moore, josh.moore at gmx.de
* @see SessionFactory
* @since 3.0-M3
*/
public interface ExtendedMetadata {
Set<String> getClasses();
/**
* Returns all the classes which implement {@link IAnnotated}
* @return the annotatable types
*/
Set<Class<IAnnotated>> getAnnotatableTypes();
/**
* Returns all the classes which subclass {@link Annotation}
* @return the types of annotation
*/
Set<Class<Annotation>> getAnnotationTypes();
/**
* Returns the query for obtaining the number of collection items to a
* particular instance. All such queries will return a ResultSet with rows
* of the form: 0 (Long) id of the locked class 1 (Long) count of the
* instances locking that class
*
* @param field
* Field name as specified in the class.
* @return String query. Never null.
* @throws ApiUsageException
* if return value would be null.
*/
String getCountQuery(String field) throws ApiUsageException;
/**
* Given the name of a database table or alternatively the simple class name
* (non-fully qualified) of an IObject, this method returns the class which
* Hibernate will map that table to.
* @param table a database table name, or simple class name of a model object
* @return the corresponding mapped class
*/
Class<IObject> getHibernateClass(String table);
/**
* walks the {@link IObject} argument <em>non-</em>recursively and gathers
* all {@link IObject} instances which will be linked to by the
* creation or updating of the argument. (Previously this was called "locking"
* since a flag was set on the object to mark it as linked, but this was
* removed in 4.2)
*
* @param iObject
* A newly created or updated {@link IObject} instance which
* might possibly lock other {@link IObject IObjects}. A null
* argument will return an empty array to be checked.
* @return A non-null array of {@link IObject IObjects} which will be linked to.
*/
IObject[] getLockCandidates(IObject iObject);
/**
* Rather than iterating over an {@link IObject} like
* {@link #getLockCandidates(IObject)} this method returns type/field name
* pairs (like {@link #getLockChecks(Class)}) to allow performing the
* queries manually.
*
* If onlyWithGroups is true, then only checks which point to non-IGlobal
* objects will be returned.
*
* @param klass Not null.
* @param onlyWithGroups if should omit checks that point to {@link IGlobal}s
* @return the lock candidates for checking
*/
String[][] getLockCandidateChecks(Class<? extends IObject> klass, boolean onlyWithGroups);
/**
* returns all class/field name pairs which may possibly link to an object
* of type <code>klass</code>.
*
* @param klass
* Non-null {@link Class subclass} of {@link IObject}
* @return A non-null array of {@link String} queries which can be used to
* determine if an {@link IObject} instance can be unlocked.
*/
String[][] getLockChecks(Class<? extends IObject> klass);
/**
* Takes the lock checks returned by {@link #getLockChecks(Class)} and
* performs the actual check returning a map from class to total number
* of locks. The key "*" contains the total value.
*
* If the id argument is null, then checks will be against all rows rather
* than individual objects, e.g.
* <pre>
* select count(x) from Linker x, Linked y where x.$FIELD.id = y.id $CLAUSE;
* </pre>
* otherwise
* <pre>
* select count(x) from Linker x where x.$FIELD.id = :id $CLAUSE'
* </pre>
*
* If the clause argument is null or empty it will be omitted.
*/
Map<String, Long> countLocks(Session session, Long id, String[][] lockChecks, String clause);
/**
* Walks the data on what locks what
* for "from" argument to see if there is any direct relationship to the
* "to" argument. If there is, the name will be returned. Otherwise, null.
*/
String getRelationship(String from, String to);
/**
* provides the link between two tables similar to
* {link {@link #getRelationship(String, String)}. However, whereas
* {@link #getRelationship(String, String)} needs to be called twice, once
* for each of the Hibernate directions,
* {@link #getSQLJoin(String, String, String, String)} need only ever be
* called once since there will be only one correct SQL join.
*
* For example, getRelationship("Image", "DatasetImageLink") returns
* "datasetLinks" while getRelationship("DatasetImageLink", "Image")
* returns "child". getSQLJoin("Image", "I", "DatasetImageLink", "L"),
* however, will always return "I.id = L.child" (though the order may be
* reversed).
*/
String getSQLJoin(String fromType, String fromAlias, String toType, String toAlias);
/**
* Check if an object of this class may have map properties.
* @param iObjectClass a class
* @return if this object or any of its mapped subclasses have any map properties
*/
boolean mayHaveMapProperties(Class<? extends IObject> iObjectClass);
/**
* Get the names of any String→RString map properties this class has, otherwise an empty set if none.
* @param className the name of a class, as from {@link Class#getName()}
* @return the class' map property names
*/
Set<String> getMapProperties(String className);
/**
* Sole implementation of ExtendedMetadata. The separation is intended to make
* unit testing without a full {@link ExtendedMetadata} possible.
*/
public static class Impl extends OnContextRefreshedEventListener implements ExtendedMetadata {
private final static Logger log = LoggerFactory.getLogger(ExtendedMetadata.class);
private final Map<String, Locks> locksHolder = new HashMap<String, Locks>();
private final Map<String, String[][]> lockedByHolder = new HashMap<String, String[][]>();
private final Map<String, Immutables> immutablesHolder = new HashMap<String, Immutables>();
private final Map<String, String> collectionCountHolder = new HashMap<String, String>();
private final Map<String, Class<IObject>> targetHolder = new HashMap<String, Class<IObject>>();
private final Set<Class<IAnnotated>> annotatableTypes = new HashSet<Class<IAnnotated>>();
private final Set<Class<Annotation>> annotationTypes = new HashSet<Class<Annotation>>();
private final Map<String, Map<String, Relationship>> relationships = new HashMap<String, Map<String, Relationship>>();
private final Map<String, Class<IObject>> hibernateClasses = new HashMap<String, Class<IObject>>();
private final Set<Class<?>> mapPropertyClasses = new HashSet<Class<?>>();
private final SetMultimap<String, String> mapProperties = HashMultimap.create();
private boolean initialized = false;
// NOTES:
// TODO we could just delegate to sf and implement the same interface.
// TOTEST
// will need to get collection items out. // no filtering.
// will also need to do the same for checkIfNeedLock once we have
// a collection valued (non-association) type. (does that exist??)
// will also need to handle components if we have any other than details.
// Doesn't handle ComponentTypes
/**
* Listener method which waits for a {@link ContextRefreshedEvent} and then
* extracts the {@link SessionFactory} from the {@link ApplicationContext}
* and pases it to {@link #setSessionFactory(SessionFactory)}.
*/
@Override
public void handleContextRefreshedEvent(ContextRefreshedEvent cre) {
ApplicationContext ctx = cre.getApplicationContext();
if (ctx.containsBean("sessionFactory")) {
SessionFactory sessionFactory = (SessionFactory) ctx
.getBean("sessionFactory");
setSessionFactory(sessionFactory);
} else {
log.warn("No session factory found. Cannot initialize");
}
}
/**
* Initializes the metadata needed by this instance.
*
* @param sessionFactory the Hibernate session factory
* @see org.hibernate.SessionFactory#getAllClassMetadata()
*/
public void setSessionFactory(SessionFactory sessionFactory) {
if (initialized) {
return; // EARLY EXIT !!
}
log.info("Calculating ExtendedMetadata...");
SessionFactoryImplementor sfi = (SessionFactoryImplementor) sessionFactory;
Map<String, ClassMetadata> m = sessionFactory.getAllClassMetadata();
// do Locks() first because they are used during the
// calculation of LockedBy()
for (String key : m.keySet()) {
ClassMetadata cm = m.get(key);
locksHolder.put(key, new Locks(cm));
}
// now that all Locks() are available, determine LockedBy()
for (String key : m.keySet()) {
lockedByHolder.put(key, lockedByFields(key, m));
}
for (String key : m.keySet()) {
ClassMetadata cm = m.get(key);
immutablesHolder.put(key, new Immutables(cm));
}
for (Map.Entry<String, ClassMetadata> entry : m.entrySet()) {
String key = entry.getKey();
ClassMetadata cm = entry.getValue();
key = key.substring(key.lastIndexOf(".") + 1).toLowerCase();
if (hibernateClasses.containsKey(key)) {
throw new RuntimeException("Duplicate keys!: " + key);
}
hibernateClasses.put(key, cm.getMappedClass(EntityMode.POJO));
}
for (String key : m.keySet()) {
Map<String, Relationship> value = new HashMap<String, Relationship>();
ClassMetadata cm = m.get(key);
for (Class<?> c : hierarchy(m, key)) {
Locks locks = locksHolder.get(c.getName());
locks.fillRelationships(sfi, value);
}
// FIXME: using simple name rather than FQN
Map<String, Relationship> value2 = new HashMap<String, Relationship>();
for (Map.Entry<String, Relationship> i : value.entrySet()) {
String k = i.getKey();
k = k.substring(k.lastIndexOf(".")+1);
value2.put(k, i.getValue());
}
relationships.put(key.substring(key.lastIndexOf(".")+1), value2);
/* note map properties */
boolean hasMapProperty = false;
final String[] propertyNames = cm.getPropertyNames();
final Type[] propertyTypes = cm.getPropertyTypes();
for (int i = 0; i < propertyNames.length; i++) {
if (propertyTypes[i] instanceof CollectionType && Map.class == propertyTypes[i].getReturnedClass()) {
final CollectionType propertyType = (CollectionType) propertyTypes[i];
final Type elementType = propertyType.getElementType((SessionFactoryImplementor) sessionFactory);
if (String.class == elementType.getReturnedClass()) {
mapProperties.put(key, propertyNames[i]);
hasMapProperty = true;
}
}
}
if (hasMapProperty) {
for (Class<?> mc = cm.getMappedClass(EntityMode.POJO); mc != null; mc = mc.getSuperclass()) {
mapPropertyClasses.add(mc);
}
}
}
Set<Class<IAnnotated>> anns = new HashSet<Class<IAnnotated>>();
Set<Class<Annotation>> anns2 = new HashSet<Class<Annotation>>();
for (String key : m.keySet()) {
ClassMetadata cm = m.get(key);
Map<String, String> queries = countQueriesAndEditTargets(key,
lockedByHolder.get(key));
collectionCountHolder.putAll(queries);
// Checking classes, specifically for ITypes
Class c = cm.getMappedClass(EntityMode.POJO);
if (IAnnotated.class.isAssignableFrom(c)) {
anns.add(c);
}
if (Annotation.class.isAssignableFrom(c)) {
anns2.add(c);
}
}
annotatableTypes.addAll(anns);
annotationTypes.addAll(anns2);
initialized = true;
}
public Set<String> getClasses() {
return locksHolder.keySet();
}
public Class<IObject> getHibernateClass(String table) {
int idx = table.lastIndexOf(".");
if (idx > 0) {
table = table.substring(idx+1);
}
table = table.toLowerCase();
return hibernateClasses.get(table);
}
private Relationship _getRelationship(String from, String to) {
Map<String, Relationship> m = relationships.get(from);
if (m != null) {
return m.get(to);
}
return null;
}
/**
* Walks the data on what locks what
* for "from" argument to see if there is any direct relationship to the
* "to" argument. If there is, the name will be returned. Otherwise, null.
*/
public String getRelationship(String from, String to) {
Relationship r = _getRelationship(from, to);
if (r != null) {
return r.relationshipName;
}
return null;
}
/**
* Note: this implementation does not yet take into account the mapping
* of joined subclasses like Job->UpdateJob.
*/
public String getSQLJoin(String fromType, String fromAlias, String toType, String toAlias) {
String fromPath = "UNKNOWN";
String toPath = "UNKNOWN";
final Relationship fromRel = _getRelationship(fromType, toType);
final Relationship toRel = _getRelationship(toType, fromType);
if (fromRel != null && !fromRel.collection) {
fromPath = fromRel.relationshipName;
toPath = "id";
} else if (toRel != null && !toRel.collection) {
toPath = toRel.relationshipName;
fromPath = "id";
} else {
StringBuilder sb = new StringBuilder();
sb.append("fromType=");
sb.append(fromType);
sb.append(";toType=");
sb.append(toType);
throw new InternalException("Unhandled SQL Join! -- " + sb);
}
return String.format("%s.%s = %s.%s",
fromAlias, fromPath, toAlias, toPath);
}
public Set<Class<IAnnotated>> getAnnotatableTypes() {
return Collections.unmodifiableSet(annotatableTypes);
}
public Set<Class<Annotation>> getAnnotationTypes() {
return Collections.unmodifiableSet(annotationTypes);
}
/**
* walks the {@link IObject} argument <em>non-</em>recursively and gathers
* all {@link IObject} instances which will be linked to by the
* creation or updating of the argument. (Previously this was called "locking"
* since a flag was set on the object to mark it as linked, but this was
* removed in 4.2)
*
* @param iObject
* A newly created or updated {@link IObject} instance which
* might possibly lock other {@link IObject IObjects}. A null
* argument will return an empty array to be checked.
* @return A non-null array of {@link IObject IObjects} which will be linked to.
*/
public IObject[] getLockCandidates(IObject iObject) {
if (iObject == null) {
return new IObject[] {};
}
Locks l = locksHolder.get(iObject.getClass().getName());
return l.getLockCandidates(iObject);
}
public String[][] getLockCandidateChecks(Class<? extends IObject> k, boolean onlyWithGroups) {
Locks l = locksHolder.get(k.getName());
return l.getLockCandidateChecks(onlyWithGroups);
}
/**
* returns all class/field name pairs which may possibly link to an object
* of type <code>klass</code>.
*
* @param klass
* Non-null {@link Class subclass} of {@link IObject}
* @return A non-null array of {@link String} queries which can be used to
* determine if an {@link IObject} instance can be unlocked.
*/
public String[][] getLockChecks(Class<? extends IObject> klass) {
if (klass == null) {
throw new ApiUsageException("Cannot proceed with null klass.");
}
String[][] checks = lockedByHolder.get(klass.getName());
if (checks == null) {
throw new ApiUsageException("Metadata not found for: "
+ klass.getName());
}
return checks;
}
public Map<String, Long> countLocks(final Session session, final Long id,
String[][] checks, String clause) {
final QueryBuilder qb = new QueryBuilder();
qb.select("count(x.id)");
qb.from("%s", "x");
// Only one of the these two will happen, so the second replacement
// argument to String.format will have to be check[1].
if (id == null) {
qb.join("x.%s", "y", false, false);
}
if (id != null) {
qb.where();
qb.and("%s.id = :id");
}
if (clause != null && clause.length() > 0) {
qb.where();
qb.and(clause);
qb.appendSpace();
}
final String queryString = qb.queryString();
final Map<String, Long> counts = new HashMap<String, Long>();
long total = 0L;
// run the individual queries
for (final String[] check : checks) {
final String hql = String.format(queryString, check[0], check[1]);
org.hibernate.Query q = session.createQuery(hql);
if (id != null) {
q.setLong("id", id);
}
Long count = (Long) q.uniqueResult();
if (count != null && count.longValue() > 0) {
total += count;
counts.put(check[0], count);
}
}
counts.put("*", total);
return counts;
}
public String[] getImmutableFields(Class<? extends IObject> klass) {
if (klass == null) {
throw new ApiUsageException("Cannot proceed with null klass.");
}
Immutables i = immutablesHolder.get(klass.getName());
return i.getImmutableFields();
}
private final static String field_msg = " is not a valid field for counting. Make sure you use "
+ "the single-valued (e.g. ImageAnnotation.IMAGE) and "
+ "not the collection-valued (e.g. Image.ANNOTATIONS) end.";
/**
* Returns the query for obtaining the number of collection items to a
* particular instance. All such queries will return a ResultSet with rows
* of the form: 0 (Long) id of the locked class 1 (Long) count of the
* instances locking that class
*
* @param field
* Field name as specified in the class.
* @return String query. Never null.
* @throws ApiUsageException
* if return value would be null.
*/
public String getCountQuery(String field) throws ApiUsageException {
String q = collectionCountHolder.get(field);
if (q == null) {
throw new ApiUsageException(field + field_msg);
}
return q;
}
/**
* Returns the {@link IObject} type which a given field points to. E.g.
* getTargetType(ImageAnnotation.IMAGE) returns Image.class.
*/
public Class<IObject> getTargetType(String field) throws ApiUsageException {
Class<IObject> k = targetHolder.get(field);
if (k == null) {
throw new ApiUsageException(field + field_msg);
}
return k;
}
@Override
public boolean mayHaveMapProperties(Class<? extends IObject> iObjectClass) {
return mapPropertyClasses.contains(iObjectClass);
}
@Override
public Set<String> getMapProperties(String className) {
return mapProperties.get(className);
}
// ~ Helpers
// =========================================================================
/**
* examines all model objects to see which fields contain a {@link Type}
* which points to this class. Uses {@link #locksFields(Type[])} since this
* is the inverse process.
*/
private String[][] lockedByFields(String klass, Map<String, ClassMetadata> m) {
if (m == null) {
throw new InternalException("ClassMetadata map cannot be null.");
}
List<String[]> fields = new ArrayList<String[]>();
for (String k : m.keySet()) {
ClassMetadata cm = m.get(k);
Type[] type = cm.getPropertyTypes();
String[] names = cm.getPropertyNames();
Locks inverse = locksHolder.get(k);
for (int i = 0; i < inverse.size(); i++) {
if (!inverse.include(i)) {
continue;
}
// this is an embedded component and must be treated
// specially. specifically, that we cannot compare against
// the top-level returnedClass name but rather against
// each of the individual subtype returnedClass names.
if (inverse.hasSubtypes(i)) {
for (int j = 0; j < inverse.numberOfSubtypes(i); j++) {
if (inverse.subtypeEquals(i, j, klass)) {
fields.add(new String[] { k,
inverse.subtypeName(i, j) });
}
}
}
// no subtypes so can compare directly
else if (klass.equals(type[i].getReturnedClass().getName())) {
fields.add(new String[] { k, names[i] });
}
}
}
return fields.toArray(new String[fields.size()][2]);
}
/**
* Pre-builds all queries for checking the count of collections based on the
* field names as defined in the ome.model.* classes.
*/
@SuppressWarnings("unchecked")
private Map<String, String> countQueriesAndEditTargets(String type,
String[][] lockedBy) {
Map<String, String> queries = new HashMap<String, String>();
for (int t = 0; t < lockedBy.length; t++) {
String ltype = lockedBy[t][0];
String lfield = lockedBy[t][1];
String field_description = String.format("%s_%s", ltype, lfield);
queries.put(field_description, String.format(
"select target.%s.id, count(target) "
+ "from %s target group by target.%s.id", lfield,
ltype, lfield));
try {
targetHolder.put(field_description, (Class<IObject>) Class
.forName(type));
} catch (Exception e) {
throw new RuntimeException("Error getting class: " + ltype, e);
}
}
return queries;
}
/**
* Takes a class name as a string and find that class and all of its
* subclasses (transitively) returns them as a list.
*/
private static List<Class<?>> hierarchy(Map<String, ClassMetadata> m, String key) {
final List<Class<?>> h = new ArrayList<Class<?>>();
ClassMetadata cm = m.get(key);
Class<?> c = cm.getMappedClass(EntityMode.POJO);
h.add(c);
int index = 0;
while (index < h.size()) {
for (String key2 : m.keySet()) {
if (key.equals(key2)) {
continue;
} else {
cm = m.get(key2);
c = cm.getMappedClass(EntityMode.POJO);
if (c.getSuperclass().equals(h.get(index))) {
h.add(c);
}
}
}
index++;
}
return h;
}
}
/**
* Simple value class to maintain all of the state for use by
* {@link ExtendedMetadata#getRelationship(String, String)} and
* {@link ExtendedMetadata#getSQLJoin(String, String, String, String)}.
*/
class Relationship {
/**
* Value to be returned by {@link ExtendedMetadata#getRelationship(String, String)}.
*/
private final String relationshipName;
/**
* Whether or not the given relationship is the many valued side, i.e.
* represents a collection. In this case, there will not be a column
* of the given name to join on for
* {@link ExtendedMetadata#getSQLJoin(String, String, String, String)}.
*/
private final boolean collection;
Relationship(String name, boolean collection) {
this.relationshipName = name;
this.collection = collection;
}
}
/**
* inner class which wraps the information (index number, path, etc) related to
* what fields a particular object can lock. This is fairly complicated because
* though the properties available are a simple array, some of those properties
* can actually be embedded components, meaning that the value of the property
* is the instance itself. In those cases (where {@link #hasSubtypes(int)} is
* true, special logic must be implemented to retrieve the proper values.
*/
class Locks {
private final ClassMetadata cm;
private final int size;
private int total = 0;
private final boolean[] include;
private final String[][] subnames;
private final Type[][] subtypes;
private final String[][] checks;
private final String[][] groupChecks;
/**
* examines all {@link Type types} for this class and stores pointers to
* those fields which represent {@link IObject} instances. These fields may
* need to be locked when an object of this type is created or updated.
*/
Locks(ClassMetadata classMetadata) {
this.cm = classMetadata;
String[] name = cm.getPropertyNames();
Type[] type = cm.getPropertyTypes();
List<String[]> checks = new ArrayList<String[]>();
List<String[]> groupChecks = new ArrayList<String[]>();
this.size = type.length;
this.include = new boolean[size];
this.subnames = new String[size][];
this.subtypes = new Type[size][];
for (int i = 0; i < type.length; i++) {
if (type[i].isComponentType()
&& ((ComponentType) type[i]).isEmbedded()) {
EmbeddedComponentType embedded = (EmbeddedComponentType) type[i];
String[] sub_name = embedded.getPropertyNames();
Type[] sub_type = embedded.getSubtypes();
List<String> name_list = new ArrayList<String>();
List<Type> type_list = new ArrayList<Type>();
for (int j = 0; j < sub_type.length; j++) {
if (IObject.class.isAssignableFrom(sub_type[j]
.getReturnedClass())) {
String path = name[i] + "." + sub_name[j];
name_list.add(path);
type_list.add(sub_type[j]);
addCheck(checks, groupChecks, sub_type[j].getReturnedClass(), path);
}
}
add(i, name_list.toArray(new String[name_list.size()]),
type_list.toArray(new Type[type_list.size()]));
} else if (IObject.class.isAssignableFrom(type[i]
.getReturnedClass())) {
add(i);
addCheck(checks, groupChecks, type[i].getReturnedClass(), name[i]);
// Create checks for
}
}
this.checks = checks.toArray(new String[checks.size()][]);
this.groupChecks = groupChecks.toArray(new String[groupChecks.size()][]);
}
private void addCheck(List<String[]> checks, List<String[]> groupChecks,
Class type, String field) {
String[] s = new String[]{type.getName(), field};
checks.add(s);
if (!IGlobal.class.isAssignableFrom(type)) {
groupChecks.add(s);
}
}
private void add(int i) {
if (i >= size) {
throw new IllegalArgumentException("size");
}
if (this.include[i] == true) {
throw new IllegalStateException("set");
}
this.include[i] = true;
this.subnames[i] = new String[] {};
this.subtypes[i] = new Type[] {};
total++;
}
private void add(int i, String[] paths, Type[] types) {
if (i >= size) {
throw new IllegalArgumentException("size");
}
if (paths == null) {
throw new IllegalArgumentException("paths");
}
if (types == null) {
throw new IllegalArgumentException("types");
}
if (paths.length != types.length) {
throw new IllegalStateException("size");
}
if (this.include[i] == true) {
throw new IllegalStateException("set");
}
if (paths.length > 0) {
this.include[i] = true;
this.subnames[i] = paths;
this.subtypes[i] = types;
total += paths.length;
}
}
// ~ Main method
// =========================================================================
/**
* For each of the fields contained in this {@link Locks} object, parse
* out the type and the field name and store those as the key and value
* in the "value" argument.
*/
public void fillRelationships(SessionFactoryImplementor sfi,
Map<String, Relationship> value) {
final Type[] types = cm.getPropertyTypes();
for (int t = 0; t < types.length; t++) {
final Type type = types[t];
final String name = type.getName();
String to = null;
Relationship field = null;
if (type instanceof EntityType) {
final EntityType entType = (EntityType) type;
to = entType.getAssociatedEntityName();
field = new Relationship(cm.getPropertyNames()[t], false);
} else if (types[t] instanceof CollectionType) {
final CollectionType colType = (CollectionType)types[t];
final Type elemType = colType.getElementType(sfi);
if (!elemType.isEntityType()) {
continue; // The case for count maps and other primitives.
}
to = elemType.getName();
int open = name.indexOf("(");
int close = name.lastIndexOf(")");
String role = name.substring(open + 1, close);
int dot = role.lastIndexOf(".");
field = new Relationship(role.substring(dot+1), true);
}
if (to != null && field != null) {
Map<String, ClassMetadata> m = sfi.getAllClassMetadata();
for (Class<?> c : Impl.hierarchy(m, to)) {
value.put(c.getName(), field);
}
}
}
}
public IObject[] getLockCandidates(IObject o) {
int idx = 0;
IObject[] toCheck = new IObject[total()];
Object[] values = cm.getPropertyValues(o, EntityMode.POJO);
for (int i = 0; i < size(); i++) {
if (!include(i)) {
continue;
}
// this relation has subtypes and therefore is an embedded
// component. This means that the value in values[] is the
// instance itself. we will now have to acquire the actual
// component values.
if (hasSubtypes(i)) {
for (int j = 0; j < numberOfSubtypes(i); j++) {
Object value = getSubtypeValue(i, j, o);
if (value != null) {
toCheck[idx++] = (IObject) value;
}
}
}
// this is a regular relation. if the value is non null,
// add it to the list of candidates.
else if (values[i] != null) {
toCheck[idx++] = (IObject) values[i];
}
}
IObject[] retVal;
retVal = new IObject[idx];
System.arraycopy(toCheck, 0, retVal, 0, idx);
return retVal;
}
public String[][] getLockCandidateChecks(boolean onlyWithGroups) {
if (onlyWithGroups) {
return groupChecks;
}
return checks;
}
// ~ Public
// =========================================================================
// public methods. know nothing about the arrays above. have only a
// linear view of the contained values.
/**
* the total number of fields for this entity. The actual number of
* {@link IObject} instances may vary since (1) some fields (like embedded
* components) can possibly point to multiple instances. See
* {@link #total()} for the final size and (2) some fields do not need to be
* examined (Integers, e.g.). See {@link #include}
* @return how many fields this entity has
*/
public int size() {
return size;
}
/**
* as opposed to {@link #size()}, the returns the actual number of fields
* that will need to be checked.
* @return how many fields must be checked for this entity
*/
public int total() {
return total;
}
/**
* returns true if this offset points to a field which may contain an
* {@link IObject} instance
*/
public boolean include(int i) {
return include[i];
}
// ~ Subtypes
// =========================================================================
/**
* returns true if this offset points to a field which is an embedded
* component.
*/
public boolean hasSubtypes(int i) {
return include(i) && subtypes[i].length > 0;
}
/**
* returns the number of subtypes for iterating over this secondary array.
* If there are no subtypes, this method will return zero. Use
* {@link #hasSubtypes(int)} to differentiate the two situations.
*/
public int numberOfSubtypes(int i) {
return hasSubtypes(i) ? subtypes[i].length : 0;
}
/**
* uses the {@link ClassMetadata} for this {@link Locks} tp retrieve the
* component value.
*/
public Object getSubtypeValue(int i, int j, Object o) {
return cm.getPropertyValue(o, subnames[i][j], EntityMode.POJO);
}
/**
* returns true is the indexed subtype returns the same class type as the
* klass argument.
*/
public boolean subtypeEquals(int i, int j, String klass) {
return klass.equals(subtypes[i][j].getReturnedClass().getName());
}
/** retrieves the full Hibernate path for this component field. */
public String subtypeName(int i, int j) {
return subnames[i][j];
}
}
class Immutables {
String[] immutableFields;
EntityPersister ep;
Immutables(ClassMetadata metadata) {
if (metadata instanceof EntityPersister) {
this.ep = (EntityPersister) metadata;
} else {
throw new IllegalArgumentException("Metadata passed to Immutables"
+ " must be an instanceof EntityPersister, not "
+ (metadata == null ? null : metadata.getClass()));
}
List<String> retVal = new ArrayList<String>();
Type[] type = this.ep.getPropertyTypes();
String[] name = this.ep.getPropertyNames();
boolean[] up = this.ep.getPropertyUpdateability();
for (int i = 0; i < type.length; i++) {
// not updateable, so our work (for this type) is done.
if (!up[i]) {
retVal.add(name[i]);
}
// updateable, but maybe a component subtype is NOT.
else if (type[i].isComponentType()
&& ((ComponentType) type[i]).isEmbedded()) {
EmbeddedComponentType embedded = (EmbeddedComponentType) type[i];
String[] sub_name = embedded.getPropertyNames();
Type[] sub_type = embedded.getSubtypes();
List<String> name_list = new ArrayList<String>();
List<Type> type_list = new ArrayList<Type>();
for (int j = 0; j < sub_type.length; j++) {
// INCOMPLETE !!!
}
}
}
immutableFields = retVal.toArray(new String[retVal.size()]);
}
public String[] getImmutableFields() {
return immutableFields;
}
}
}