diff --git a/build.gradle b/build.gradle index 10744082d..c9868b469 100644 --- a/build.gradle +++ b/build.gradle @@ -116,7 +116,7 @@ gradle.beforeProject { Project project -> if (!docs && !examples && !testProject && !legacyBraveProject) { apply plugin: "jacoco" jacoco { - toolVersion = "0.7.6.201602180812" + toolVersion = "0.8.5" } rootProject.tasks.jacocoMerge.executionData tasks.withType(Test) diff --git a/crnk-client/src/main/java/io/crnk/client/CrnkClient.java b/crnk-client/src/main/java/io/crnk/client/CrnkClient.java index 9fc43e7ad..768e6539e 100644 --- a/crnk-client/src/main/java/io/crnk/client/CrnkClient.java +++ b/crnk-client/src/main/java/io/crnk/client/CrnkClient.java @@ -134,6 +134,8 @@ public class CrnkClient { private ClientProxyFactory configuredProxyFactory; + private boolean filterCriteriaInRequestBody; + public enum ClientType { SIMPLE_lINKS, @@ -274,13 +276,13 @@ public ModuleRegistry getModuleRegistry() { } @Override - public DefaultResourceList getCollection(Class resourceClass, String url) { + public DefaultResourceList getCollection(Class resourceClass, String url, String body) { RegistryEntry entry = resourceRegistry.findEntry(resourceClass); ResourceInformation resourceInformation = entry.getResourceInformation(); // TODO add decoration final ResourceRepositoryStubImpl repositoryStub = - new ResourceRepositoryStubImpl<>(CrnkClient.this, resourceClass, resourceInformation, urlBuilder); - return repositoryStub.findAll(url); + new ResourceRepositoryStubImpl<>(CrnkClient.this, resourceClass, resourceInformation, urlBuilder, filterCriteriaInRequestBody); + return repositoryStub.findAll(url, body); } }); @@ -320,6 +322,7 @@ protected void init() { setupServiceDiscovery(); initHttpAdapter(); + setupUrlMapper(); setupPagingBehavior(); initObjectMapper(); configureObjectMapper(); @@ -335,6 +338,17 @@ protected void init() { } } + private void setupUrlMapper() { + if (filterCriteriaInRequestBody) { + QuerySpecUrlMapper urlMapper = moduleRegistry.getUrlMapper(); + if (urlMapper instanceof DefaultQuerySpecUrlMapper) { + ((DefaultQuerySpecUrlMapper) urlMapper).setFilterCriteriaInRequestBody(filterCriteriaInRequestBody); + } else { + throw new IllegalStateException("Need DefaultQuerySpecUrlMapper for filterCriteriaInRequestBody option"); + } + } + } + protected void initObjectMapper() { if (objectMapper == null) { objectMapper = createDefaultObjectMapper(); @@ -415,7 +429,7 @@ protected RegistryEntry allocateRepository(Class resourceClass, Regist ModuleUtils.adaptInformation(resourceInformation, moduleRegistry); final ResourceRepository repositoryStub = (ResourceRepository) decorate( - new ResourceRepositoryStubImpl(this, resourceClass, resourceInformation, urlBuilder) + new ResourceRepositoryStubImpl(this, resourceClass, resourceInformation, urlBuilder, filterCriteriaInRequestBody) ); // create interface for it! @@ -486,7 +500,7 @@ private RelationshipRepositoryAdapter allocateRepositoryRelation(ResourceField f } final Object relationshipRepositoryStub = decorate( - new RelationshipRepositoryStubImpl(this, sourceClass, targetClass, sourceEntry.getResourceInformation(), urlBuilder) + new RelationshipRepositoryStubImpl(this, sourceClass, targetClass, sourceEntry.getResourceInformation(), urlBuilder, filterCriteriaInRequestBody) ); RelationshipRepositoryAdapter adapter = new RelationshipRepositoryAdapterImpl(field, moduleRegistry, relationshipRepositoryStub); @@ -543,7 +557,7 @@ public ResourceRepository getRepositoryForPath(String resource , null , PagingSpec.class ); - return (ResourceRepository) decorate(new ResourceRepositoryStubImpl<>(this, Resource.class, resourceInformation, urlBuilder)); + return (ResourceRepository) decorate(new ResourceRepositoryStubImpl<>(this, Resource.class, resourceInformation, urlBuilder, filterCriteriaInRequestBody)); } /** @@ -558,7 +572,7 @@ public RelationshipRepository getRepositoryF PagingSpec.class); return (RelationshipRepository) decorate( new RelationshipRepositoryStubImpl<>(this, Resource.class, Resource.class, sourceResourceInformation, - urlBuilder)); + urlBuilder, filterCriteriaInRequestBody)); } @@ -734,4 +748,9 @@ public boolean isInitialized(Class clazz) { return super.getEntry(clazz) != null; } } + + public void setFilterCriteriaInRequestBody(boolean filterCriteriaInRequestBody) { + PreconditionUtil.verify(!initialized, "CrnkClient already initialized, cannot add filters to body"); + this.filterCriteriaInRequestBody = filterCriteriaInRequestBody; + } } diff --git a/crnk-client/src/main/java/io/crnk/client/http/apache/HttpClientRequest.java b/crnk-client/src/main/java/io/crnk/client/http/apache/HttpClientRequest.java index df1e3fdcc..11bf56753 100644 --- a/crnk-client/src/main/java/io/crnk/client/http/apache/HttpClientRequest.java +++ b/crnk-client/src/main/java/io/crnk/client/http/apache/HttpClientRequest.java @@ -39,7 +39,11 @@ public HttpClientRequest(CloseableHttpClient impl, String url, HttpMethod method this.listeners = listeners; this.requestBody = requestBody; if (method == HttpMethod.GET) { - requestBase = new HttpGet(url); + if (requestBody == null || requestBody.isEmpty()) { + requestBase = new HttpGet(url); + } else { + requestBase = new HttpGetWithBody(url, requestBody, CONTENT_TYPE); + } } else if (method == HttpMethod.POST) { HttpPost post = new HttpPost(url); post.setEntity(new StringEntity(requestBody, CONTENT_TYPE)); diff --git a/crnk-client/src/main/java/io/crnk/client/http/apache/HttpGetWithBody.java b/crnk-client/src/main/java/io/crnk/client/http/apache/HttpGetWithBody.java new file mode 100644 index 000000000..fe83aed89 --- /dev/null +++ b/crnk-client/src/main/java/io/crnk/client/http/apache/HttpGetWithBody.java @@ -0,0 +1,20 @@ +package io.crnk.client.http.apache; + +import io.crnk.core.engine.http.HttpMethod; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; + +import java.net.URI; + +public class HttpGetWithBody extends HttpEntityEnclosingRequestBase { + public HttpGetWithBody(String url, String body, ContentType contentType) { + setURI(URI.create(url)); + setEntity(new StringEntity(body, contentType)); + } + + @Override + public String getMethod() { + return HttpMethod.GET.toString(); + } +} diff --git a/crnk-client/src/main/java/io/crnk/client/internal/ClientStubBase.java b/crnk-client/src/main/java/io/crnk/client/internal/ClientStubBase.java index 7435a49e4..f422c439c 100644 --- a/crnk-client/src/main/java/io/crnk/client/internal/ClientStubBase.java +++ b/crnk-client/src/main/java/io/crnk/client/internal/ClientStubBase.java @@ -1,15 +1,9 @@ package io.crnk.client.internal; -import java.io.IOException; -import java.util.List; -import java.util.Optional; - +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import io.crnk.client.ClientException; -import io.crnk.client.ClientFormat; -import io.crnk.client.CrnkClient; -import io.crnk.client.ResponseBodyException; -import io.crnk.client.TransportException; +import io.crnk.client.*; import io.crnk.client.http.HttpAdapter; import io.crnk.client.http.HttpAdapterRequest; import io.crnk.client.http.HttpAdapterResponse; @@ -19,10 +13,15 @@ import io.crnk.core.engine.error.ExceptionMapper; import io.crnk.core.engine.http.HttpHeaders; import io.crnk.core.engine.http.HttpMethod; +import io.crnk.core.engine.information.resource.ResourceInformation; import io.crnk.core.engine.internal.exception.ExceptionMapperRegistry; import io.crnk.core.engine.internal.utils.PreconditionUtil; import io.crnk.core.engine.query.QueryContext; import io.crnk.core.exception.CrnkException; +import io.crnk.core.queryspec.FilterSpec; +import io.crnk.core.queryspec.QuerySpec; +import io.crnk.core.queryspec.internal.JsonFilterSpecMapper; +import io.crnk.core.queryspec.mapper.DefaultQuerySpecUrlMapper; import io.crnk.core.queryspec.mapper.UrlBuilder; import io.crnk.core.resource.list.DefaultResourceList; import io.crnk.core.resource.meta.JsonLinksInformation; @@ -30,9 +29,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + public class ClientStubBase { private static final Logger LOGGER = LoggerFactory.getLogger(ClientStubBase.class); + protected final boolean filterCriteriaInRequestBody; + protected final JsonFilterSpecMapper filterSpecMapper; + protected final ObjectMapper compactMapper; protected CrnkClient client; @@ -41,14 +48,22 @@ public class ClientStubBase { protected Class resourceClass; - public ClientStubBase(CrnkClient client, UrlBuilder urlBuilder, Class resourceClass) { + public ClientStubBase(CrnkClient client, UrlBuilder urlBuilder, Class resourceClass, boolean filterCriteriaInRequestBody) { this.client = client; this.urlBuilder = urlBuilder; this.resourceClass = resourceClass; + this.filterCriteriaInRequestBody = filterCriteriaInRequestBody; + if (filterCriteriaInRequestBody) { + filterSpecMapper = ((DefaultQuerySpecUrlMapper) client.getUrlMapper()).getJsonParser(); + compactMapper = client.getObjectMapper(); + } else { + filterSpecMapper = null; + compactMapper = null; + } } - protected Object executeGet(String requestUrl, ResponseType responseType) { - return execute(requestUrl, responseType, HttpMethod.GET, null); + protected Object executeGet(String requestUrl, String body, ResponseType responseType) { + return execute(requestUrl, responseType, HttpMethod.GET, body); } protected Object executeDelete(String requestUrl) { @@ -99,8 +114,7 @@ protected Object execute(String url, ResponseType responseType, HttpMethod metho if (Resource.class.equals(resourceClass)) { Document document = objectMapper.readValue(body, format.getDocumentClass()); return toResourceResponse(document, objectMapper); - } - else { + } else { Document document = objectMapper.readValue(body, format.getDocumentClass()); ClientDocumentMapper documentMapper = client.getDocumentMapper(); @@ -109,8 +123,7 @@ protected Object execute(String url, ResponseType responseType, HttpMethod metho } } return null; - } - catch (IOException e) { + } catch (IOException e) { throw new TransportException(e); } } @@ -127,8 +140,7 @@ private static Object toResourceResponse(Document document, ObjectMapper objectM list.setLinks(new JsonLinksInformation(document.getMeta(), objectMapper)); } return list; - } - else { + } else { return data; } } @@ -165,16 +177,32 @@ public static RuntimeException handleError(CrnkClient client, HttpAdapterRespons Throwable throwable = mapper.get().fromErrorResponse(errorResponse); if (throwable instanceof RuntimeException) { return (RuntimeException) throwable; - } - else { + } else { return new ClientException(response.code(), response.message(), throwable); } - } - else { + } else { return new ClientException(response.code(), response.message()); } } + protected String serializeFilter(QuerySpec querySpec, ResourceInformation resourceInformation) { + if (querySpec.getFilters() == null || querySpec.getFilters().isEmpty()) { + return null; + } + // filter lists without operator does not work well with JSON serialization -> replace with AND expression + List filters = querySpec.getFilters(); + if (filters.size() > 1) { + filters = Collections.singletonList(FilterSpec.and(filters)); + } + JsonNode jsonNode = filterSpecMapper.serialize(resourceInformation, filters, client.getQueryContext()); + try { + return compactMapper.writeValueAsString(jsonNode); + } catch (JsonProcessingException e) { + throw new IllegalStateException(e); + } + } + + public enum ResponseType { NONE, RESOURCE, RESOURCES } diff --git a/crnk-client/src/main/java/io/crnk/client/internal/RelationshipRepositoryStubImpl.java b/crnk-client/src/main/java/io/crnk/client/internal/RelationshipRepositoryStubImpl.java index 31f59f51d..b852cbf2a 100644 --- a/crnk-client/src/main/java/io/crnk/client/internal/RelationshipRepositoryStubImpl.java +++ b/crnk-client/src/main/java/io/crnk/client/internal/RelationshipRepositoryStubImpl.java @@ -21,114 +21,123 @@ import java.util.Collection; public class RelationshipRepositoryStubImpl extends ClientStubBase - implements RelationshipRepository { - - private Class sourceClass; - - private Class targetClass; - - private ResourceInformation sourceResourceInformation; - - public RelationshipRepositoryStubImpl(CrnkClient client, Class sourceClass, Class targetClass, - ResourceInformation sourceResourceInformation, UrlBuilder urlBuilder) { - super(client, urlBuilder, targetClass); - this.sourceClass = sourceClass; - this.targetClass = targetClass; - this.sourceResourceInformation = sourceResourceInformation; - } - - @Override - public void setRelation(T source, J targetId, String fieldName) { - Serializable sourceId = getSourceId(source); - String url = urlBuilder.buildUrl(client.getQueryContext(), sourceResourceInformation, sourceId, (QuerySpec) null, fieldName); - executeWithId(url, HttpMethod.PATCH, targetId); - } - - @Override - public void setRelations(T source, Collection targetIds, String fieldName) { - Serializable sourceId = getSourceId(source); - String url = urlBuilder.buildUrl(client.getQueryContext(), sourceResourceInformation, sourceId, (QuerySpec) null, fieldName); - executeWithIds(url, HttpMethod.PATCH, targetIds); - } - - @Override - public void addRelations(T source, Collection targetIds, String fieldName) { - Serializable sourceId = getSourceId(source); - String url = urlBuilder.buildUrl(client.getQueryContext(), sourceResourceInformation, sourceId, (QuerySpec) null, fieldName); - executeWithIds(url, HttpMethod.POST, targetIds); - } - - @Override - public void removeRelations(T source, Collection targetIds, String fieldName) { - Serializable sourceId = getSourceId(source); - String url = urlBuilder.buildUrl(client.getQueryContext(), sourceResourceInformation, sourceId, (QuerySpec) null, fieldName); - executeWithIds(url, HttpMethod.DELETE, targetIds); - } - - private Serializable getSourceId(T source) { - if (source instanceof Resource) { - return ((Resource) source).getId(); - } - ResourceField idField = sourceResourceInformation.getIdField(); - return (Serializable) idField.getAccessor().getValue(source); - } - - @SuppressWarnings("unchecked") - @Override - public D findOneTarget(I sourceId, String fieldName, QuerySpec querySpec) { - verifyQuerySpec(querySpec); - String url = urlBuilder.buildUrl(client.getQueryContext(), sourceResourceInformation, sourceId, querySpec, fieldName, false); - return (D) executeGet(url, ResponseType.RESOURCE); - } - - @Override - public DefaultResourceList findManyTargets(I sourceId, String fieldName, QuerySpec querySpec) { - verifyQuerySpec(querySpec); - String url = urlBuilder.buildUrl(client.getQueryContext(), sourceResourceInformation, sourceId, querySpec, fieldName, false); - return (DefaultResourceList) executeGet(url, ResponseType.RESOURCES); - } - - protected void verifyQuerySpec(QuerySpec querySpec) { - Class resourceClass = querySpec.getResourceClass(); - if (resourceClass != null && !resourceClass.equals(targetClass)) { - throw new BadRequestException("resourceClass mismatch between repository and QuerySpec argument: " - + resourceClass + " vs " + targetClass); - } - } - - - private void executeWithIds(String requestUrl, HttpMethod method, Collection targetIds) { - Document document = new Document(); - ArrayList resourceIdentifiers = new ArrayList<>(); - for (Object targetId : targetIds) { - resourceIdentifiers.add(sourceResourceInformation.toResourceIdentifier(targetId)); - } - document.setData(Nullable.of(resourceIdentifiers)); - Document transportDocument = client.getFormat().toTransportDocument(document); - doExecute(requestUrl, method, transportDocument); - } - - private void executeWithId(String requestUrl, HttpMethod method, Object targetId) { - Document document = new Document(); - ResourceIdentifier resourceIdentifier = sourceResourceInformation.toResourceIdentifier(targetId); - document.setData(Nullable.of(resourceIdentifier)); - Document transportDocument = client.getFormat().toTransportDocument(document); - doExecute(requestUrl, method, transportDocument); - } - - private void doExecute(String requestUrl, HttpMethod method, final Document document) { - final ObjectMapper objectMapper = client.getObjectMapper(); - String requestBodyValue = ExceptionUtil.wrapCatchedExceptions(() -> objectMapper.writeValueAsString(document)); - execute(requestUrl, ResponseType.NONE, method, requestBodyValue); - } - - @Override - public Class getSourceResourceClass() { - return sourceClass; - } - - @Override - public Class getTargetResourceClass() { - return targetClass; - } + implements RelationshipRepository { + + private Class sourceClass; + + private Class targetClass; + + private ResourceInformation sourceResourceInformation; + + public RelationshipRepositoryStubImpl(CrnkClient client, Class sourceClass, Class targetClass, + ResourceInformation sourceResourceInformation, UrlBuilder urlBuilder, + boolean filterCriteriaInRequestBody) { + super(client, urlBuilder, targetClass, filterCriteriaInRequestBody); + this.sourceClass = sourceClass; + this.targetClass = targetClass; + this.sourceResourceInformation = sourceResourceInformation; + } + + @Override + public void setRelation(T source, J targetId, String fieldName) { + Serializable sourceId = getSourceId(source); + String url = urlBuilder.buildUrl(client.getQueryContext(), sourceResourceInformation, sourceId, (QuerySpec) null, fieldName); + executeWithId(url, HttpMethod.PATCH, targetId); + } + + @Override + public void setRelations(T source, Collection targetIds, String fieldName) { + Serializable sourceId = getSourceId(source); + String url = urlBuilder.buildUrl(client.getQueryContext(), sourceResourceInformation, sourceId, (QuerySpec) null, fieldName); + executeWithIds(url, HttpMethod.PATCH, targetIds); + } + + @Override + public void addRelations(T source, Collection targetIds, String fieldName) { + Serializable sourceId = getSourceId(source); + String url = urlBuilder.buildUrl(client.getQueryContext(), sourceResourceInformation, sourceId, (QuerySpec) null, fieldName); + executeWithIds(url, HttpMethod.POST, targetIds); + } + + @Override + public void removeRelations(T source, Collection targetIds, String fieldName) { + Serializable sourceId = getSourceId(source); + String url = urlBuilder.buildUrl(client.getQueryContext(), sourceResourceInformation, sourceId, (QuerySpec) null, fieldName); + executeWithIds(url, HttpMethod.DELETE, targetIds); + } + + private Serializable getSourceId(T source) { + if (source instanceof Resource) { + return ((Resource) source).getId(); + } + ResourceField idField = sourceResourceInformation.getIdField(); + return (Serializable) idField.getAccessor().getValue(source); + } + + @SuppressWarnings("unchecked") + @Override + public D findOneTarget(I sourceId, String fieldName, QuerySpec querySpec) { + verifyQuerySpec(querySpec); + String url = urlBuilder.buildUrl(client.getQueryContext(), sourceResourceInformation, sourceId, querySpec, fieldName, false); + String body = null; + if (filterCriteriaInRequestBody) { + body = serializeFilter(querySpec, sourceResourceInformation); + } + return (D) executeGet(url, body, ResponseType.RESOURCE); + } + + @Override + public DefaultResourceList findManyTargets(I sourceId, String fieldName, QuerySpec querySpec) { + verifyQuerySpec(querySpec); + String url = urlBuilder.buildUrl(client.getQueryContext(), sourceResourceInformation, sourceId, querySpec, fieldName, false); + String body = null; + if (filterCriteriaInRequestBody) { + body = serializeFilter(querySpec, sourceResourceInformation); + } + return (DefaultResourceList) executeGet(url, body, ResponseType.RESOURCES); + } + + protected void verifyQuerySpec(QuerySpec querySpec) { + Class resourceClass = querySpec.getResourceClass(); + if (resourceClass != null && !resourceClass.equals(targetClass)) { + throw new BadRequestException("resourceClass mismatch between repository and QuerySpec argument: " + + resourceClass + " vs " + targetClass); + } + } + + + private void executeWithIds(String requestUrl, HttpMethod method, Collection targetIds) { + Document document = new Document(); + ArrayList resourceIdentifiers = new ArrayList<>(); + for (Object targetId : targetIds) { + resourceIdentifiers.add(sourceResourceInformation.toResourceIdentifier(targetId)); + } + document.setData(Nullable.of(resourceIdentifiers)); + Document transportDocument = client.getFormat().toTransportDocument(document); + doExecute(requestUrl, method, transportDocument); + } + + private void executeWithId(String requestUrl, HttpMethod method, Object targetId) { + Document document = new Document(); + ResourceIdentifier resourceIdentifier = sourceResourceInformation.toResourceIdentifier(targetId); + document.setData(Nullable.of(resourceIdentifier)); + Document transportDocument = client.getFormat().toTransportDocument(document); + doExecute(requestUrl, method, transportDocument); + } + + private void doExecute(String requestUrl, HttpMethod method, final Document document) { + final ObjectMapper objectMapper = client.getObjectMapper(); + String requestBodyValue = ExceptionUtil.wrapCatchedExceptions(() -> objectMapper.writeValueAsString(document)); + execute(requestUrl, ResponseType.NONE, method, requestBodyValue); + } + + @Override + public Class getSourceResourceClass() { + return sourceClass; + } + + @Override + public Class getTargetResourceClass() { + return targetClass; + } } diff --git a/crnk-client/src/main/java/io/crnk/client/internal/ResourceRepositoryStubImpl.java b/crnk-client/src/main/java/io/crnk/client/internal/ResourceRepositoryStubImpl.java index 6241d8ec5..692f88282 100644 --- a/crnk-client/src/main/java/io/crnk/client/internal/ResourceRepositoryStubImpl.java +++ b/crnk-client/src/main/java/io/crnk/client/internal/ResourceRepositoryStubImpl.java @@ -11,7 +11,6 @@ import io.crnk.core.engine.information.resource.ResourceInformation; import io.crnk.core.engine.internal.document.mapper.DocumentMappingConfig; import io.crnk.core.engine.internal.utils.ExceptionUtil; -import io.crnk.core.engine.internal.utils.JsonApiUrlBuilder; import io.crnk.core.engine.query.QueryAdapter; import io.crnk.core.exception.BadRequestException; import io.crnk.core.queryspec.QuerySpec; @@ -37,8 +36,8 @@ public class ResourceRepositoryStubImpl extends ClientStubBase public ResourceRepositoryStubImpl(CrnkClient client, Class resourceClass, ResourceInformation resourceInformation, - UrlBuilder urlBuilder) { - super(client, urlBuilder, resourceClass); + UrlBuilder urlBuilder, boolean filterCriteriaInRequestBody) { + super(client, urlBuilder, resourceClass, filterCriteriaInRequestBody); this.resourceInformation = resourceInformation; } @@ -171,19 +170,27 @@ public T findOne(I id, QuerySpec querySpec) { public DefaultResourceList findAll(QuerySpec querySpec) { verifyQuerySpec(querySpec); String url = urlBuilder.buildUrl(client.getQueryContext(), resourceInformation, null, querySpec); - return findAll(url); + String body = null; + if (filterCriteriaInRequestBody) { + body = serializeFilter(querySpec, resourceInformation); + } + return findAll(url, body); } @Override public DefaultResourceList findAll(Collection ids, QuerySpec querySpec) { verifyQuerySpec(querySpec); String url = urlBuilder.buildUrl(client.getQueryContext(), resourceInformation, ids, querySpec); - return findAll(url); + String body = null; + if (filterCriteriaInRequestBody) { + body = serializeFilter(querySpec, resourceInformation); + } + return findAll(url, body); } @SuppressWarnings("unchecked") - public DefaultResourceList findAll(String url) { - return (DefaultResourceList) executeGet(url, ResponseType.RESOURCES); + public DefaultResourceList findAll(String url, String body) { + return (DefaultResourceList) executeGet(url, body, ResponseType.RESOURCES); } protected void verifyQuerySpec(QuerySpec querySpec) { @@ -196,7 +203,7 @@ protected void verifyQuerySpec(QuerySpec querySpec) { @SuppressWarnings("unchecked") protected T findOne(String url) { - return (T) executeGet(url, ResponseType.RESOURCE); + return (T) executeGet(url, null, ResponseType.RESOURCE); } } diff --git a/crnk-client/src/main/java/io/crnk/client/internal/proxy/ClientProxyFactoryContext.java b/crnk-client/src/main/java/io/crnk/client/internal/proxy/ClientProxyFactoryContext.java index 0a3d8fb6d..35b3dcc84 100644 --- a/crnk-client/src/main/java/io/crnk/client/internal/proxy/ClientProxyFactoryContext.java +++ b/crnk-client/src/main/java/io/crnk/client/internal/proxy/ClientProxyFactoryContext.java @@ -7,6 +7,9 @@ public interface ClientProxyFactoryContext { ModuleRegistry getModuleRegistry(); - DefaultResourceList getCollection(Class resourceClass, String url); + DefaultResourceList getCollection(Class resourceClass, String url, String body); + default DefaultResourceList getCollection(Class resourceClass, String url) { + return getCollection(resourceClass, url, null); + } } diff --git a/crnk-client/src/test/java/io/crnk/client/FilterCriteriaInBodyClientTest.java b/crnk-client/src/test/java/io/crnk/client/FilterCriteriaInBodyClientTest.java new file mode 100644 index 000000000..3cc07f717 --- /dev/null +++ b/crnk-client/src/test/java/io/crnk/client/FilterCriteriaInBodyClientTest.java @@ -0,0 +1,160 @@ +package io.crnk.client; + +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.crnk.client.http.apache.HttpClientAdapter; +import io.crnk.core.boot.CrnkProperties; +import io.crnk.core.exception.InternalServerErrorException; +import io.crnk.core.queryspec.*; +import io.crnk.core.queryspec.mapper.DefaultQuerySpecUrlMapper; +import io.crnk.core.repository.ResourceRepository; +import io.crnk.core.resource.list.ResourceList; +import io.crnk.test.mock.models.PrimitiveAttributeResource; +import io.crnk.test.mock.models.Project; +import io.crnk.test.mock.models.Task; +import io.crnk.test.mock.repository.ProjectRepository; +import io.crnk.test.mock.repository.TaskRepository; +import org.apache.commons.lang3.time.DateUtils; +import org.junit.Before; +import org.junit.Test; + +import java.time.LocalDate; +import java.util.*; + +import static junit.framework.TestCase.assertNotNull; +import static org.junit.Assert.assertEquals; + +public class FilterCriteriaInBodyClientTest extends AbstractClientTest { + private static final int PROJECT_COUNT = 10; + + private final ProjectRepository projectRepository = new ProjectRepository(); + private final TaskRepository taskRepository = new TaskRepository(); + + @Before + public void setup() { + super.setup(); + createProjects(); + } + + private void createProjects() { + for (int i = 0; i < PROJECT_COUNT; i++) { + Task task = new Task(); + task.setId((long) i); + task.setName("Task " + i); + taskRepository.create(task); + Project project = new Project(); + project.setId((long) i); + project.setName("Project " + (char) ('A' + i)); + project.getTasks().add(task); + if (i == 5) { + project.setDescription("Test"); + } + projectRepository.create(project); + } + } + + @Override + protected TestApplication configure() { + TestApplication app = super.configure(); + Map properties = new HashMap<>(); + properties.put(CrnkProperties.FILTER_CRITERIA_IN_HTTP_BODY, Boolean.TRUE.toString()); + properties.put(CrnkProperties.ALLOW_UNKNOWN_ATTRIBUTES, Boolean.TRUE.toString()); + app.setProperties(properties); + return app; + } + + @Override + protected void setupClient(CrnkClient client) { + client.setHttpAdapter(new HttpClientAdapter()); + client.setFilterCriteriaInRequestBody(true); + ((DefaultQuerySpecUrlMapper) client.getUrlMapper()).setAllowUnknownAttributes(true); + client.getObjectMapper().disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + client.getObjectMapper().registerModule(new JavaTimeModule()); + } + + @Test + public void testWithoutFilter() { + ResourceRepository projectRepo = client.getRepositoryForType(Project.class); + ResourceList projects = projectRepo.findAll(new QuerySpec(Project.class)); + assertNotNull(projects); + assertEquals(PROJECT_COUNT, projects.size()); + } + + @Test + public void testWithSingleFilter() { + QuerySpec querySpec = new QuerySpec(Project.class); + querySpec.addFilter(PathSpec.of("id").filter(FilterOperator.LT, 5)); + ResourceRepository projectRepo = client.getRepositoryForType(Project.class); + ResourceList projects = projectRepo.findAll(querySpec); + assertNotNull(projects); + assertEquals(4, projects.size()); + } + + @Test + public void testWithCombinedFilter() { + QuerySpec querySpec = new QuerySpec(Project.class); + querySpec.addFilter(FilterSpec.or( + PathSpec.of("id").filter(FilterOperator.LT, 5), + PathSpec.of("description").filter(FilterOperator.EQ, "Test"))); + ResourceRepository projectRepo = client.getRepositoryForType(Project.class); + ResourceList projects = projectRepo.findAll(querySpec); + assertNotNull(projects); + assertEquals(5, projects.size()); + } + + @Test(expected = InternalServerErrorException.class) + public void testUnknownFilterAttributeIsPassedToServer() { + ResourceRepository projectRepo = client.getRepositoryForType(Project.class); + QuerySpec querySpec = new QuerySpec(Project.class); + String unknownPath = "unknown"; + querySpec.addFilter(PathSpec.of(unknownPath).filter(FilterOperator.EQ, 1)); + projectRepo.findAll(querySpec); + } + + @Test + public void filterByDateWithPlainDate() { + Date now = new Date(); + createResource(now); + + QuerySpec querySpec = new QuerySpec(PrimitiveAttributeResource.class); + Date max = DateUtils.addDays(now, 1); + querySpec.addFilter(PathSpec.of("dateValue").filter(FilterOperator.LT, max)); + ResourceRepository primitiveRepo = client.getRepositoryForType(PrimitiveAttributeResource.class); + ResourceList resources = primitiveRepo.findAll(querySpec); + assertNotNull(resources); + assertEquals(1, resources.size()); + } + + private void createResource(Date date) { + ResourceRepository primitiveRepo = client.getRepositoryForType(PrimitiveAttributeResource.class); + PrimitiveAttributeResource resource = new PrimitiveAttributeResource(); + resource.setId(1L); + resource.setDateValue(date); + primitiveRepo.create(resource); + } + + @Test + public void filterByDateWithLocalDate() { + Date now = new Date(); + createResource(now); + + QuerySpec querySpec = new QuerySpec(PrimitiveAttributeResource.class); + LocalDate max = LocalDate.now().plusDays(-1); + querySpec.addFilter(PathSpec.of("dateValue").filter(FilterOperator.GT, max)); + ResourceRepository primitiveRepo = client.getRepositoryForType(PrimitiveAttributeResource.class); + ResourceList resources = primitiveRepo.findAll(querySpec); + assertNotNull(resources); + assertEquals(1, resources.size()); + } + + @Test + public void testListOfFiltersIsSerializedCorrectly() { + QuerySpec querySpec = new QuerySpec(Project.class); + querySpec.addFilter(PathSpec.of("tasks.id").filter(FilterOperator.EQ, Arrays.asList(3L, 4L))); + querySpec.addFilter(PathSpec.of("tasks.name").filter(FilterOperator.EQ, Arrays.asList("Task 4", "Task 5"))); + ResourceRepository projectRepo = client.getRepositoryForType(Project.class); + ResourceList projects = projectRepo.findAll(querySpec); + assertNotNull(projects); + assertEquals(1, projects.size()); + } +} diff --git a/crnk-client/src/test/java/io/crnk/client/internal/ClientStubBaseTest.java b/crnk-client/src/test/java/io/crnk/client/internal/ClientStubBaseTest.java index 01b887e38..9c940cf37 100644 --- a/crnk-client/src/test/java/io/crnk/client/internal/ClientStubBaseTest.java +++ b/crnk-client/src/test/java/io/crnk/client/internal/ClientStubBaseTest.java @@ -42,7 +42,7 @@ public void setup() { urlBuilder = Mockito.mock(JsonApiUrlBuilder.class); - stub = new ClientStubBase(client, urlBuilder, Task.class); + stub = new ClientStubBase(client, urlBuilder, Task.class, false); } @Test diff --git a/crnk-core/src/main/java/io/crnk/core/boot/CrnkBoot.java b/crnk-core/src/main/java/io/crnk/core/boot/CrnkBoot.java index 680d45dd8..bc55aa0b9 100644 --- a/crnk-core/src/main/java/io/crnk/core/boot/CrnkBoot.java +++ b/crnk-core/src/main/java/io/crnk/core/boot/CrnkBoot.java @@ -543,7 +543,9 @@ private void setupQuerySpecUrlMapper() { if (moduleRegistry.getUrlMapper() == null) { List list = serviceDiscovery.getInstancesByType(QuerySpecUrlMapper.class); if (list.isEmpty()) { - moduleRegistry.setUrlMapper(new DefaultQuerySpecUrlMapper()); + DefaultQuerySpecUrlMapper urlMapper = new DefaultQuerySpecUrlMapper(); + urlMapper.setFilterCriteriaInRequestBody("true".equals(propertiesProvider.getProperty(CrnkProperties.FILTER_CRITERIA_IN_HTTP_BODY))); + moduleRegistry.setUrlMapper(urlMapper); } else { moduleRegistry.setUrlMapper(list.get(0)); diff --git a/crnk-core/src/main/java/io/crnk/core/boot/CrnkProperties.java b/crnk-core/src/main/java/io/crnk/core/boot/CrnkProperties.java index e2e8acc55..4d695ba91 100644 --- a/crnk-core/src/main/java/io/crnk/core/boot/CrnkProperties.java +++ b/crnk-core/src/main/java/io/crnk/core/boot/CrnkProperties.java @@ -152,9 +152,17 @@ private CrnkProperties() { *

* Set a boolean whether Crnk should throw * {@link io.crnk.core.exception.ResourceNotFoundException ResourceNotFoundException} if related resources are missing. - *

+ *

crnk.config.resource.request.filterCriteriaInHttp *

Set this to false to ignore missing related resources.

*/ public static final String EXCEPTION_ON_MISSING_RELATED_RESOURCE = "crnk.config.serialize.relation.exceptionOnMissingRelatedResource"; + /** + *

Set a boolean if crnk should send filter criteria in the HTTP GET request body.

+ *

By default and in compliance with the JSON API Spec, the filter has to be sent as query param in the URL. + * But this might result in errors related to the maximum supported length of URLs, especially with nested filters. + * Sending the parameters in the request body will allow for longer, more complex filters.

+ */ + public static final String FILTER_CRITERIA_IN_HTTP_BODY = "crnk.config.resource.request.filterCriteriaInHttpBody"; + } diff --git a/crnk-core/src/main/java/io/crnk/core/engine/internal/http/JsonApiRequestProcessorBase.java b/crnk-core/src/main/java/io/crnk/core/engine/internal/http/JsonApiRequestProcessorBase.java index e43934d07..374fd4eaa 100644 --- a/crnk-core/src/main/java/io/crnk/core/engine/internal/http/JsonApiRequestProcessorBase.java +++ b/crnk-core/src/main/java/io/crnk/core/engine/internal/http/JsonApiRequestProcessorBase.java @@ -9,10 +9,7 @@ import io.crnk.core.engine.dispatcher.Response; import io.crnk.core.engine.document.Document; import io.crnk.core.engine.document.ErrorData; -import io.crnk.core.engine.http.HttpHeaders; -import io.crnk.core.engine.http.HttpRequestContext; -import io.crnk.core.engine.http.HttpResponse; -import io.crnk.core.engine.http.HttpStatus; +import io.crnk.core.engine.http.*; import io.crnk.core.exception.MethodNotAllowedException; import io.crnk.core.module.Module; import org.slf4j.Logger; @@ -70,7 +67,7 @@ protected String getContentType() { protected Document getRequestDocument(HttpRequestContext requestContext) throws JsonProcessingException { byte[] requestBody = requestContext.getRequestBody(); - if (requestBody != null && requestBody.length > 0) { + if (requestBody != null && requestBody.length > 0 && !HttpMethod.GET.toString().equals(requestContext.getMethod())) { ObjectMapper objectMapper = moduleContext.getObjectMapper(); try { return objectMapper.readerFor(Document.class).readValue(requestBody); diff --git a/crnk-core/src/main/java/io/crnk/core/queryspec/mapper/DefaultQuerySpecUrlMapper.java b/crnk-core/src/main/java/io/crnk/core/queryspec/mapper/DefaultQuerySpecUrlMapper.java index 08c08d6e5..ebfd67dc1 100644 --- a/crnk-core/src/main/java/io/crnk/core/queryspec/mapper/DefaultQuerySpecUrlMapper.java +++ b/crnk-core/src/main/java/io/crnk/core/queryspec/mapper/DefaultQuerySpecUrlMapper.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import io.crnk.core.engine.http.HttpMethod; import io.crnk.core.engine.information.resource.ResourceInformation; import io.crnk.core.engine.internal.utils.ClassUtils; import io.crnk.core.engine.internal.utils.PreconditionUtil; @@ -29,6 +30,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -57,6 +59,8 @@ public class DefaultQuerySpecUrlMapper private boolean allowUnknownParameters = false; + private boolean filterCriteriaInRequestBody; + protected QuerySpecUrlContext context; protected QueryPathResolver pathResolver = new DefaultQueryPathResolver(); @@ -91,6 +95,10 @@ public void init(QuerySpecUrlContext ctx) { jsonParser = new JsonFilterSpecMapper(ctx, supportedOperators, defaultOperator, pathResolver); } + public JsonFilterSpecMapper getJsonParser() { + return jsonParser; + } + /** * @return true if attribute paths must be separated with ".". Ealier * Crnk versions did made use of brackets @@ -178,6 +186,9 @@ public QuerySpec deserialize(ResourceInformation resourceInformation, Map pagingBehavior = entry.getPagingBehavior(); @@ -212,6 +226,20 @@ public QuerySpec deserialize(ResourceInformation resourceInformation, Map 0) { + try { + String body = new String(requestBody, StandardCharsets.UTF_8); + JsonNode jsonNode = context.getObjectMapper().readTree(body); + List filterSpecs = jsonParser.deserialize(jsonNode, resourceInformation, queryContext); + filterSpecs.forEach(rootQuerySpec::addFilter); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } + private static void put(Map> map, String key, String value) { map.put(key, new HashSet<>(Arrays.asList(value))); } @@ -270,7 +298,9 @@ protected void serialize(QuerySpec querySpec, Map> map, Quer boolean isRoot = querySpec == rootQuerySpec; - serializeFilters(querySpec, resourceInformation, map, isRoot, queryContext); + if (! filterCriteriaInRequestBody) { + serializeFilters(querySpec, resourceInformation, map, isRoot, queryContext); + } serializeSorting(querySpec, resourceInformation, map, isRoot, queryContext); serializeIncludedFields(querySpec, resourceInformation, map, isRoot, queryContext); serializeIncludedRelations(querySpec, resourceInformation, map, isRoot, queryContext); @@ -706,4 +736,12 @@ public boolean getAllowUnknownParameters() { public QueryPathResolver getPathResolver() { return pathResolver; } + + public boolean isFilterCriteriaInRequestBody() { + return filterCriteriaInRequestBody; + } + + public void setFilterCriteriaInRequestBody(boolean filterCriteriaInRequestBody) { + this.filterCriteriaInRequestBody = filterCriteriaInRequestBody; + } } diff --git a/crnk-core/src/test/java/io/crnk/core/mock/TestHttpRequestContext.java b/crnk-core/src/test/java/io/crnk/core/mock/TestHttpRequestContext.java new file mode 100644 index 000000000..c080dbf2c --- /dev/null +++ b/crnk-core/src/test/java/io/crnk/core/mock/TestHttpRequestContext.java @@ -0,0 +1,108 @@ +package io.crnk.core.mock; + +import io.crnk.core.engine.http.HttpMethod; +import io.crnk.core.engine.http.HttpRequestContext; +import io.crnk.core.engine.http.HttpResponse; +import io.crnk.core.engine.query.QueryContext; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Set; + +public class TestHttpRequestContext implements HttpRequestContext { + private byte[] requestBody = new byte[0]; + + @Override + public boolean accepts(String contentType) { + return false; + } + + @Override + public boolean acceptsAny() { + return false; + } + + @Override + public T unwrap(Class type) { + return null; + } + + @Override + public Object getRequestAttribute(String name) { + return null; + } + + @Override + public void setRequestAttribute(String name, Object value) { + + } + + @Override + public QueryContext getQueryContext() { + return null; + } + + @Override + public boolean hasResponse() { + return false; + } + + @Override + public Set getRequestHeaderNames() { + return null; + } + + @Override + public String getRequestHeader(String name) { + return null; + } + + @Override + public Map> getRequestParameters() { + return null; + } + + @Override + public String getPath() { + return null; + } + + @Override + public String getBaseUrl() { + return null; + } + + @Override + public byte[] getRequestBody() { + return requestBody; + } + + public void setRequestBody(byte[] requestBody) { + this.requestBody = requestBody; + } + + public void setRequestBody(String body) { + requestBody = body.getBytes(StandardCharsets.UTF_8); + } + + @Override + public String getMethod() { + return HttpMethod.GET.toString(); + } + + @Override + public URI getRequestUri() { + return null; + } + + @Override + public HttpResponse getResponse() { + return null; + } + + @Override + public void setResponse(HttpResponse response) { + + } +} diff --git a/crnk-core/src/test/java/io/crnk/core/queryspec/mapper/DefaultQuerySpecUrlMapperDeserializerFromBodyTest.java b/crnk-core/src/test/java/io/crnk/core/queryspec/mapper/DefaultQuerySpecUrlMapperDeserializerFromBodyTest.java new file mode 100644 index 000000000..823e96878 --- /dev/null +++ b/crnk-core/src/test/java/io/crnk/core/queryspec/mapper/DefaultQuerySpecUrlMapperDeserializerFromBodyTest.java @@ -0,0 +1,127 @@ +package io.crnk.core.queryspec.mapper; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.crnk.core.CoreTestContainer; +import io.crnk.core.engine.information.resource.ResourceInformation; +import io.crnk.core.engine.parser.TypeParser; +import io.crnk.core.engine.registry.RegistryEntry; +import io.crnk.core.engine.registry.ResourceRegistry; +import io.crnk.core.mock.TestHttpRequestContext; +import io.crnk.core.mock.models.Task; +import io.crnk.core.mock.repository.TaskWithPagingBehavior; +import io.crnk.core.mock.repository.TaskWithPagingBehaviorRepository; +import io.crnk.core.mock.repository.TaskWithPagingBehaviorToProjectRepository; +import io.crnk.core.module.SimpleModule; +import io.crnk.core.queryspec.*; +import io.crnk.core.queryspec.pagingspec.CustomOffsetLimitPagingBehavior; +import io.crnk.core.queryspec.pagingspec.OffsetLimitPagingBehavior; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +public class DefaultQuerySpecUrlMapperDeserializerFromBodyTest extends AbstractQuerySpecTest { + protected DefaultQuerySpecUrlMapper urlMapper; + + protected ResourceInformation taskInformation; + + private ResourceInformation taskWithPagingBehaviorInformation; + + private QuerySpecUrlContext deserializerContext; + private TestHttpRequestContext httpRequestContext; + + @Before + public void setup() { + super.setup(); + + deserializerContext = new QuerySpecUrlContext() { + + @Override + public ResourceRegistry getResourceRegistry() { + return resourceRegistry; + } + + @Override + public TypeParser getTypeParser() { + return moduleRegistry.getTypeParser(); + } + + @Override + public ObjectMapper getObjectMapper() { + return container.getObjectMapper(); + } + + @Override + public UrlBuilder getUrlBuilder() { + return container.getModuleRegistry().getUrlBuilder(); + } + }; + + urlMapper = new DefaultQuerySpecUrlMapper(); + urlMapper.setFilterCriteriaInRequestBody(true); + urlMapper.init(deserializerContext); + + RegistryEntry taskEntry = resourceRegistry.getEntry(Task.class); + taskInformation = taskEntry.getResourceInformation(); + taskWithPagingBehaviorInformation = resourceRegistry.getEntry(TaskWithPagingBehavior.class).getResourceInformation(); + + httpRequestContext = new TestHttpRequestContext(); + queryContext.setRequestContext(httpRequestContext); + } + + @Override + protected void setup(CoreTestContainer container) { + super.setup(container); + SimpleModule customPagingModule = new SimpleModule("customPaging"); + customPagingModule.addRepository(new TaskWithPagingBehaviorRepository()); + customPagingModule.addRepository(new TaskWithPagingBehaviorToProjectRepository()); + customPagingModule.addPagingBehavior(new OffsetLimitPagingBehavior()); + customPagingModule.addPagingBehavior(new CustomOffsetLimitPagingBehavior()); + container.addModule(customPagingModule); + } + + @Test + public void testEmptyFilter() { + QuerySpec querySpec = urlMapper.deserialize(taskInformation, Collections.emptyMap(), queryContext); + assertNotNull(querySpec.getFilters()); + assertTrue(querySpec.getFilters().isEmpty()); + } + + @Test + public void testSimpleEqualsFilter() { + httpRequestContext.setRequestBody("{\"name\": \"test\"}"); + QuerySpec querySpec = urlMapper.deserialize(taskInformation, Collections.emptyMap(), queryContext); + List filters = querySpec.getFilters(); + assertNotNull(filters); + assertEquals(1, filters.size()); + FilterSpec filter = filters.get(0); + assertEquals(FilterOperator.EQ, filter.getOperator()); + assertEquals(Collections.singletonList("name"), filter.getAttributePath()); + assertEquals("test", filter.getValue()); + } + + @Test + public void testCompoundFilter() { + httpRequestContext.setRequestBody("{ \"OR\": [ {\"id\": [12, 13, 14]}, {\"name\": \"test\"} ] }"); + QuerySpec querySpec = urlMapper.deserialize(taskInformation, Collections.emptyMap(), queryContext); + List filters = querySpec.getFilters(); + assertNotNull(filters); + assertEquals(1, filters.size()); + FilterSpec filter = filters.get(0); + assertEquals(FilterOperator.OR, filter.getOperator()); + List expression = Arrays.asList( + PathSpec.of("id").filter(FilterOperator.EQ, Arrays.asList(12L, 13L, 14L)), + PathSpec.of("name").filter(FilterOperator.EQ, "test") + ); + assertEquals(expression, filter.getExpression()); + } + + @Test(expected = IllegalStateException.class) + public void testFilterInUrlResultsInException() { + urlMapper.deserialize(taskInformation, Collections.singletonMap("filter[name]", Collections.singleton("test")), queryContext); + } +} diff --git a/crnk-core/src/test/java/io/crnk/core/queryspec/mapper/DefaultQuerySpecUrlMapperSerializerTest.java b/crnk-core/src/test/java/io/crnk/core/queryspec/mapper/DefaultQuerySpecUrlMapperSerializerTest.java index eed20d919..b2b40a086 100644 --- a/crnk-core/src/test/java/io/crnk/core/queryspec/mapper/DefaultQuerySpecUrlMapperSerializerTest.java +++ b/crnk-core/src/test/java/io/crnk/core/queryspec/mapper/DefaultQuerySpecUrlMapperSerializerTest.java @@ -12,11 +12,7 @@ import io.crnk.core.mock.models.Project; import io.crnk.core.mock.models.Schedule; import io.crnk.core.mock.models.Task; -import io.crnk.core.queryspec.Direction; -import io.crnk.core.queryspec.FilterOperator; -import io.crnk.core.queryspec.FilterSpec; -import io.crnk.core.queryspec.QuerySpec; -import io.crnk.core.queryspec.SortSpec; +import io.crnk.core.queryspec.*; import io.crnk.core.queryspec.pagingspec.NumberSizePagingBehavior; import io.crnk.core.queryspec.pagingspec.OffsetLimitPagingBehavior; import org.junit.Assert; @@ -49,6 +45,7 @@ public void setup() { container.boot(); urlMapper = (DefaultQuerySpecUrlMapper) container.getBoot().getUrlMapper(); + urlMapper.setFilterCriteriaInRequestBody(false); resourceRegistry = container.getResourceRegistry(); urlBuilder = container.getModuleRegistry().getUrlBuilder(); @@ -278,4 +275,12 @@ private void check(String expectedUrl, Object id, QuerySpec querySpec) { String actualUrl = urlBuilder.buildUrl(queryContext, entry.getResourceInformation(), id, querySpec); assertEquals(expectedUrl, actualUrl); } + + @Test + public void testFilterNotAddedToUrlWhenInHttpBody() { + urlMapper.setFilterCriteriaInRequestBody(true); + QuerySpec querySpec = new QuerySpec(Schedule.class); + querySpec.addFilter(new FilterSpec(PathSpec.of("desc"), FilterOperator.EQ, "test")); + check("http://127.0.0.1/schedules", null, querySpec); + } } diff --git a/crnk-ui/package-lock.json b/crnk-ui/package-lock.json index 311af2100..d453d72c5 100644 --- a/crnk-ui/package-lock.json +++ b/crnk-ui/package-lock.json @@ -470,15 +470,13 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.2.tgz", "integrity": "sha512-ZpYajIfO0j2cOFTO955KUMIKNmj6zhX8kVztMAxFsDaMwz+9Z9SV0uou2pC9HJqcfpffOsjnbrDMvkNy+9RXPw==", - "dev": true, - "optional": true + "dev": true }, "are-we-there-yet": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", "dev": true, - "optional": true, "requires": { "delegates": "1.0.0", "readable-stream": "2.3.3" @@ -1522,8 +1520,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "constants-browserify": { "version": "1.0.0", @@ -1919,8 +1916,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true + "dev": true }, "denodeify": { "version": "1.2.1", @@ -2733,7 +2729,6 @@ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", "dev": true, - "optional": true, "requires": { "graceful-fs": "4.1.11", "inherits": "2.0.3", @@ -2752,7 +2747,6 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, - "optional": true, "requires": { "aproba": "1.1.2", "console-control-strings": "1.1.0", @@ -2769,7 +2763,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "1.0.1" } @@ -2779,7 +2772,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "1.1.0", "is-fullwidth-code-point": "1.0.0", @@ -2808,8 +2800,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true, - "optional": true + "dev": true }, "getpass": { "version": "0.1.7", @@ -3015,8 +3006,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true + "dev": true }, "hash-base": { "version": "2.0.2", @@ -4554,8 +4544,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true, - "optional": true + "dev": true }, "matcher-collection": { "version": "1.0.4", @@ -4966,7 +4955,6 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, - "optional": true, "requires": { "are-we-there-yet": "1.1.4", "console-control-strings": "1.1.0", @@ -7281,6 +7269,15 @@ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", "dev": true }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -7308,15 +7305,6 @@ } } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", @@ -8404,7 +8392,6 @@ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", "dev": true, - "optional": true, "requires": { "string-width": "1.0.2" }, @@ -8414,7 +8401,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "1.0.1" } @@ -8424,7 +8410,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "1.1.0", "is-fullwidth-code-point": "1.0.0",