diff --git a/framework/src/play/db/Model.java b/framework/src/play/db/Model.java index efa95e9537..e88923f4d6 100644 --- a/framework/src/play/db/Model.java +++ b/framework/src/play/db/Model.java @@ -3,6 +3,8 @@ import java.io.InputStream; import java.lang.reflect.Field; import java.util.List; +import java.util.Map; + import play.Play; import play.PlayPlugin; import play.exceptions.UnexpectedException; @@ -22,6 +24,7 @@ public static class Property { public boolean isMultiple; public boolean isRelation; public boolean isGenerated; + public boolean isKey; public Class relationType; public Choices choices; @@ -35,15 +38,41 @@ public static interface Choices { public static interface Factory { + /** + * Returns the list of properties for this factory's type. + */ + public List listProperties(); + /** + * Returns the list of key properties for this factory's type. + */ + public List listKeys(); + /** + * @deprecated this only works for single non-composite keys. It will throw an exception in every other case. Use listKeys(). + */ + @Deprecated public String keyName(); + /** + * Returns the type of key. For a single key it will be the key's field's type. For a composite key this will be the type + * of the composite key (as specified by @IdClass) + */ public Class keyType(); + /** + * Returns the key value. For a single key it will return the key's field. For a composite key this will return an instance of the type + * of the composite key (as specified by @IdClass) + */ public Object keyValue(Model m); + /** + * Makes a key valid for this factory's type, with all the given components of this key taken from a map. + */ + public Object makeKey(Map id); public Model findById(Object id); + /** + * Returns true if this factory's type has a generated key, false if the user can set the key himself. + */ + public boolean isGeneratedKey(); public List fetch(int offset, int length, String orderBy, String orderDirection, List properties, String keywords, String where); public Long count(List properties, String keywords, String where); public void deleteAll(); - public List listProperties(); - } public static class Manager { diff --git a/framework/src/play/db/jpa/GenericModel.java b/framework/src/play/db/jpa/GenericModel.java index 4981e319ad..5a48f5c744 100644 --- a/framework/src/play/db/jpa/GenericModel.java +++ b/framework/src/play/db/jpa/GenericModel.java @@ -1,36 +1,39 @@ package play.db.jpa; import java.io.File; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; 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.List; import java.util.Map; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.MappedSuperclass; -import javax.persistence.NoResultException; import javax.persistence.OneToMany; import javax.persistence.OneToOne; +import javax.persistence.PostLoad; +import javax.persistence.PostPersist; +import javax.persistence.PostUpdate; import javax.persistence.Query; + +import org.apache.commons.lang.StringUtils; + import play.Play; import play.data.binding.BeanWrapper; import play.data.binding.Binder; import play.data.validation.Validation; import play.exceptions.UnexpectedException; import play.mvc.Scope.Params; -import java.lang.annotation.Annotation; -import java.lang.reflect.Constructor; -import java.util.SortedSet; -import java.util.TreeSet; -import javax.persistence.PostLoad; -import javax.persistence.PostPersist; -import javax.persistence.PostUpdate; import play.Logger; /** @@ -55,12 +58,7 @@ public static T edit(Object o, String name, Map fields = new HashSet(); - Class clazz = o.getClass(); - while (!clazz.equals(Object.class)) { - Collections.addAll(fields, clazz.getDeclaredFields()); - clazz = clazz.getSuperclass(); - } + Set fields = JPAPlugin.getModelFields(o.getClass()); for (Field field : fields) { boolean isEntity = false; String relation = null; @@ -78,9 +76,9 @@ public static T edit(Object o, String name, Map c = (Class) Play.classloader.loadClass(relation); + Class c = (Class) Play.classloader.loadClass(relation); if (JPABase.class.isAssignableFrom(c)) { - String keyName = Model.Manager.factoryFor(c).keyName(); + Factory relationFactory = Model.Manager.factoryFor(c); if (multiple && Collection.class.isAssignableFrom(field.getType())) { Collection l = new ArrayList(); if (SortedSet.class.isAssignableFrom(field.getType())) { @@ -88,38 +86,18 @@ public static T edit(Object o, String name, Map) Play.classloader.loadClass(relation)).keyType())); - try { - l.add(q.getSingleResult()); - } catch (NoResultException e) { - Validation.addError(name + "." + field.getName(), "validation.notFound", _id); - } - } - bw.set(field.getName(), o, l); - } + l.addAll(findEntities(name + "." + field.getName(), params, true, c, relationFactory)); + if(!l.isEmpty()) + bw.set(field.getName(), o, l); } else { - String[] ids = params.get(name + "." + field.getName() + "." + keyName); - if (ids != null && ids.length > 0 && !ids[0].equals("")) { - params.remove(name + "." + field.getName() + "." + keyName); - Query q = JPA.em().createQuery("from " + relation + " where " + keyName + " = ?"); - q.setParameter(1, Binder.directBind(ids[0], Model.Manager.factoryFor((Class) Play.classloader.loadClass(relation)).keyType())); - try { - Object to = q.getSingleResult(); - bw.set(field.getName(), o, to); - } catch (NoResultException e) { - Validation.addError(name + "." + field.getName(), "validation.notFound", ids[0]); - } - } else if (ids != null && ids.length > 0 && ids[0].equals("")) { - bw.set(field.getName(), o, null); - params.remove(name + "." + field.getName() + "." + keyName); + List entities = findEntities(name + "." + field.getName(), params, false, c, relationFactory); + if(!entities.isEmpty()){ + JPABase entity = entities.get(0); + if (entity != null) { + bw.set(field.getName(), o, entity); + } else { + bw.set(field.getName(), o, null); + } } } } @@ -153,6 +131,119 @@ public static T edit(Object o, String name, Map List findEntities(String name, Map params, + boolean wantsCollection, + Class relation, Factory relationFactory) throws Exception{ + List keys = relationFactory.listKeys(); + // we put the more complex composite key resolving somewhere else + if(keys.size() > 1) + return findEntitiesWithCompositeKey(name, params, wantsCollection, relation, relationFactory, keys); + // single key + String keyName = keys.get(0).name; + List results = new ArrayList(); + String[] ids = params.get(name + "." + keyName); + if (ids != null) { + params.remove(name + "." + keyName); + // special case for blanking out the property + if(!wantsCollection){ + if(ids.length > 1) + throw new UnexpectedException("Too many entries for non-collection: "+name); + if(ids.length == 1 && StringUtils.isEmpty(ids[0])){ + results.add(null); + return results; + } + } + for (String _id : ids) { + if (_id.equals("")) { + continue; + } + T entity = JPA.em().find(relation, Binder.directBind(_id, relationFactory.keyType())); + if(entity != null) + results.add(entity); + else + Validation.addError(name, "validation.notFound", _id); + } + } + return results; + } + + private static List findEntitiesWithCompositeKey(String name, Map params, + boolean wantsCollection, + Class relation, Factory relationFactory, List keys) throws Exception{ + Object[][] idParts = new Object[keys.size()][]; + int keyCount = 0; + // collect every key part + for (int i = 0; i < idParts.length; i++) { + Property key = keys.get(i); + String paramKey = name + "." + key.name; + // we can resolve single keys in this method, but not relations, for that we recurse + if(key.isRelation){ + List idEntities = findEntities(paramKey, params, true, + (Class)key.relationType, Manager.factoryFor((Class)key.relationType)); + idParts[i] = idEntities.toArray(); + }else + idParts[i] = params.get(paramKey); + + int thisKeyCount = 0; + if(idParts[i] != null){ + params.remove(paramKey); + thisKeyCount = idParts[i].length; + } + // make sure each part has the right number of keys + if(i > 0 && keyCount != thisKeyCount){ + throw new UnexpectedException("Missing key parts"); + }else + keyCount = thisKeyCount; + } + List results = new ArrayList(); + if(keyCount == 0) + return results; + // special case for a single entry with null ids + if(!wantsCollection){ + if(keyCount > 1) + throw new UnexpectedException("Too many entries for non-collection: "+name); + if(keyCount == 1){ + boolean isAllEmpty = true; + for (int i = 0; i < idParts.length; i++) { + Object idPart = idParts[i][0]; + if(idPart != null + || (idPart instanceof String && !StringUtils.isEmpty((String)idPart))){ + isAllEmpty = false; + break; + } + } + if(isAllEmpty){ + results.add(null); + return results; + } + } + } + // now resolve + for (int i = 0; i < idParts[0].length; i++) { + Map id = new HashMap(); + for (int p = 0; p < idParts.length; p++) { + Property key = keys.get(p); + Object unboundValue = idParts[p][i]; + Object boundValue; + // is this an id? + if(key.isRelation){ + // we have already resolved it + boundValue = unboundValue; + }else + boundValue = Binder.directBind((String)unboundValue, key.type); + id.put(key.name, boundValue); + } + // we have all parts of the id, make an id + Object realId = relationFactory.makeKey(id); + T q = JPA.em().find(relation, realId); + if(q == null) + Validation.addError(name, "validation.notFound", realId.toString()); + else + results.add(q); + } + return results; + } + public T edit(String name, Map params) { edit(this, name, params, new Annotation[0]); return (T) this; diff --git a/framework/src/play/db/jpa/JPAModelLoader.java b/framework/src/play/db/jpa/JPAModelLoader.java new file mode 100644 index 0000000000..03daa7510b --- /dev/null +++ b/framework/src/play/db/jpa/JPAModelLoader.java @@ -0,0 +1,407 @@ +package play.db.jpa; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.MappedSuperclass; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import javax.persistence.Query; +import javax.persistence.Transient; + +import org.apache.commons.beanutils.PropertyUtils; + +import play.Logger; +import play.db.Model; +import play.db.Model.Factory; +import play.db.Model.Property; +import play.exceptions.UnexpectedException; + +public class JPAModelLoader implements Model.Factory { + + private Class clazz; + private Map properties; + private List keyProperties; + private boolean isGeneratedKey; + private static WeakHashMap, JPAModelLoader> cache = new WeakHashMap, JPAModelLoader>(); + + public JPAModelLoader(Class clazz) { + this.clazz = clazz; + } + + public static Factory instance(Class modelClass) { + synchronized (cache) { + JPAModelLoader factory = cache.get(modelClass); + if(factory == null){ + Logger.debug("Cache miss for %s factory", modelClass); + factory = new JPAModelLoader(modelClass); + cache.put(modelClass, factory); + }else + Logger.debug("Cache hit for %s factory", modelClass); + return factory; + } + } + + public Model findById(Object id) { + if (id == null) { + return null; + } + try { + return JPA.em().find(clazz, id); + } catch (Exception e) { + // Key is invalid, thus nothing was found + return null; + } + } + + @SuppressWarnings("unchecked") + public List fetch(int offset, int size, String orderBy, String order, List searchFields, String keywords, String where) { + String q = "from " + clazz.getName(); + if (keywords != null && !keywords.equals("")) { + String searchQuery = getSearchQuery(searchFields); + if (!searchQuery.equals("")) { + q += " where (" + searchQuery + ")"; + } + q += (where != null ? " and " + where : ""); + } else { + q += (where != null ? " where " + where : ""); + } + if (orderBy == null && order == null) { + orderBy = "id"; + order = "ASC"; + } + if (orderBy == null && order != null) { + orderBy = "id"; + } + if (order == null || (!order.equals("ASC") && !order.equals("DESC"))) { + order = "ASC"; + } + q += " order by " + orderBy + " " + order; + Query query = JPA.em().createQuery(q); + if (keywords != null && !keywords.equals("") && q.indexOf("?1") != -1) { + query.setParameter(1, "%" + keywords.toLowerCase() + "%"); + } + query.setFirstResult(offset); + query.setMaxResults(size); + return query.getResultList(); + } + + public Long count(List searchFields, String keywords, String where) { + String q = "select count(e) from " + clazz.getName() + " e"; + if (keywords != null && !keywords.equals("")) { + String searchQuery = getSearchQuery(searchFields); + if (!searchQuery.equals("")) { + q += " where (" + searchQuery + ")"; + } + q += (where != null ? " and " + where : ""); + } else { + q += (where != null ? " where " + where : ""); + } + Query query = JPA.em().createQuery(q); + if (keywords != null && !keywords.equals("") && q.indexOf("?1") != -1) { + query.setParameter(1, "%" + keywords.toLowerCase() + "%"); + } + return Long.decode(query.getSingleResult().toString()); + } + + public void deleteAll() { + JPA.em().createQuery("delete from " + clazz.getName()).executeUpdate(); + } + + public List listProperties() { + initProperties(); + return new ArrayList(this.properties.values()); + } + + private void initProperties() { + synchronized(this){ + if(properties != null) + return; + properties = new HashMap(); + keyProperties = new ArrayList(); + Set fields = JPAPlugin.getModelFields(clazz); + for (Field f : fields) { + if (Modifier.isTransient(f.getModifiers())) { + continue; + } + if (f.isAnnotationPresent(Transient.class)) { + continue; + } + Model.Property mp = buildProperty(f); + if (mp != null) { + properties.put(mp.name, mp); + if(mp.isKey) + keyProperties.add(mp); + } + } + } + } + + public Class keyType() { + List keys = listKeys(); + if(keys.size() == 1) + return keys.get(0).type; + // if we have more than one it's an idClass + return getCompositeKeyClass(); + } + + private Class getCompositeKeyClass(){ + Class tclazz = clazz; + while (!tclazz.equals(Object.class)) { + // Only consider mapped types + if(tclazz.isAnnotationPresent(Entity.class) + || tclazz.isAnnotationPresent(MappedSuperclass.class)){ + IdClass idClass = tclazz.getAnnotation(IdClass.class); + if(idClass != null) + return idClass.value(); + } + tclazz = tclazz.getSuperclass(); + } + throw new UnexpectedException("Invalid mapping for class " + clazz + ": multiple IDs with no @IdClass annotation"); + } + + public Object keyValue(Model m) { + List keys = listKeys(); + try { + // FIXME: this might have to check whether the ID is a relation, in which case we should use its ID + // single ID + if(keys.size() == 1) + return keys.get(0).field.get(m); + // if we have more than one it's an idClass + // make one and bind it + return makeCompositeKey(m); + } catch (UnexpectedException ex) { + throw ex; + } catch (Exception ex) { + throw new UnexpectedException(ex); + } + } + + interface PropertyGetter { + Object getProperty(Property p) throws Exception; + } + + private Object makeCompositeKey(final Model m) throws Exception { + return makeCompositeKey(new PropertyGetter(){ + @Override + public Object getProperty(Property p) throws Exception { + return p.field.get(m); + }}); + } + + private Object makeCompositeKey(PropertyGetter propertyGetter) throws Exception { + initProperties(); + Class idClass = getCompositeKeyClass(); + Object id = idClass.newInstance(); + PropertyDescriptor[] idProperties = PropertyUtils.getPropertyDescriptors(idClass); + if(idProperties == null || idProperties.length == 0) + throw new UnexpectedException("Composite id has no properties: "+idClass.getName()); + for (PropertyDescriptor idProperty : idProperties) { + // do we have a field for this? + String idPropertyName = idProperty.getName(); + // skip the "class" property... + if(idPropertyName.equals("class")) + continue; + Property modelProperty = this.properties.get(idPropertyName); + if(modelProperty == null) + throw new UnexpectedException("Composite id propery missing: "+clazz.getName()+"."+idPropertyName + +" (defined in IdClass "+idClass.getName()+")"); + // sanity check + if(modelProperty.isMultiple) + throw new UnexpectedException("Composite id property cannot be multiple: "+clazz.getName()+"."+idPropertyName); + // now is this property a relation? if yes then we must use its ID in the key (as per specs) + Object value = propertyGetter.getProperty(modelProperty); + if(modelProperty.isRelation){ + // get its id + if(!Model.class.isAssignableFrom(modelProperty.type)) + throw new UnexpectedException("Composite id property entity has to be a subclass of Model: " + +clazz.getName()+"."+idPropertyName); + // we already checked that cast above + @SuppressWarnings("unchecked") + Factory factory = Model.Manager.factoryFor((Class) modelProperty.type); + if(factory == null) + throw new UnexpectedException("Failed to find factory for Composite id property entity: " + +clazz.getName()+"."+idPropertyName); + // we already checked that cast above + if(value != null) + value = factory.keyValue((Model) value); + } + // now affect the composite id with this id + PropertyUtils.setSimpleProperty(id, idPropertyName, value); + } + return id; + } + + + @Override + public Object makeKey(Map ids) { + List keys = listKeys(); + try { + // FIXME: this might have to check whether the ID is a relation, in which case we should use its ID + // single ID + if(keys.size() == 1){ + Property key = keys.get(0); + Object id = ids.get(key.name); + if(id == null) + throw new UnexpectedException("Missing key from mapping: "+key.name); + return id; + } + // if we have more than one it's an idClass + // make one and bind it + return makeCompositeKey(ids); + } catch (UnexpectedException ex) { + throw ex; + } catch (Exception ex) { + throw new UnexpectedException(ex); + } + } + + private Object makeCompositeKey(final Map ids) throws Exception { + return makeCompositeKey(new PropertyGetter(){ + @Override + public Object getProperty(Property p) throws Exception { + return ids.get(p.name); + }}); + } + + // + public List listKeys() { + initProperties(); + if(keyProperties.isEmpty()) + throw new UnexpectedException("Cannot get the object @Id for an object of type " + clazz); + return keyProperties; + } + + String getSearchQuery(List searchFields) { + String q = ""; + for (Model.Property property : listProperties()) { + if (property.isSearchable && (searchFields == null || searchFields.isEmpty() ? true : searchFields.contains(property.name))) { + if (!q.equals("")) { + q += " or "; + } + q += "lower(" + property.name + ") like ?1"; + } + } + return q; + } + + Model.Property buildProperty(final Field field) { + Model.Property modelProperty = new Model.Property(); + modelProperty.type = field.getType(); + modelProperty.field = field; + if (Model.class.isAssignableFrom(field.getType())) { + if (field.isAnnotationPresent(OneToOne.class)) { + if (field.getAnnotation(OneToOne.class).mappedBy().equals("")) { + modelProperty.isRelation = true; + modelProperty.relationType = field.getType(); + modelProperty.choices = new Model.Choices() { + + @SuppressWarnings("unchecked") + public List list() { + return JPA.em().createQuery("from " + field.getType().getName()).getResultList(); + } + }; + } + } + if (field.isAnnotationPresent(ManyToOne.class)) { + modelProperty.isRelation = true; + modelProperty.relationType = field.getType(); + modelProperty.choices = new Model.Choices() { + + @SuppressWarnings("unchecked") + public List list() { + return JPA.em().createQuery("from " + field.getType().getName()).getResultList(); + } + }; + } + } + if (Collection.class.isAssignableFrom(field.getType())) { + final Class fieldType = (Class) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]; + if (field.isAnnotationPresent(OneToMany.class)) { + if (field.getAnnotation(OneToMany.class).mappedBy().equals("")) { + modelProperty.isRelation = true; + modelProperty.isMultiple = true; + modelProperty.relationType = fieldType; + modelProperty.choices = new Model.Choices() { + + @SuppressWarnings("unchecked") + public List list() { + return JPA.em().createQuery("from " + fieldType.getName()).getResultList(); + } + }; + } + } + if (field.isAnnotationPresent(ManyToMany.class)) { + if (field.getAnnotation(ManyToMany.class).mappedBy().equals("")) { + modelProperty.isRelation = true; + modelProperty.isMultiple = true; + modelProperty.relationType = fieldType; + modelProperty.choices = new Model.Choices() { + + @SuppressWarnings("unchecked") + public List list() { + return JPA.em().createQuery("from " + fieldType.getName()).getResultList(); + } + }; + } + } + } + if (field.getType().isEnum()) { + modelProperty.choices = new Model.Choices() { + + @SuppressWarnings("unchecked") + public List list() { + return (List) Arrays.asList(field.getType().getEnumConstants()); + } + }; + } + modelProperty.name = field.getName(); + if (field.getType().equals(String.class)) { + modelProperty.isSearchable = true; + } + if (field.isAnnotationPresent(GeneratedValue.class)) { + modelProperty.isGenerated = true; + } + if (field.isAnnotationPresent(Id.class) + || field.isAnnotationPresent(EmbeddedId.class)) { + modelProperty.isKey = true; + if(modelProperty.isGenerated) + isGeneratedKey = true; + } + return modelProperty; + } + + @Override + public boolean isGeneratedKey() { + initProperties(); + return isGeneratedKey; + } + + @Deprecated + @Override + public String keyName() { + List keys = listKeys(); + if(keys.size() > 1) + throw new UnexpectedException("Property.keyName() does not work for composite keys. Use listKeys() instead."); + if(keys.get(0).isRelation) + throw new UnexpectedException("Property.keyName() does not work for relation keys. Use listKeys() instead."); + return keys.get(0).name; + } +} \ No newline at end of file diff --git a/framework/src/play/db/jpa/JPAPlugin.java b/framework/src/play/db/jpa/JPAPlugin.java index a409bb18de..b275d9384b 100644 --- a/framework/src/play/db/jpa/JPAPlugin.java +++ b/framework/src/play/db/jpa/JPAPlugin.java @@ -3,47 +3,38 @@ import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; + import javax.persistence.Entity; import javax.persistence.EntityManager; import javax.persistence.FlushModeType; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.ManyToMany; -import javax.persistence.ManyToOne; -import javax.persistence.NoResultException; -import javax.persistence.OneToMany; -import javax.persistence.OneToOne; +import javax.persistence.MappedSuperclass; import javax.persistence.PersistenceException; -import javax.persistence.Query; -import javax.persistence.Transient; + +import org.apache.commons.lang.StringUtils; import org.apache.log4j.Level; import org.hibernate.CallbackException; import org.hibernate.EmptyInterceptor; import org.hibernate.collection.PersistentCollection; import org.hibernate.ejb.Ejb3Configuration; import org.hibernate.type.Type; + import play.Logger; import play.Play; import play.PlayPlugin; import play.classloading.ApplicationClasses.ApplicationClass; import play.db.DB; import play.db.Model; +import play.db.Model.Factory; import play.exceptions.JPAException; -import play.utils.Utils; -import org.apache.commons.lang.StringUtils; -import play.data.binding.Binder; import play.exceptions.UnexpectedException; +import play.utils.Utils; /** * JPA Plugin @@ -57,21 +48,27 @@ public class JPAPlugin extends PlayPlugin { public Object bind(String name, Class clazz, java.lang.reflect.Type type, Annotation[] annotations, Map params) { // TODO need to be more generic in order to work with JPASupport if (JPABase.class.isAssignableFrom(clazz)) { - String keyName = Model.Manager.factoryFor(clazz).keyName(); - String idKey = name + "." + keyName; - if (params.containsKey(idKey) && params.get(idKey).length > 0 && params.get(idKey)[0] != null && params.get(idKey)[0].trim().length() > 0) { - String id = params.get(idKey)[0]; - try { - Query query = JPA.em().createQuery("from " + clazz.getName() + " o where o." + keyName + " = ?"); - query.setParameter(1, play.data.binding.Binder.directBind(name, annotations, id + "", Model.Manager.factoryFor(clazz).keyType())); - Object o = query.getSingleResult(); - return GenericModel.edit(o, name, params, annotations); - } catch (NoResultException e) { - // ok - } catch (Exception e) { - throw new UnexpectedException(e); - } - } + Factory factory = Model.Manager.factoryFor(clazz); + try { + // what happens here is that we're going to try to find an entity based on its ID, and this removes + // all the entity's IDs from params. This is OK if we do find the entity, but for entities that we + // cannot find, and whose ID is not generated, we should create the entity with the specified IDs + // (those that were removed during find), so we have to save them and restore them. + Map backupParams = null; + if(!factory.isGeneratedKey()){ + backupParams = new HashMap(); + backupParams.putAll(params); + } + List entities = GenericModel.findEntities(name, params, false, clazz, factory); + // ignore it if we have no id specified or if the id is null + if(!entities.isEmpty() && entities.get(0) != null) + return GenericModel.edit(entities.get(0), name, params, annotations); + // let's fall-through and create the entity, but if its IDs are not generated, let's restore them + if(backupParams != null) + params = backupParams; + } catch (Exception e) { + throw new UnexpectedException(e); + } return GenericModel.create(clazz, name, params, annotations); } return super.bind(name, clazz, type, annotations, params); @@ -366,7 +363,7 @@ public static void closeTx(boolean rollback) { @Override public Model.Factory modelFactory(Class modelClass) { if (modelClass.isAnnotationPresent(Entity.class)) { - return new JPAModelLoader(modelClass); + return JPAModelLoader.instance(modelClass); } return null; } @@ -378,229 +375,16 @@ public void afterFixtureLoad() { } } - public static class JPAModelLoader implements Model.Factory { - - private Class clazz; - - public JPAModelLoader(Class clazz) { - this.clazz = clazz; - } - - public Model findById(Object id) { - if (id == null) { - return null; - } - try { - return JPA.em().find(clazz, Binder.directBind(id.toString(), Model.Manager.factoryFor(clazz).keyType())); - } catch (Exception e) { - // Key is invalid, thus nothing was found - return null; - } - } - - @SuppressWarnings("unchecked") - public List fetch(int offset, int size, String orderBy, String order, List searchFields, String keywords, String where) { - String q = "from " + clazz.getName(); - if (keywords != null && !keywords.equals("")) { - String searchQuery = getSearchQuery(searchFields); - if (!searchQuery.equals("")) { - q += " where (" + searchQuery + ")"; - } - q += (where != null ? " and " + where : ""); - } else { - q += (where != null ? " where " + where : ""); - } - if (orderBy == null && order == null) { - orderBy = "id"; - order = "ASC"; - } - if (orderBy == null && order != null) { - orderBy = "id"; - } - if (order == null || (!order.equals("ASC") && !order.equals("DESC"))) { - order = "ASC"; - } - q += " order by " + orderBy + " " + order; - Query query = JPA.em().createQuery(q); - if (keywords != null && !keywords.equals("") && q.indexOf("?1") != -1) { - query.setParameter(1, "%" + keywords.toLowerCase() + "%"); - } - query.setFirstResult(offset); - query.setMaxResults(size); - return query.getResultList(); - } - - public Long count(List searchFields, String keywords, String where) { - String q = "select count(e) from " + clazz.getName() + " e"; - if (keywords != null && !keywords.equals("")) { - String searchQuery = getSearchQuery(searchFields); - if (!searchQuery.equals("")) { - q += " where (" + searchQuery + ")"; - } - q += (where != null ? " and " + where : ""); - } else { - q += (where != null ? " where " + where : ""); - } - Query query = JPA.em().createQuery(q); - if (keywords != null && !keywords.equals("") && q.indexOf("?1") != -1) { - query.setParameter(1, "%" + keywords.toLowerCase() + "%"); - } - return Long.decode(query.getSingleResult().toString()); - } - - public void deleteAll() { - JPA.em().createQuery("delete from " + clazz.getName()).executeUpdate(); - } - - public List listProperties() { - List properties = new ArrayList(); - Set fields = new LinkedHashSet(); - Class tclazz = clazz; - while (!tclazz.equals(Object.class)) { - Collections.addAll(fields, tclazz.getDeclaredFields()); - tclazz = tclazz.getSuperclass(); - } - for (Field f : fields) { - if (Modifier.isTransient(f.getModifiers())) { - continue; - } - if (f.isAnnotationPresent(Transient.class)) { - continue; - } - Model.Property mp = buildProperty(f); - if (mp != null) { - properties.add(mp); - } - } - return properties; - } - - public String keyName() { - return keyField().getName(); - } - - public Class keyType() { - return keyField().getType(); - } - - public Object keyValue(Model m) { - try { - return keyField().get(m); - } catch (Exception ex) { - throw new UnexpectedException(ex); - } - } - - // - Field keyField() { - Class c = clazz; - try { - while (!c.equals(Object.class)) { - for (Field field : c.getDeclaredFields()) { - if (field.isAnnotationPresent(Id.class)) { - field.setAccessible(true); - return field; - } - } - c = c.getSuperclass(); - } - } catch (Exception e) { - throw new UnexpectedException("Error while determining the object @Id for an object of type " + clazz); - } - throw new UnexpectedException("Cannot get the object @Id for an object of type " + clazz); - } - - String getSearchQuery(List searchFields) { - String q = ""; - for (Model.Property property : listProperties()) { - if (property.isSearchable && (searchFields == null || searchFields.isEmpty() ? true : searchFields.contains(property.name))) { - if (!q.equals("")) { - q += " or "; - } - q += "lower(" + property.name + ") like ?1"; - } - } - return q; - } - - Model.Property buildProperty(final Field field) { - Model.Property modelProperty = new Model.Property(); - modelProperty.type = field.getType(); - modelProperty.field = field; - if (Model.class.isAssignableFrom(field.getType())) { - if (field.isAnnotationPresent(OneToOne.class)) { - if (field.getAnnotation(OneToOne.class).mappedBy().equals("")) { - modelProperty.isRelation = true; - modelProperty.relationType = field.getType(); - modelProperty.choices = new Model.Choices() { - - @SuppressWarnings("unchecked") - public List list() { - return JPA.em().createQuery("from " + field.getType().getName()).getResultList(); - } - }; - } - } - if (field.isAnnotationPresent(ManyToOne.class)) { - modelProperty.isRelation = true; - modelProperty.relationType = field.getType(); - modelProperty.choices = new Model.Choices() { - - @SuppressWarnings("unchecked") - public List list() { - return JPA.em().createQuery("from " + field.getType().getName()).getResultList(); - } - }; - } - } - if (Collection.class.isAssignableFrom(field.getType())) { - final Class fieldType = (Class) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]; - if (field.isAnnotationPresent(OneToMany.class)) { - if (field.getAnnotation(OneToMany.class).mappedBy().equals("")) { - modelProperty.isRelation = true; - modelProperty.isMultiple = true; - modelProperty.relationType = fieldType; - modelProperty.choices = new Model.Choices() { - - @SuppressWarnings("unchecked") - public List list() { - return JPA.em().createQuery("from " + fieldType.getName()).getResultList(); - } - }; - } - } - if (field.isAnnotationPresent(ManyToMany.class)) { - if (field.getAnnotation(ManyToMany.class).mappedBy().equals("")) { - modelProperty.isRelation = true; - modelProperty.isMultiple = true; - modelProperty.relationType = fieldType; - modelProperty.choices = new Model.Choices() { - - @SuppressWarnings("unchecked") - public List list() { - return JPA.em().createQuery("from " + fieldType.getName()).getResultList(); - } - }; - } - } - } - if (field.getType().isEnum()) { - modelProperty.choices = new Model.Choices() { - - @SuppressWarnings("unchecked") - public List list() { - return (List) Arrays.asList(field.getType().getEnumConstants()); - } - }; - } - modelProperty.name = field.getName(); - if (field.getType().equals(String.class)) { - modelProperty.isSearchable = true; - } - if (field.isAnnotationPresent(GeneratedValue.class)) { - modelProperty.isGenerated = true; - } - return modelProperty; - } + public static Set getModelFields(Class clazz){ + Set fields = new LinkedHashSet(); + Class tclazz = clazz; + while (!tclazz.equals(Object.class)) { + // Only add fields for mapped types + if(tclazz.isAnnotationPresent(Entity.class) + || tclazz.isAnnotationPresent(MappedSuperclass.class)) + Collections.addAll(fields, tclazz.getDeclaredFields()); + tclazz = tclazz.getSuperclass(); + } + return fields; } } diff --git a/framework/src/play/db/jpa/JPQL.java b/framework/src/play/db/jpa/JPQL.java index 6854abaa26..62931b124a 100644 --- a/framework/src/play/db/jpa/JPQL.java +++ b/framework/src/play/db/jpa/JPQL.java @@ -17,7 +17,7 @@ public EntityManager em() { } public long count(String entity) { - return Long.parseLong(em().createQuery("select count(e) from " + entity + " e").getSingleResult().toString()); + return Long.parseLong(em().createQuery("select count(*) from " + entity).getSingleResult().toString()); } public long count(String entity, String query, Object[] params) { @@ -154,7 +154,7 @@ public String createCountQuery(String entityName, String entityClass, String que if (query.trim().length() == 0) { return "select count(*) from " + entityName; } - return "select count(e) from " + entityName + " e where " + query; + return "select count(*) from " + entityName + " where " + query; } @SuppressWarnings("unchecked") diff --git a/framework/src/play/test/Fixtures.java b/framework/src/play/test/Fixtures.java index 9552de1d6d..29492dc939 100644 --- a/framework/src/play/test/Fixtures.java +++ b/framework/src/play/test/Fixtures.java @@ -6,17 +6,15 @@ import java.sql.ResultSet; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.io.FileUtils; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.scanner.ScannerException; @@ -30,6 +28,7 @@ import play.db.DB; import play.db.DBPlugin; import play.db.Model; +import play.db.Model.Property; import play.exceptions.UnexpectedException; import play.exceptions.YAMLException; import play.vfs.VirtualFile; @@ -193,8 +192,14 @@ public static void load(String name) { } } - model._save(); + try{ + model._save(); + }catch(Exception x){ + throw new UnexpectedException("Failed to load fixture "+name+": problem while saving object "+id, x); + } Class tType = cType; + // FIXME: this is most probably wrong since superclasses might share IDs implemented by disjoint + // subclasses. Besides why create the key for each if it's supposed to be the same value??? while (!tType.equals(Object.class)) { idCache.put(tType.getName() + "-" + id, Model.Manager.factoryFor(cType).keyValue(model)); tType = tType.getSuperclass(); @@ -206,6 +211,8 @@ public static void load(String name) { for(PlayPlugin plugin : Play.plugins) { plugin.afterFixtureLoad(); } + }catch (UnexpectedException x){ + throw x; } catch (ClassNotFoundException e) { throw new RuntimeException("Class " + e.getMessage() + " was not found", e); } catch (ScannerException e) { @@ -247,34 +254,56 @@ static void serialize(Map values, String prefix, Map ser } } - @SuppressWarnings("unchecked") - static void resolveDependencies(Class type, Map serialized, Map idCache) { - Set fields = new HashSet(); - Class clazz = type; - while (!clazz.equals(Object.class)) { - Collections.addAll(fields, clazz.getDeclaredFields()); - clazz = clazz.getSuperclass(); - } + protected static void resolveDependencies(Class type, Map serialized, Map idCache) throws Exception { for (Model.Property field : Model.Manager.factoryFor(type).listProperties()) { if (field.isRelation) { - String[] ids = serialized.get("object." + field.name); + String prefix = "object." + field.name; + String[] ids = serialized.get(prefix); + Object[] persistedIds = null; if (ids != null) { + persistedIds = new Object[ids.length]; for (int i = 0; i < ids.length; i++) { String id = ids[i]; id = field.relationType.getName() + "-" + id; if (!idCache.containsKey(id)) { throw new RuntimeException("No previous reference found for object of type " + field.name + " with key " + ids[i]); } - ids[i] = idCache.get(id).toString(); + persistedIds[i] = idCache.get(id); } } - serialized.remove("object." + field.name); - serialized.put("object." + field.name + "." + Model.Manager.factoryFor((Class)field.relationType).keyName(), ids); - } + serialized.remove(prefix); + if(persistedIds != null) + serializeKey(prefix, field.relationType, persistedIds, serialized, idCache); + } } } - public static void deleteDirectory(String path) { + private static void serializeKey(String prefix, + Class relationType, Object[] persistedIds, Map serialized, Map idCache) throws Exception { + @SuppressWarnings("unchecked") + List keys = Model.Manager.factoryFor((Class) relationType).listKeys(); + // serialise each ID into as many keys + for(Property key : keys){ + String fieldName = prefix + "." + key.name; + if(key.isRelation){ + // get that part of the key + Object[] idParts = new Object[persistedIds.length]; + for (int i = 0; i < idParts.length; i++) { + idParts[i] = PropertyUtils.getSimpleProperty(persistedIds[i], key.name); + } + serializeKey(fieldName, key.relationType, idParts, serialized, idCache); + }else{ + // not composite so it must be serialisable as string + String[] ids= new String[persistedIds.length]; + for (int i = 0; i < ids.length; i++) { + ids[i] = persistedIds[i].toString(); + } + serialized.put(fieldName, ids); + } + } + } + + public static void deleteDirectory(String path) { try { FileUtils.deleteDirectory(Play.getFile(path)); } catch (IOException ex) { diff --git a/modules/crud/app/controllers/CRUD.java b/modules/crud/app/controllers/CRUD.java index e4c27427d2..f443c7237b 100644 --- a/modules/crud/app/controllers/CRUD.java +++ b/modules/crud/app/controllers/CRUD.java @@ -1,16 +1,30 @@ package controllers; -import java.util.*; -import java.lang.reflect.*; -import java.lang.annotation.*; - -import play.*; -import play.data.binding.*; -import play.mvc.*; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import play.Play; +import play.data.binding.Binder; +import play.data.validation.MaxSize; +import play.data.validation.Password; +import play.data.validation.Required; import play.db.Model; -import play.data.validation.*; -import play.exceptions.*; -import play.i18n.*; +import play.db.Model.Factory; +import play.exceptions.TemplateNotFoundException; +import play.i18n.Messages; +import play.mvc.Before; +import play.mvc.Controller; +import play.mvc.Router; public abstract class CRUD extends Controller { @@ -43,7 +57,7 @@ public static void list(int page, String search, String searchFields, String ord } } - public static void show(String id) { + public static void show(String id) throws Exception { ObjectType type = ObjectType.get(getControllerClass()); notFoundIfNull(type); Model object = type.findById(id); @@ -144,7 +158,7 @@ public static void create() throws Exception { redirect(request.controller + ".show", object._key()); } - public static void delete(String id) { + public static void delete(String id) throws Exception { ObjectType type = ObjectType.get(getControllerClass()); notFoundIfNull(type); Model object = type.findById(id); @@ -187,11 +201,13 @@ public static class ObjectType implements Comparable { public String modelName; public String controllerName; public String keyName; + public Factory factory; public ObjectType(Class modelClass) { this.modelName = modelClass.getSimpleName(); this.entityClass = modelClass; - this.keyName = Model.Manager.factoryFor(entityClass).keyName(); + this.factory = Model.Manager.factoryFor(entityClass); + this.keyName = factory.keyName(); } @SuppressWarnings("unchecked") @@ -246,22 +262,24 @@ public Object getBlankAction() { } public Long count(String search, String searchFields, String where) { - return Model.Manager.factoryFor(entityClass).count(searchFields == null ? new ArrayList() : Arrays.asList(searchFields.split("[ ]")), search, where); + return factory.count(searchFields == null ? new ArrayList() : Arrays.asList(searchFields.split("[ ]")), search, where); } @SuppressWarnings("unchecked") public List findPage(int page, String search, String searchFields, String orderBy, String order, String where) { - return Model.Manager.factoryFor(entityClass).fetch((page - 1) * getPageSize(), getPageSize(), orderBy, order, searchFields == null ? new ArrayList() : Arrays.asList(searchFields.split("[ ]")), search, where); + return factory.fetch((page - 1) * getPageSize(), getPageSize(), orderBy, order, searchFields == null ? new ArrayList() : Arrays.asList(searchFields.split("[ ]")), search, where); } - public Model findById(Object id) { + public Model findById(String id) throws Exception { if (id == null) return null; - return Model.Manager.factoryFor(entityClass).findById(id); + // since this only works so far for non-composite IDs, we might make this simple: + Object boundId = Binder.directBind(id, factory.keyType()); + return factory.findById(boundId); } public List getFields() { List fields = new ArrayList(); - for (Model.Property f : Model.Manager.factoryFor(entityClass).listProperties()) { + for (Model.Property f : factory.listProperties()) { ObjectField of = new ObjectField(f); if (of.type != null) { fields.add(of); diff --git a/samples-and-tests/just-test-cases/app/models/CompositeIdEntity.java b/samples-and-tests/just-test-cases/app/models/CompositeIdEntity.java new file mode 100644 index 0000000000..b5d61eaab9 --- /dev/null +++ b/samples-and-tests/just-test-cases/app/models/CompositeIdEntity.java @@ -0,0 +1,24 @@ +package models; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +import play.db.jpa.GenericModel; + +@Entity +@IdClass(CompositeIdPk.class) +public class CompositeIdEntity extends GenericModel { + @Id + @JoinColumn(name = "a_id") + @ManyToOne + public CompositeIdForeignA compositeIdForeignA; + + @Id + @JoinColumn(name = "b_id") + @ManyToOne + public CompositeIdForeignB compositeIdForeignB; + +} diff --git a/samples-and-tests/just-test-cases/app/models/CompositeIdForeignA.java b/samples-and-tests/just-test-cases/app/models/CompositeIdForeignA.java new file mode 100644 index 0000000000..55b8eb091d --- /dev/null +++ b/samples-and-tests/just-test-cases/app/models/CompositeIdForeignA.java @@ -0,0 +1,18 @@ +package models; + +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.OneToMany; + +import play.db.jpa.Model; + +@Entity +public class CompositeIdForeignA extends Model { + @OneToMany(mappedBy = "compositeIdForeignA", fetch = FetchType.LAZY) + public Set a2Bs = new HashSet(); + + public String testId; +} diff --git a/samples-and-tests/just-test-cases/app/models/CompositeIdForeignB.java b/samples-and-tests/just-test-cases/app/models/CompositeIdForeignB.java new file mode 100644 index 0000000000..97037407c9 --- /dev/null +++ b/samples-and-tests/just-test-cases/app/models/CompositeIdForeignB.java @@ -0,0 +1,18 @@ +package models; + +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.OneToMany; + +import play.db.jpa.Model; + +@Entity +public class CompositeIdForeignB extends Model { + @OneToMany(mappedBy = "compositeIdForeignB", fetch = FetchType.LAZY) + public Set a2Bs = new HashSet(); + + public String testId; +} diff --git a/samples-and-tests/just-test-cases/app/models/CompositeIdPk.java b/samples-and-tests/just-test-cases/app/models/CompositeIdPk.java new file mode 100644 index 0000000000..92e7092ca7 --- /dev/null +++ b/samples-and-tests/just-test-cases/app/models/CompositeIdPk.java @@ -0,0 +1,71 @@ +package models; + +import java.io.Serializable; + +public class CompositeIdPk implements Serializable { + + private Long compositeIdForeignA; + + private Long compositeIdForeignB; + + public CompositeIdPk() { + } + + public CompositeIdPk(Long compositeIdForeignA, Long compositeIdForeignB) { + this.compositeIdForeignA = compositeIdForeignA; + this.compositeIdForeignB = compositeIdForeignB; + } + + public Long getCompositeIdForeignA() { + return compositeIdForeignA; + } + + public void setCompositeIdForeignA(Long compositeIdForeignA) { + this.compositeIdForeignA = compositeIdForeignA; + } + + public Long getCompositeIdForeignB() { + return compositeIdForeignB; + } + + public void setCompositeIdForeignB(Long compositeIdForeignB) { + this.compositeIdForeignB = compositeIdForeignB; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime + * result + + ((compositeIdForeignA == null) ? 0 : compositeIdForeignA + .hashCode()); + result = prime + * result + + ((compositeIdForeignB == null) ? 0 : compositeIdForeignB + .hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CompositeIdPk other = (CompositeIdPk) obj; + if (compositeIdForeignA == null) { + if (other.compositeIdForeignA != null) + return false; + } else if (!compositeIdForeignA.equals(other.compositeIdForeignA)) + return false; + if (compositeIdForeignB == null) { + if (other.compositeIdForeignB != null) + return false; + } else if (!compositeIdForeignB.equals(other.compositeIdForeignB)) + return false; + return true; + } +} diff --git a/samples-and-tests/just-test-cases/test/CompositeIdBinderTest.java b/samples-and-tests/just-test-cases/test/CompositeIdBinderTest.java new file mode 100644 index 0000000000..ef654f4c1b --- /dev/null +++ b/samples-and-tests/just-test-cases/test/CompositeIdBinderTest.java @@ -0,0 +1,87 @@ +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import models.CompositeIdEntity; +import models.CompositeIdForeignA; +import models.CompositeIdForeignB; +import models.CompositeIdPk; + +import org.junit.Before; +import org.junit.Test; + +import play.data.binding.Binder; +import play.db.Model; +import play.db.Model.Factory; +import play.db.Model.Property; +import play.db.jpa.JPA; +import play.test.Fixtures; +import play.test.UnitTest; + +public class CompositeIdBinderTest extends UnitTest { + + @Before + public void setup() { + Fixtures.delete(CompositeIdEntity.class, CompositeIdForeignA.class, CompositeIdForeignB.class); + } + + @Test + public void testBinderFound() { + CompositeIdForeignA a = new CompositeIdForeignA(); + a.save(); + CompositeIdForeignB b = new CompositeIdForeignB(); + b.save(); + CompositeIdEntity e = new CompositeIdEntity(); + e.compositeIdForeignA = a; + e.compositeIdForeignB = b; + e.save(); + + Map params = new HashMap(); + params.put("object.compositeIdForeignA.id", new String[]{a.id.toString()}); + params.put("object.compositeIdForeignB.id", new String[]{b.id.toString()}); + Object bound = Binder.bind("object", CompositeIdEntity.class, CompositeIdEntity.class, null, params); + // they have to be the same object + assertTrue(e == bound); + assertEquals(e, bound); + } + + @Test + public void testBinderNotFound() { + Map params = new HashMap(); + params.put("object.compositeIdForeignA.id", new String[]{"10000"}); + params.put("object.compositeIdForeignB.id", new String[]{"10000"}); + Object bound = Binder.bind("object", CompositeIdEntity.class, CompositeIdEntity.class, null, params); + assertTrue(bound instanceof CompositeIdEntity); + CompositeIdEntity entity = (CompositeIdEntity) bound; + assertNull(entity.compositeIdForeignA); + assertNull(entity.compositeIdForeignB); + assertFalse(entity.isPersistent()); + } + + @Test + public void testBinderSimple() { + CompositeIdForeignA a = new CompositeIdForeignA(); + a.save(); + CompositeIdForeignB b = new CompositeIdForeignB(); + b.save(); + CompositeIdEntity e = new CompositeIdEntity(); + e.compositeIdForeignA = a; + e.compositeIdForeignB = b; + e.save(); + + Map params = new HashMap(); + params.put("object.id", new String[]{a.id.toString()}); + Object bound = Binder.bind("object", CompositeIdForeignA.class, CompositeIdForeignA.class, null, params); + // they have to be the same object + assertTrue(a == bound); + assertEquals(a, bound); + } +/* + * a.id = a1 + * a.a2Bs.compositeIdForeignA.id = a1 + * a.a2Bs.compositeIdForeignB.id = b1 + * a.a2Bs.compositeIdForeignA.id = a1 + * a.a2Bs.compositeIdForeignB.id = b2 + */ +} + diff --git a/samples-and-tests/just-test-cases/test/CompositeIdFactoryTest.java b/samples-and-tests/just-test-cases/test/CompositeIdFactoryTest.java new file mode 100644 index 0000000000..f943cb0b54 --- /dev/null +++ b/samples-and-tests/just-test-cases/test/CompositeIdFactoryTest.java @@ -0,0 +1,120 @@ +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import models.CompositeIdEntity; +import models.CompositeIdForeignA; +import models.CompositeIdForeignB; +import models.CompositeIdPk; + +import org.junit.Before; +import org.junit.Test; + +import play.db.Model; +import play.db.Model.Factory; +import play.db.Model.Property; +import play.db.jpa.JPA; +import play.test.Fixtures; +import play.test.UnitTest; + +public class CompositeIdFactoryTest extends UnitTest { + + @Before + public void setup() { + Fixtures.delete(CompositeIdEntity.class, CompositeIdForeignA.class, CompositeIdForeignB.class); + } + + @Test + public void testFactoryKeys() { + Factory factory = Model.Manager.factoryFor(CompositeIdEntity.class); + assertNotNull(factory); + List keys = factory.listKeys(); + assertNotNull(keys); + assertEquals(2, keys.size()); + Property a = findKey(keys, "compositeIdForeignA"); + assertTrue(a.isKey); + assertTrue(a.isRelation); + Property b = findKey(keys, "compositeIdForeignB"); + assertTrue(b.isKey); + assertTrue(b.isRelation); + } + + @Test + public void testFactoryKeyType() { + Factory factory = Model.Manager.factoryFor(CompositeIdEntity.class); + assertNotNull(factory); + assertEquals(CompositeIdPk.class, factory.keyType()); + } + + @Test + public void testMakeId() { + Factory factory = Model.Manager.factoryFor(CompositeIdEntity.class); + assertNotNull(factory); + Map ids = new HashMap(); + CompositeIdForeignA a = new CompositeIdForeignA(); + a.id = 1l; + CompositeIdForeignB b = new CompositeIdForeignB(); + b.id = 2l; + ids.put("compositeIdForeignA", a); + ids.put("compositeIdForeignB", b); + // let's make a key + Object id = factory.makeKey(ids); + assertNotNull(id); + assertTrue(id instanceof CompositeIdPk); + CompositeIdPk pk = (CompositeIdPk) id; + assertEquals(Long.valueOf(1), pk.getCompositeIdForeignA()); + assertEquals(Long.valueOf(2), pk.getCompositeIdForeignB()); + } + + @Test + public void testGetId() { + Factory factory = Model.Manager.factoryFor(CompositeIdEntity.class); + assertNotNull(factory); + CompositeIdForeignA a = new CompositeIdForeignA(); + a.save(); + CompositeIdForeignB b = new CompositeIdForeignB(); + b.save(); + CompositeIdEntity e = new CompositeIdEntity(); + e.compositeIdForeignA = a; + e.compositeIdForeignB = b; + e.save(); + + // let's get its key + Object id = factory.keyValue(e); + assertNotNull(id); + assertTrue(id instanceof CompositeIdPk); + CompositeIdPk pk = (CompositeIdPk) id; + assertEquals(a.id, pk.getCompositeIdForeignA()); + assertEquals(b.id, pk.getCompositeIdForeignB()); + } + + @Test + public void testBindById() { + Factory factory = Model.Manager.factoryFor(CompositeIdEntity.class); + assertNotNull(factory); + CompositeIdForeignA a = new CompositeIdForeignA(); + a.save(); + CompositeIdForeignB b = new CompositeIdForeignB(); + b.save(); + CompositeIdEntity e = new CompositeIdEntity(); + e.compositeIdForeignA = a; + e.compositeIdForeignB = b; + e.save(); + + // let's get its key + Object id = factory.keyValue(e); + Model eDB = factory.findById(id); + assertEquals(e, eDB); + } + + private Property findKey(List keys, String name) { + for(Property p : keys){ + if(p.name.equals(name)) + return p; + } + fail("Could not find key property " + name); + // never reached + return null; + } +} + diff --git a/samples-and-tests/just-test-cases/test/CompositeIdFixturesTest.java b/samples-and-tests/just-test-cases/test/CompositeIdFixturesTest.java new file mode 100644 index 0000000000..e6e983a085 --- /dev/null +++ b/samples-and-tests/just-test-cases/test/CompositeIdFixturesTest.java @@ -0,0 +1,54 @@ + +import java.util.HashMap; +import java.util.Map; + +import models.CompositeIdEntity; +import models.CompositeIdForeignA; +import models.CompositeIdForeignB; + +import org.junit.Test; + +import play.db.Model; +import play.test.Fixtures; +import play.test.UnitTest; + +public class CompositeIdFixturesTest extends UnitTest { + + // to get around access restrictions + public static class FixturesTest extends Fixtures{ + protected static void resolveDependencies(Class type, Map serialized, Map idCache) throws Exception { + Fixtures.resolveDependencies(type, serialized, idCache); + } + } + + @Test + public void testImport() throws Exception { + CompositeIdForeignA a = new CompositeIdForeignA(); + a.save(); + CompositeIdForeignB b = new CompositeIdForeignB(); + b.save(); + Map idCache = new HashMap(); + idCache.put("models.CompositeIdForeignA-a", a.getId()); + idCache.put("models.CompositeIdForeignB-b", b.getId()); + Map serialized = new HashMap(); + serialized.put("object.compositeIdForeignA", new String[]{"a"}); + serialized.put("object.compositeIdForeignB", new String[]{"b"}); + FixturesTest.resolveDependencies(CompositeIdEntity.class, serialized, idCache); + + assertEquals(2, serialized.size()); + assertEquals(2, idCache.size()); + + assertFalse(serialized.containsKey("object.CompositeIdForeignA")); + String[] serializedIds = serialized.get("object.compositeIdForeignA.id"); + assertNotNull(serializedIds); + assertEquals(1, serializedIds.length); + assertEquals(a.id.toString(), serializedIds[0]); + + assertFalse(serialized.containsKey("object.CompositeIdForeignB")); + serializedIds = serialized.get("object.compositeIdForeignB.id"); + assertNotNull(serializedIds); + assertEquals(1, serializedIds.length); + assertEquals(b.id.toString(), serializedIds[0]); + } +} + diff --git a/samples-and-tests/just-test-cases/test/CompositeIdJPATest.java b/samples-and-tests/just-test-cases/test/CompositeIdJPATest.java new file mode 100644 index 0000000000..a9363fd42f --- /dev/null +++ b/samples-and-tests/just-test-cases/test/CompositeIdJPATest.java @@ -0,0 +1,57 @@ +import models.CompositeIdEntity; +import models.CompositeIdForeignA; +import models.CompositeIdForeignB; +import models.CompositeIdPk; + +import org.junit.Before; +import org.junit.Test; + +import play.db.jpa.JPA; +import play.db.jpa.JPABase; +import play.test.Fixtures; +import play.test.UnitTest; + +public class CompositeIdJPATest extends UnitTest { + + @Before + public void setup() { + Fixtures.deleteAll(); + Fixtures.load("compositeIds.yml"); + } + + @Test + public void testImport() { + assertEquals(2, CompositeIdForeignA.count()); + assertEquals(4, CompositeIdForeignB.count()); + assertEquals(4, CompositeIdEntity.count()); + for(CompositeIdForeignA a : CompositeIdForeignA.findAll()){ + assertEquals(2, a.a2Bs.size()); + } + for(CompositeIdForeignB b : CompositeIdForeignB.findAll()){ + assertEquals(1, b.a2Bs.size()); + } + } + + @Test + public void testAllAndDelete(){ + assertEquals(4, CompositeIdEntity.all().fetch().size()); + CompositeIdEntity.deleteAll(); + assertEquals(0, CompositeIdEntity.all().fetch().size()); + } + + @Test + public void testFind(){ + CompositeIdForeignA a = (CompositeIdForeignA) CompositeIdForeignA.findAll().get(0); + CompositeIdEntity e = a.a2Bs.iterator().next(); + CompositeIdForeignB b = e.compositeIdForeignB; + + CompositeIdPk id = new CompositeIdPk(); + id.setCompositeIdForeignA(a.id); + id.setCompositeIdForeignB(b.id); + + CompositeIdEntity eDB = CompositeIdEntity.findById(id); + assertNotNull(eDB); + assertTrue(eDB == e); + } +} + diff --git a/samples-and-tests/just-test-cases/test/JPABinding.test.html b/samples-and-tests/just-test-cases/test/JPABinding.test.html index b57ea40875..5f0b3ba345 100644 --- a/samples-and-tests/just-test-cases/test/JPABinding.test.html +++ b/samples-and-tests/just-test-cases/test/JPABinding.test.html @@ -23,7 +23,7 @@ type('id', '89776') clickAndWait('go') - assertTextPresent('detached entity passed to persist: models.Project') + assertTextPresent('Object not found for id 89776') open('@{JPABinding.createCompany()}?company.name=zenexity') assertValue('name', 'zenexity') diff --git a/samples-and-tests/just-test-cases/test/compositeIds.yml b/samples-and-tests/just-test-cases/test/compositeIds.yml new file mode 100644 index 0000000000..84d3baa5ed --- /dev/null +++ b/samples-and-tests/just-test-cases/test/compositeIds.yml @@ -0,0 +1,34 @@ + +CompositeIdForeignA(a1): + testId: a1 + +CompositeIdForeignA(a2): + testId: a2 + +CompositeIdForeignB(b1): + testId: b1 + +CompositeIdForeignB(b2): + testId: b2 + +CompositeIdForeignB(b3): + testId: b3 + +CompositeIdForeignB(b4): + testId: b4 + +CompositeIdEntity(e1): + compositeIdForeignA: a1 + compositeIdForeignB: b1 + +CompositeIdEntity(e2): + compositeIdForeignA: a1 + compositeIdForeignB: b2 + +CompositeIdEntity(e3): + compositeIdForeignA: a2 + compositeIdForeignB: b3 + +CompositeIdEntity(e4): + compositeIdForeignA: a2 + compositeIdForeignB: b4