Merge branch 'develop' of https://github.com/mifosio/portfolio into develop
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
index cd02791..b2868e5 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
@@ -18,6 +18,8 @@
import io.mifos.core.api.annotation.ThrowsException;
import io.mifos.core.api.util.CustomFeignClientsConfiguration;
import io.mifos.portfolio.api.v1.domain.*;
+import io.mifos.portfolio.api.v1.validation.ValidSortColumn;
+import io.mifos.portfolio.api.v1.validation.ValidSortDirection;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -58,7 +60,12 @@
produces = MediaType.ALL_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
- List<Product> getAllProducts(@RequestParam(value = "includeDisabled", required = false) final Boolean includeDisabled);
+ ProductPage getProducts(@RequestParam(value = "includeDisabled", required = false) final Boolean includeDisabled,
+ @RequestParam(value = "term", required = false) final String term,
+ @RequestParam(value = "pageIndex") final Integer pageIndex,
+ @RequestParam(value = "size") final Integer size,
+ @RequestParam(value = "sortColumn", required = false) @ValidSortColumn(value = {"lastModifiedOn", "identifier", "name"}) final String sortColumn,
+ @RequestParam(value = "sortDirection", required = false) @ValidSortDirection final String sortDirection);
@RequestMapping(
value = "/products",
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/ProductPage.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/ProductPage.java
new file mode 100644
index 0000000..32e8962
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/ProductPage.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.portfolio.api.v1.domain;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+public class ProductPage {
+ private List<Product> elements;
+ private Integer totalPages;
+ private Long totalElements;
+
+ public ProductPage(List<Product> elements, Integer totalPages, Long totalElements) {
+ this.elements = elements;
+ this.totalPages = totalPages;
+ this.totalElements = totalElements;
+ }
+
+ public List<Product> getElements() {
+ return elements;
+ }
+
+ public void setElements(List<Product> elements) {
+ this.elements = elements;
+ }
+
+ public Integer getTotalPages() {
+ return totalPages;
+ }
+
+ public void setTotalPages(Integer totalPages) {
+ this.totalPages = totalPages;
+ }
+
+ public Long getTotalElements() {
+ return totalElements;
+ }
+
+ public void setTotalElements(Long totalElements) {
+ this.totalElements = totalElements;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ProductPage casePage = (ProductPage) o;
+ return Objects.equals(elements, casePage.elements) &&
+ Objects.equals(totalPages, casePage.totalPages) &&
+ Objects.equals(totalElements, casePage.totalElements);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(elements, totalPages, totalElements);
+ }
+
+ @Override
+ public String toString() {
+ return "CasePage{" +
+ "elements=" + elements +
+ ", totalPages=" + totalPages +
+ ", totalElements=" + totalElements +
+ '}';
+ }
+}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidSortColumn.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidSortColumn.java
new file mode 100644
index 0000000..97c1578
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidSortColumn.java
@@ -0,0 +1,29 @@
+package io.mifos.portfolio.api.v1.validation;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("WeakerAccess")
+public class CheckValidSortColumn implements ConstraintValidator<ValidSortColumn, String> {
+ private Set<String> allowableColumns;
+
+ @Override
+ public void initialize(final ValidSortColumn constraintAnnotation) {
+ allowableColumns = new HashSet<>(Arrays.asList(constraintAnnotation.value()));
+ }
+
+ @Override
+ public boolean isValid(final String value, final ConstraintValidatorContext context) {
+ return validate(value, allowableColumns);
+ }
+
+ public static boolean validate(String value, Set<String> allowableColumns) {
+ return value == null || allowableColumns.contains(value);
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidSortDirection.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidSortDirection.java
new file mode 100644
index 0000000..ad09a58
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidSortDirection.java
@@ -0,0 +1,24 @@
+package io.mifos.portfolio.api.v1.validation;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("WeakerAccess")
+public class CheckValidSortDirection implements ConstraintValidator<ValidSortDirection, String> {
+ @Override
+ public void initialize(ValidSortDirection constraintAnnotation) {
+
+ }
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext context) {
+ return validate(value);
+ }
+
+ public static boolean validate(String value) {
+ return (value == null) || (value.equals("ASC") || (value.equals("DESC")));
+ }
+}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidAccountAssignments.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidAccountAssignments.java
index d55672a..956b3d0 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidAccountAssignments.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidAccountAssignments.java
@@ -35,6 +35,4 @@
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
-
- int maxLength() default 32;
}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidPaymentCycleUnit.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidPaymentCycleUnit.java
index 816af95..853ab24 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidPaymentCycleUnit.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidPaymentCycleUnit.java
@@ -35,6 +35,4 @@
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
-
- int maxLength() default 32;
}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidSortColumn.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidSortColumn.java
new file mode 100644
index 0000000..cf367a0
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidSortColumn.java
@@ -0,0 +1,25 @@
+package io.mifos.portfolio.api.v1.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+ validatedBy = {CheckValidSortColumn.class}
+)
+public @interface ValidSortColumn {
+ String message() default "Use a sort column available in the data model.";
+
+ Class<?>[] groups() default {};
+
+ Class<? extends Payload>[] payload() default {};
+
+ String[] value();
+}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidSortDirection.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidSortDirection.java
new file mode 100644
index 0000000..b1f6865
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidSortDirection.java
@@ -0,0 +1,23 @@
+package io.mifos.portfolio.api.v1.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+ validatedBy = {CheckValidSortDirection.class}
+)
+public @interface ValidSortDirection {
+ String message() default "Only ASC, and DESC are valid sort directions.";
+
+ Class<?>[] groups() default {};
+
+ Class<? extends Payload>[] payload() default {};
+}
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestProducts.java b/component-test/src/main/java/io/mifos/portfolio/TestProducts.java
index 1db9f00..41160a4 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestProducts.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestProducts.java
@@ -52,10 +52,98 @@
Assert.assertFalse(portfolioManager.getProductEnabled(product.getIdentifier()));
+ {
+ final ProductPage productsPage = portfolioManager.getProducts(true, null, 0, 100, null, null);
+ Assert.assertTrue(productsPage.getElements().contains(productAsSaved));
+ }
+
+ {
+ final ProductPage productsPage = portfolioManager.getProducts(true, product.getIdentifier().substring(2, 5), 0, 100, null, null);
+ Assert.assertTrue(productsPage.getElements().contains(productAsSaved));
+ }
+
+ {
+ final ProductPage productsPage = portfolioManager.getProducts(false, null, 0, 100, null, null);
+ Assert.assertFalse(productsPage.getElements().contains(productAsSaved));
+ }
+
+ {
+ final ProductPage productsPage = portfolioManager.getProducts(false, product.getIdentifier().substring(2, 5), 0, 100, null, null);
+ Assert.assertFalse(productsPage.getElements().contains(productAsSaved));
+ }
+
portfolioManager.enableProduct(product.getIdentifier(), true);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_PRODUCT_ENABLE, product.getIdentifier()));
Assert.assertTrue(portfolioManager.getProductEnabled(product.getIdentifier()));
+
+ {
+ final ProductPage productsPage = portfolioManager.getProducts(false, null, 0, 100, null, null);
+ Assert.assertTrue(productsPage.getElements().contains(productAsSaved));
+ }
+
+ {
+ final ProductPage productsPage = portfolioManager.getProducts(false, product.getIdentifier().substring(2, 5), 0, 100, null, null);
+ Assert.assertTrue(productsPage.getElements().contains(productAsSaved));
+ }
+ }
+
+ @Test
+ public void shouldCorrectlyOrderProducts() throws InterruptedException {
+
+ final Product productA = createAdjustedProduct(x -> {
+ x.setIdentifier("aaaaaaaa");
+ x.setName("ZZZZZZZ");
+ });
+ final Product productZ = createAdjustedProduct(x -> {
+ x.setIdentifier("zzzzzzzz");
+ x.setName("AAAAAAA");
+ });
+
+ final Product productASaved = portfolioManager.getProduct(productA.getIdentifier());
+ final Product productZSaved = portfolioManager.getProduct(productZ.getIdentifier());
+
+ {
+ final ProductPage productsPage = portfolioManager.getProducts(true, null, 0, 100, null, null);
+ Assert.assertEquals(productZSaved, productsPage.getElements().get(0)); //Modified by ordering.
+ }
+
+ {
+ final ProductPage productsPage = portfolioManager.getProducts(true, null, 0, 100, "identifier", "ASC");
+ Assert.assertEquals(productASaved, productsPage.getElements().get(0)); //Alphabetic ordering by identifier
+ }
+
+ {
+ final ProductPage productsPage = portfolioManager.getProducts(true, null, 0, 100, "name", "DESC");
+ Assert.assertEquals(productASaved, productsPage.getElements().get(0)); //Alphabetic ordering by name. Descending.
+ }
+ }
+
+ @Test
+ public void badArgumentsToSortOrderAndDirectionShouldThrow() throws InterruptedException {
+ try {
+ portfolioManager.getProducts(true, null, 0, 100, null, "asc");
+ Assert.fail("Should've thrown");
+ }
+ catch (final IllegalArgumentException ignored) { }
+
+ try {
+ portfolioManager.getProducts(true, null, 0, 100, null, "ACS");
+ Assert.fail("Should've thrown");
+ }
+ catch (final IllegalArgumentException ignored) { }
+
+ try {
+ portfolioManager.getProducts(true, null, 0, 100, "non-existent-column", null);
+ Assert.fail("Should've thrown");
+ }
+ catch (final IllegalArgumentException ignored) { }
+
+ try {
+ portfolioManager.getProducts(true, null, 0, 100, "", null);
+ Assert.fail("Should've thrown");
+ }
+ catch (final IllegalArgumentException ignored) { }
}
@Test
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/ProductRepository.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/ProductRepository.java
index 57c4614..06fa008 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/repository/ProductRepository.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/ProductRepository.java
@@ -15,6 +15,8 @@
*/
package io.mifos.portfolio.service.internal.repository;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@@ -26,4 +28,7 @@
@Repository
public interface ProductRepository extends JpaRepository<ProductEntity, Long> {
Optional<ProductEntity> findByIdentifier(String identifier);
+ Page<ProductEntity> findByEnabled(boolean enabled, Pageable pageable);
+ Page<ProductEntity> findByIdentifierContaining(final String term, final Pageable pageable);
+ Page<ProductEntity> findByEnabledAndIdentifierContaining(boolean enabled, final String term, final Pageable pageable);
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/service/ProductService.java b/service/src/main/java/io/mifos/portfolio/service/internal/service/ProductService.java
index dd97c33..7d8b475 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/service/ProductService.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/service/ProductService.java
@@ -18,13 +18,20 @@
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.Product;
+import io.mifos.portfolio.api.v1.domain.ProductPage;
import io.mifos.portfolio.service.internal.mapper.ProductMapper;
import io.mifos.portfolio.service.internal.repository.ProductEntity;
import io.mifos.portfolio.service.internal.repository.ProductRepository;
import io.mifos.portfolio.service.internal.util.AccountingAdapter;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import java.util.*;
import java.util.stream.Collectors;
@@ -48,8 +55,48 @@
this.accountingAdapter = accountingAdapter;
}
- public List<Product> findAllEntities() {
- return ProductMapper.map(this.productRepository.findAll());
+ public ProductPage findEntities(final boolean includeDisabled,
+ final @Nullable String term,
+ final int pageIndex,
+ final int size,
+ final @Nullable String sortColumn,
+ final @Nullable String sortDirection) {
+ final Pageable pageRequest = new PageRequest(pageIndex,
+ size,
+ translateSortDirection(sortDirection),
+ translateSortColumn(sortColumn));
+
+ final Page<ProductEntity> ret;
+ if (includeDisabled) {
+ if (term == null)
+ ret = productRepository.findAll(pageRequest);
+ else
+ ret = productRepository.findByIdentifierContaining(term, pageRequest);
+ }
+ else {
+ if (term == null)
+ ret = productRepository.findByEnabled(true, pageRequest);
+ else
+ ret = productRepository.findByEnabledAndIdentifierContaining(true, term, pageRequest);
+ }
+
+ return new ProductPage(ProductMapper.map(ret.getContent()), ret.getTotalPages(), ret.getTotalElements());
+ }
+
+ private Sort.Direction translateSortDirection(@Nullable final String sortDirection) {
+ return sortDirection == null ? Sort.Direction.DESC :
+ Sort.Direction.valueOf(sortDirection);
+ }
+
+ private @Nonnull
+ String translateSortColumn(@Nullable final String sortColumn) {
+ if (sortColumn == null)
+ return "lastModifiedOn";
+
+ if (!sortColumn.equals("name") && !sortColumn.equals("identifier") && !sortColumn.equals("lastModifiedOn"))
+ throw new IllegalStateException("Illegal input for Sort Column should've been blocked in Rest Controller.");
+
+ return sortColumn;
}
public Optional<Product> findByIdentifier(final String identifier)
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/ProductRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/ProductRestController.java
index c8f5928..fe117f7 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/ProductRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/ProductRestController.java
@@ -24,6 +24,10 @@
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
import io.mifos.portfolio.api.v1.domain.Pattern;
import io.mifos.portfolio.api.v1.domain.Product;
+import io.mifos.portfolio.api.v1.domain.ProductPage;
+import io.mifos.portfolio.api.v1.validation.CheckValidSortColumn;
+import io.mifos.portfolio.api.v1.validation.CheckValidSortDirection;
+import io.mifos.portfolio.api.v1.validation.ValidSortDirection;
import io.mifos.portfolio.service.internal.command.ChangeEnablingOfProductCommand;
import io.mifos.portfolio.service.internal.command.ChangeProductCommand;
import io.mifos.portfolio.service.internal.command.CreateProductCommand;
@@ -36,8 +40,10 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
+import javax.annotation.Nullable;
import javax.validation.Valid;
-import java.util.List;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.Set;
/**
@@ -47,6 +53,7 @@
@RestController //
@RequestMapping("/products") //
public class ProductRestController {
+ private final static Set<String> VALID_SORT_COLUMNS = new HashSet<>(Arrays.asList("lastModifiedOn", "identifier", "name"));
private final CommandGateway commandGateway;
private final CaseService caseService;
@@ -62,12 +69,23 @@
this.caseService = caseService;
this.productService = productService;
this.patternService = patternService;
+
}
@Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
@RequestMapping(method = RequestMethod.GET) //
- public @ResponseBody List<Product> getAllEntities() {
- return this.productService.findAllEntities();
+ public @ResponseBody
+ ProductPage getProducts(@RequestParam(value = "includeDisabled", required = false) final Boolean includeDisabled,
+ @RequestParam(value = "term", required = false) final @Nullable String term,
+ @RequestParam(value = "pageIndex") final Integer pageIndex,
+ @RequestParam(value = "size") final Integer size,
+ @RequestParam(value = "sortColumn", required = false) final String sortColumn,
+ @RequestParam(value = "sortDirection", required = false) final @Valid @ValidSortDirection String sortDirection) {
+ if (!CheckValidSortColumn.validate(sortColumn, VALID_SORT_COLUMNS))
+ throw ServiceException.badRequest("Invalid sort column ''{0}''. Valid inputs are ''{1}''.", sortColumn, VALID_SORT_COLUMNS);
+ if (!CheckValidSortDirection.validate(sortDirection))
+ throw ServiceException.badRequest("Invalid sort direction ''{0}''.", sortDirection);
+ return this.productService.findEntities(includeDisabled, term, pageIndex, size, sortColumn, sortDirection);
}
@Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)