Added sorting to product listing.
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 5b64d5c..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;
@@ -60,8 +62,10 @@
   )
   ProductPage getProducts(@RequestParam(value = "includeDisabled", required = false) final Boolean includeDisabled,
                           @RequestParam(value = "term", required = false) final String term,
-                          @RequestParam("pageIndex") final Integer pageIndex,
-                          @RequestParam("size") final Integer size);
+                          @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/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 c220b28..41160a4 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestProducts.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestProducts.java
@@ -53,22 +53,22 @@
     Assert.assertFalse(portfolioManager.getProductEnabled(product.getIdentifier()));
 
     {
-      final ProductPage productsPage = portfolioManager.getProducts(true, null, 0, 100);
+      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);
+      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);
+      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);
+      final ProductPage productsPage = portfolioManager.getProducts(false, product.getIdentifier().substring(2, 5), 0, 100, null, null);
       Assert.assertFalse(productsPage.getElements().contains(productAsSaved));
     }
 
@@ -78,17 +78,75 @@
     Assert.assertTrue(portfolioManager.getProductEnabled(product.getIdentifier()));
 
     {
-      final ProductPage productsPage = portfolioManager.getProducts(false, null, 0, 100);
+      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);
+      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
   public void shouldCreateProductWithMaximumLengthEverything() throws InterruptedException {
     final Product product = getTestProductWithMaximumLengthEverything();
     portfolioManager.createProduct(product);
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 20c52ea..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
@@ -30,6 +30,7 @@
 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;
@@ -57,9 +58,13 @@
   public ProductPage findEntities(final boolean includeDisabled,
                                   final @Nullable String term,
                                   final int pageIndex,
-                                  final int size) {
-
-    final Pageable pageRequest = new PageRequest(pageIndex, size, Sort.Direction.DESC, "lastModifiedOn");
+                                  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) {
@@ -78,6 +83,22 @@
     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)
   {
     return productRepository.findByIdentifier(identifier).map(ProductMapper::map);
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 affa139..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
@@ -25,6 +25,9 @@
 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;
@@ -39,6 +42,8 @@
 
 import javax.annotation.Nullable;
 import javax.validation.Valid;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Set;
 
 /**
@@ -48,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;
@@ -63,6 +69,7 @@
     this.caseService = caseService;
     this.productService = productService;
     this.patternService = patternService;
+
   }
 
   @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
@@ -70,9 +77,15 @@
   public @ResponseBody
   ProductPage getProducts(@RequestParam(value = "includeDisabled", required = false) final Boolean includeDisabled,
                           @RequestParam(value = "term", required = false) final @Nullable String term,
-                          @RequestParam("pageIndex") final Integer pageIndex,
-                          @RequestParam("size") final Integer size) {
-    return this.productService.findEntities(includeDisabled, term, pageIndex, size);
+                          @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)