Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for #479 #55

33 changes: 31 additions & 2 deletions framework/src/play/db/Model.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -35,15 +38,41 @@ public static interface Choices {

public static interface Factory {

/**
* Returns the list of properties for this factory's type.
*/
public List<Model.Property> listProperties();
/**
* Returns the list of key properties for this factory's type.
*/
public List<Model.Property> 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<String, Object> 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<Model> fetch(int offset, int length, String orderBy, String orderDirection, List<String> properties, String keywords, String where);
public Long count(List<String> properties, String keywords, String where);
public void deleteAll();
public List<Model.Property> listProperties();

}

public static class Manager {
Expand Down
187 changes: 139 additions & 48 deletions framework/src/play/db/jpa/GenericModel.java
Original file line number Diff line number Diff line change
@@ -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;

/**
Expand All @@ -55,12 +58,7 @@ public static <T extends JPABase> T edit(Object o, String name, Map<String, Stri
try {
BeanWrapper bw = new BeanWrapper(o.getClass());
// Start with relations
Set<Field> fields = new HashSet<Field>();
Class clazz = o.getClass();
while (!clazz.equals(Object.class)) {
Collections.addAll(fields, clazz.getDeclaredFields());
clazz = clazz.getSuperclass();
}
Set<Field> fields = JPAPlugin.getModelFields(o.getClass());
for (Field field : fields) {
boolean isEntity = false;
String relation = null;
Expand All @@ -78,48 +76,28 @@ public static <T extends JPABase> T edit(Object o, String name, Map<String, Stri
}

if (isEntity) {
Class<Model> c = (Class<Model>) Play.classloader.loadClass(relation);
Class<JPABase> c = (Class<JPABase>) 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())) {
l = new TreeSet();
} else if (Set.class.isAssignableFrom(field.getType())) {
l = new HashSet();
}
String[] ids = params.get(name + "." + field.getName() + "." + keyName);
if (ids != null) {
params.remove(name + "." + field.getName() + "." + keyName);
for (String _id : ids) {
if (_id.equals("")) {
continue;
}
Query q = JPA.em().createQuery("from " + relation + " where " + keyName + " = ?");
q.setParameter(1, Binder.directBind(_id, Model.Manager.factoryFor((Class<Model>) 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<Model>) 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<JPABase> 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);
}
}
}
}
Expand Down Expand Up @@ -153,6 +131,119 @@ public static <T extends JPABase> T edit(Object o, String name, Map<String, Stri
}
}

static <T extends JPABase> List<T> findEntities(String name, Map<String, String[]> params,
boolean wantsCollection,
Class<T> relation, Factory relationFactory) throws Exception{
List<Property> 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<T> results = new ArrayList<T>();
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 <T extends JPABase> List<T> findEntitiesWithCompositeKey(String name, Map<String, String[]> params,
boolean wantsCollection,
Class<T> relation, Factory relationFactory, List<Property> 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<JPABase> idEntities = findEntities(paramKey, params, true,
(Class<JPABase>)key.relationType, Manager.factoryFor((Class<JPABase>)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<T> results = new ArrayList<T>();
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<String, Object> id = new HashMap<String, Object>();
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 extends GenericModel> T edit(String name, Map<String, String[]> params) {
edit(this, name, params, new Annotation[0]);
return (T) this;
Expand Down
Loading