Added query capability.

This commit is contained in:
Jon Brisbin
2012-05-01 09:32:32 -05:00
parent d78126d018
commit 393a3b56af
11 changed files with 213 additions and 24 deletions

View File

@@ -1,7 +1,6 @@
package org.springframework.data.rest.repository;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.data.repository.Repository;
@@ -19,8 +18,8 @@ public interface RepositoryMetadata<R extends Repository<? extends Object, ? ext
E entityMetadata();
Method queryMethod(String key);
RepositoryQueryMethod queryMethod(String key);
Map<String, Method> queryMethods();
Map<String, RepositoryQueryMethod> queryMethods();
}

View File

@@ -0,0 +1,62 @@
package org.springframework.data.rest.repository;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.data.repository.query.Param;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public class RepositoryQueryMethod {
private static final Class[] SIMPLE_TYPES = new Class[]{
String.class,
Integer.class,
Long.class,
Boolean.class
};
private static final LocalVariableTableParameterNameDiscoverer nameLookup = new LocalVariableTableParameterNameDiscoverer();
private Method method;
private Class<?>[] paramTypes;
private String[] paramNames;
public RepositoryQueryMethod(Method method) {
this.method = method;
paramTypes = method.getParameterTypes();
paramNames = nameLookup.getParameterNames(method);
if (null == paramNames) {
paramNames = new String[paramTypes.length];
}
Annotation[][] paramAnnos = method.getParameterAnnotations();
for (int i = 0; i < paramAnnos.length; i++) {
if (paramAnnos[i].length > 0) {
for (Annotation anno : paramAnnos[i]) {
if (Param.class.isAssignableFrom(anno.getClass())) {
Param p = (Param) anno;
paramNames[i] = p.value();
break;
}
}
}
if (null == paramNames[i]) {
paramNames[i] = "arg" + i;
}
}
}
public Class<?>[] paramTypes() {
return paramTypes;
}
public String[] paramNames() {
return paramNames;
}
public Method method() {
return method;
}
}

View File

@@ -139,4 +139,15 @@ public class JpaAttributeMetadata implements AttributeMetadata {
return this;
}
@Override public String toString() {
return "JpaAttributeMetadata{" +
"name='" + name + '\'' +
", attribute=" + attribute +
", type=" + type +
", field=" + field +
", getter=" + getter +
", setter=" + setter +
'}';
}
}

View File

@@ -35,7 +35,9 @@ public class JpaEntityMetadata implements EntityMetadata<JpaAttributeMetadata> {
if (repositories.hasRepositoryFor(attrType)) {
linkedAttributes.put(attr.getName(), new JpaAttributeMetadata(entityType, attr));
} else {
embeddedAttributes.put(attr.getName(), new JpaAttributeMetadata(entityType, attr));
if (attr != idAttribute && attr != versionAttribute) {
embeddedAttributes.put(attr.getName(), new JpaAttributeMetadata(entityType, attr));
}
}
}
}
@@ -73,4 +75,14 @@ public class JpaEntityMetadata implements EntityMetadata<JpaAttributeMetadata> {
return null;
}
@Override public String toString() {
return "JpaEntityMetadata{" +
"type=" + type +
", idAttribute=" + idAttribute +
", versionAttribute=" + versionAttribute +
", embeddedAttributes=" + embeddedAttributes +
", linkedAttributes=" + linkedAttributes +
'}';
}
}

View File

@@ -13,6 +13,7 @@ import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.data.rest.repository.RepositoryQueryMethod;
import org.springframework.data.rest.repository.annotation.RestPathSegment;
import org.springframework.util.ReflectionUtils;
@@ -25,7 +26,7 @@ public class JpaRepositoryMetadata<R extends Repository<Object, Serializable>> i
private final Class<?> repoClass;
private final R repository;
private final EntityInformation entityInfo;
private final Map<String, Method> queryMethods = new HashMap<String, Method>();
private final Map<String, RepositoryQueryMethod> queryMethods = new HashMap<String, RepositoryQueryMethod>();
private JpaEntityMetadata entityMetadata;
@SuppressWarnings({"unchecked"})
@@ -46,7 +47,7 @@ public class JpaRepositoryMetadata<R extends Repository<Object, Serializable>> i
@Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
String pathSeg = AnnotationUtils.findAnnotation(method, RestPathSegment.class).value();
ReflectionUtils.makeAccessible(method);
queryMethods.put(pathSeg, method);
queryMethods.put(pathSeg, new RepositoryQueryMethod(method));
}
},
new ReflectionUtils.MethodFilter() {
@@ -80,12 +81,23 @@ public class JpaRepositoryMetadata<R extends Repository<Object, Serializable>> i
return entityMetadata;
}
@Override public Method queryMethod(String key) {
@Override public RepositoryQueryMethod queryMethod(String key) {
return queryMethods.get(key);
}
@Override public Map<String, Method> queryMethods() {
@Override public Map<String, RepositoryQueryMethod> queryMethods() {
return Collections.unmodifiableMap(queryMethods);
}
@Override public String toString() {
return "JpaRepositoryMetadata{" +
"name='" + name + '\'' +
", repoClass=" + repoClass +
", repository=" + repository +
", entityInfo=" + entityInfo +
", queryMethods=" + queryMethods +
", entityMetadata=" + entityMetadata +
'}';
}
}

View File

@@ -5,7 +5,8 @@ import javax.persistence.PersistenceContext
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.data.repository.CrudRepository
import org.springframework.data.rest.repository.JpaRepositoryMetadata
import org.springframework.data.rest.repository.RepositoryExporter
import org.springframework.data.rest.repository.RepositoryMetadata
import org.springframework.data.rest.repository.test.Family
import org.springframework.data.rest.repository.test.FamilyRepository
import org.springframework.data.rest.repository.test.Person
@@ -26,19 +27,23 @@ class JpaMetadataSpec extends Specification {
@Autowired
Collection<CrudRepository> repositories
@Autowired
JpaRepositoryMetadata repoMeta
List<RepositoryExporter> exporters
RepositoryMetadata repositoryMetadataFor(name) {
exporters.find { null != it.repositoryMetadataFor(name) }?.repositoryMetadataFor(name)
}
def "finds repositories in ApplicationContext"() {
when: "find repo by String identifier"
def repo = repoMeta.repositoryFor("person")
def repo = repositoryMetadataFor("person").repository()
then:
null != repo
repo instanceof PersonRepository
when: "find repo by domain Class<?>"
repo = repoMeta.repositoryFor(Family)
repo = repositoryMetadataFor(Family).repository()
then:
null != repo
@@ -49,8 +54,8 @@ class JpaMetadataSpec extends Specification {
def "provides entity metadata"() {
given:
def personRepo = repoMeta.repositoryFor(Person)
def familyRepo = repoMeta.repositoryFor(Family)
def personRepo = repositoryMetadataFor(Person).repository()
def familyRepo = repositoryMetadataFor(Family).repository()
def johnDoe = personRepo.save(new Person("John Doe"))
def janeDoe = personRepo.save(new Person("Jane Doe"))
def doeFamily = familyRepo.save(new Family(
@@ -59,13 +64,13 @@ class JpaMetadataSpec extends Specification {
))
when:
def personMeta = repoMeta.entityMetadataFor(Person)
def familyMeta = repoMeta.entityMetadataFor(Family)
def personMeta = repositoryMetadataFor(Person).entityMetadata()
def familyMeta = repositoryMetadataFor(Family).entityMetadata()
then:
personMeta.get("name", johnDoe) == "John Doe"
familyMeta.get("surname", doeFamily) == "Doe"
familyMeta.get("members", doeFamily).size() == 2
personMeta.attribute("name").get(johnDoe) == "John Doe"
familyMeta.attribute("surname").get(doeFamily) == "Doe"
familyMeta.attribute("members").get(doeFamily).size() == 2
personMeta.embeddedAttributes().size() == 1
familyMeta.linkedAttributes().size() == 1

View File

@@ -1,9 +1,16 @@
package org.springframework.data.rest.repository.test;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.repository.annotation.RestPathSegment;
/**
* @author Jon Brisbin <jon@jbrisbin.com>
*/
public interface PersonRepository extends CrudRepository<Person, Long> {
@RestPathSegment("byName")
public List<Person> findByName(String name);
}

View File

@@ -28,6 +28,6 @@
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
<bean id="jpaRepositoryMetadata" class="org.springframework.data.rest.repository.JpaRepositoryMetadata"/>
<bean class="org.springframework.data.rest.repository.jpa.JpaRepositoryExporter"/>
</beans>

View File

@@ -17,8 +17,7 @@ dependencies {
// Repository Exporter support
compile project(":spring-data-rest-repository")
// Testing
testRuntime "org.hibernate:hibernate-entitymanager:$hibernateVersion"
testRuntime "org.hsqldb:hsqldb:1.8.0.10"
runtime "org.hibernate:hibernate-entitymanager:$hibernateVersion"
runtime "org.hsqldb:hsqldb:2.2.8"
}

View File

@@ -4,6 +4,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
@@ -23,8 +24,10 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.Repository;
import org.springframework.data.rest.core.Handler;
import org.springframework.data.rest.core.Link;
import org.springframework.data.rest.core.SimpleLink;
@@ -35,6 +38,7 @@ import org.springframework.data.rest.repository.RepositoryConstraintViolationExc
import org.springframework.data.rest.repository.RepositoryExporter;
import org.springframework.data.rest.repository.RepositoryExporterSupport;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.data.rest.repository.RepositoryQueryMethod;
import org.springframework.data.rest.repository.context.AfterDeleteEvent;
import org.springframework.data.rest.repository.context.AfterLinkSaveEvent;
import org.springframework.data.rest.repository.context.AfterSaveEvent;
@@ -61,6 +65,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.util.UriComponentsBuilder;
/**
@@ -224,13 +229,83 @@ public class RepositoryRestController
while (iter.hasNext()) {
Object o = iter.next();
Serializable id = (Serializable) repoMeta.entityMetadata().idAttribute().get(o);
links.add(new SimpleLink(o.getClass().getSimpleName(), buildUri(baseUri, repository, id.toString())));
links.add(new SimpleLink(repository + "." + o.getClass().getSimpleName(),
buildUri(baseUri, repository, id.toString())));
}
for (Map.Entry<String, RepositoryQueryMethod> entry : ((Map<String, RepositoryQueryMethod>) repoMeta.queryMethods())
.entrySet()) {
links.add(new SimpleLink(repository + "." + entry.getKey(),
buildUri(baseUri, repository, "search", entry.getKey())));
}
model.addAttribute(STATUS, HttpStatus.OK);
model.addAttribute(RESOURCE, links);
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}/search/{query}",
method = RequestMethod.GET,
produces = {
"application/json"
}
)
public void query(WebRequest request,
UriComponentsBuilder uriBuilder,
@PathVariable String repository,
@PathVariable String query,
Model model) {
URI baseUri = uriBuilder.build().toUri();
RepositoryMetadata repoMeta = repositoryMetadataFor(repository);
Repository repo = repoMeta.repository();
RepositoryQueryMethod queryMethod = repoMeta.queryMethod(query);
Class<?>[] paramTypes = queryMethod.paramTypes();
String[] paramNames = queryMethod.paramNames();
Object[] paramVals = new Object[paramTypes.length];
for (int i = 0; i < paramVals.length; i++) {
String queryVal = request.getParameter(paramNames[i]);
if (paramTypes[i].isAssignableFrom(String.class)) {
paramVals[i] = queryVal;
} else {
paramVals[i] = conversionService.convert(queryVal, paramTypes[i]);
}
}
try {
Object result = queryMethod.method().invoke(repo, paramVals);
if (result instanceof Collection) {
Collection coll = new ArrayList();
for (Object o : (Collection) result) {
RepositoryMetadata elemRepoMeta = repositoryMetadataFor(o.getClass());
if (null != elemRepoMeta) {
Map<String, Object> dto = extractPropertiesLinkAware(o, elemRepoMeta.entityMetadata(), baseUri);
coll.add(dto);
} else {
coll.add(o);
}
}
model.addAttribute(RESOURCE, coll);
model.addAttribute(STATUS, HttpStatus.OK);
} else {
RepositoryMetadata elemRepoMeta = repositoryMetadataFor(result.getClass());
if (null != elemRepoMeta) {
Map<String, Object> dto = extractPropertiesLinkAware(result, elemRepoMeta.entityMetadata(), baseUri);
model.addAttribute(RESOURCE, dto);
} else {
model.addAttribute(RESOURCE, result);
}
model.addAttribute(STATUS, HttpStatus.OK);
}
} catch (IllegalAccessException e) {
throw new DataRetrievalFailureException(e.getMessage(), e);
} catch (InvocationTargetException e) {
throw new DataRetrievalFailureException(e.getMessage(), e);
}
}
@SuppressWarnings({"unchecked"})
@RequestMapping(
value = "/{repository}",

View File

@@ -1,6 +1,9 @@
package org.springframework.data.rest.test.webmvc;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.repository.annotation.RestPathSegment;
/**
@@ -8,4 +11,8 @@ import org.springframework.data.rest.repository.annotation.RestPathSegment;
*/
@RestPathSegment("person")
public interface PersonRepository extends CrudRepository<Person, Long> {
@RestPathSegment("byName")
public List<Person> findByName(@Param("nme") String name);
}