fineract-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From my...@apache.org
Subject [fineract-cn-customer] 11/28: Add ability to get/upload/delete portrait image(png/jpg) for customer
Date Mon, 22 Jan 2018 15:24:48 GMT
This is an automated email from the ASF dual-hosted git repository.

myrle pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract-cn-customer.git

commit 021793bec328a3f539547513c7ca418871d6dbd0
Author: Mark <mark.vanveen@gmail.com>
AuthorDate: Mon May 15 15:25:48 2017 +0200

    Add ability to get/upload/delete portrait image(png/jpg) for customer
---
 api/build.gradle                                   |   4 +-
 .../customer/api/v1/CustomerEventConstants.java    |   6 +
 .../customer/api/v1/client/CustomerManager.java    |  35 +++++-
 .../api/v1/client/PortraitNotFoundException.java   |  19 ++++
 .../api/v1/client/PortraitValidationException.java |  19 ++++
 .../api/v1/config/CustomerFeignClientConfig.java   |  93 ++++++++++++++++
 .../api/v1/config/encoder/CustomDecoder.java       |  47 ++++++++
 .../api/v1/config/encoder/CustomEncoder.java       |  49 +++++++++
 .../main/java/io/mifos/customer/TestCustomer.java  | 101 ++++++++++++++---
 .../customer/listener/CustomerEventListener.java   |  18 +++
 .../internal/command/CreatePortraitCommand.java    |  38 +++++++
 .../internal/command/DeletePortraitCommand.java    |  31 ++++++
 .../command/handler/CustomerAggregate.java         |  70 +++++++-----
 .../service/internal/mapper/PortraitMapper.java    |  37 +++++++
 .../internal/repository/PortraitEntity.java        |  86 +++++++++++++++
 .../internal/repository/PortraitRepository.java    |  32 ++++++
 .../service/internal/service/CustomerService.java  |  22 ++--
 .../rest/controller/CustomerRestController.java    | 121 ++++++++++++++++-----
 service/src/main/resources/application.yml         |   4 +
 .../migrations/mariadb/V2__customer_portrait.sql   |  31 ++++++
 20 files changed, 783 insertions(+), 80 deletions(-)

diff --git a/api/build.gradle b/api/build.gradle
index b6a26a1..7bce588 100644
--- a/api/build.gradle
+++ b/api/build.gradle
@@ -18,7 +18,9 @@ dependencies {
     compile(
             [group: 'org.springframework.cloud', name: 'spring-cloud-starter-feign'],
             [group: 'io.mifos.core', name: 'api', version: versions.frameworkapi],
-            [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator]
+            [group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator],
+            [group: 'io.github.openfeign.form', name: 'feign-form', version: '2.1.0'],
+            [group: 'io.github.openfeign.form', name: 'feign-form-spring', version: '2.1.0']
     )
 }
 
diff --git a/api/src/main/java/io/mifos/customer/api/v1/CustomerEventConstants.java b/api/src/main/java/io/mifos/customer/api/v1/CustomerEventConstants.java
index 434f6f3..9a4a13c 100644
--- a/api/src/main/java/io/mifos/customer/api/v1/CustomerEventConstants.java
+++ b/api/src/main/java/io/mifos/customer/api/v1/CustomerEventConstants.java
@@ -39,6 +39,9 @@ public interface CustomerEventConstants {
   String POST_TASK = "post-task";
   String PUT_TASK = "put-task";
 
+  String PUT_PORTRAIT = "put-portrait";
+  String DELETE_PORTRAIT = "delete-portrait";
+
   String SELECTOR_INITIALIZE = SELECTOR_NAME + " = '" + INITIALIZE + "'";
 
   String SELECTOR_POST_CUSTOMER = SELECTOR_NAME + " = '" + POST_CUSTOMER + "'";
@@ -55,4 +58,7 @@ public interface CustomerEventConstants {
 
   String SELECTOR_POST_TASK = SELECTOR_NAME + " = '" + POST_TASK + "'";
   String SELECTOR_PUT_TASK = SELECTOR_NAME + " = '" + PUT_TASK + "'";
+
+  String SELECTOR_PUT_PORTRAIT = SELECTOR_NAME + " = '" + PUT_PORTRAIT + "'";
+  String SELECTOR_DELETE_PORTRAIT = SELECTOR_NAME + " = '" + DELETE_PORTRAIT + "'";
 }
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/CustomerManager.java b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerManager.java
index 47d04fe..98f46ef 100644
--- a/api/src/main/java/io/mifos/customer/api/v1/client/CustomerManager.java
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/CustomerManager.java
@@ -18,6 +18,7 @@ package io.mifos.customer.api.v1.client;
 import io.mifos.core.api.annotation.ThrowsException;
 import io.mifos.core.api.annotation.ThrowsExceptions;
 import io.mifos.core.api.util.CustomFeignClientsConfiguration;
+import io.mifos.customer.api.v1.config.CustomerFeignClientConfig;
 import io.mifos.customer.api.v1.domain.Address;
 import io.mifos.customer.api.v1.domain.Command;
 import io.mifos.customer.api.v1.domain.ContactDetail;
@@ -33,11 +34,12 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 
 @SuppressWarnings("unused")
-@FeignClient(name="customer-v1", path="/customer/v1", configuration=CustomFeignClientsConfiguration.class)
+@FeignClient(name="customer-v1", path="/customer/v1", configuration= CustomerFeignClientConfig.class)
 public interface CustomerManager {
 
   @RequestMapping(
@@ -183,6 +185,37 @@ public interface CustomerManager {
                              @RequestBody final IdentificationCard identificationCard);
 
   @RequestMapping(
+          value = "/customers/{identifier}/portrait",
+          method = RequestMethod.GET,
+          produces = MediaType.ALL_VALUE
+  )
+  @ThrowsExceptions({
+          @ThrowsException(status = HttpStatus.NOT_FOUND, exception = PortraitNotFoundException.class),
+  })
+  byte[] getPortrait(@PathVariable("identifier") final String identifier);
+
+  @RequestMapping(
+          value = "/customers/{identifier}/portrait",
+          method = RequestMethod.PUT,
+          produces = MediaType.ALL_VALUE,
+          consumes = MediaType.MULTIPART_FORM_DATA_VALUE
+  )
+  @ThrowsExceptions({
+          @ThrowsException(status = HttpStatus.NOT_FOUND, exception = CustomerNotFoundException.class),
+          @ThrowsException(status = HttpStatus.BAD_REQUEST, exception = PortraitValidationException.class),
+  })
+  void putPortrait(@PathVariable("identifier") final String identifier,
+                   @RequestBody final MultipartFile portrait);
+
+  @RequestMapping(
+          value = "/customers/{identifier}/portrait",
+          method = RequestMethod.DELETE,
+          produces = MediaType.ALL_VALUE,
+          consumes = MediaType.APPLICATION_JSON_VALUE
+  )
+  void deletePortrait(@PathVariable("identifier") final String identifier);
+
+  @RequestMapping(
       value = "/tasks",
       method = RequestMethod.POST,
       produces = MediaType.APPLICATION_JSON_VALUE,
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/PortraitNotFoundException.java b/api/src/main/java/io/mifos/customer/api/v1/client/PortraitNotFoundException.java
new file mode 100644
index 0000000..d5cb2b9
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/PortraitNotFoundException.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2016 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.customer.api.v1.client;
+
+public class PortraitNotFoundException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/client/PortraitValidationException.java b/api/src/main/java/io/mifos/customer/api/v1/client/PortraitValidationException.java
new file mode 100644
index 0000000..000b8d9
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/client/PortraitValidationException.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2016 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.customer.api.v1.client;
+
+public class PortraitValidationException extends RuntimeException {
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/config/CustomerFeignClientConfig.java b/api/src/main/java/io/mifos/customer/api/v1/config/CustomerFeignClientConfig.java
new file mode 100644
index 0000000..37fa00f
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/config/CustomerFeignClientConfig.java
@@ -0,0 +1,93 @@
+package io.mifos.customer.api.v1.config;/*
+ * Copyright 2016 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.
+ */
+
+import feign.Feign;
+import feign.Target;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+import feign.form.spring.SpringFormEncoder;
+import feign.gson.GsonDecoder;
+import feign.gson.GsonEncoder;
+import io.mifos.core.api.util.AnnotatedErrorDecoder;
+import io.mifos.core.api.util.TenantedTargetInterceptor;
+import io.mifos.core.api.util.TokenedTargetInterceptor;
+import io.mifos.customer.api.v1.config.encoder.CustomDecoder;
+import io.mifos.customer.api.v1.config.encoder.CustomEncoder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.cloud.netflix.feign.FeignClientsConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.context.annotation.Scope;
+
+public class CustomerFeignClientConfig extends FeignClientsConfiguration {
+
+    @Bean
+    @ConditionalOnMissingBean
+    public TenantedTargetInterceptor tenantedTargetInterceptor() {
+        return new TenantedTargetInterceptor();
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public TokenedTargetInterceptor tokenedTargetInterceptor() {
+        return new TokenedTargetInterceptor();
+    }
+
+    @Bean(
+            name = {"api-logger"}
+    )
+    public Logger logger() {
+        return LoggerFactory.getLogger("api-logger");
+    }
+
+    @Bean
+    @Scope("prototype")
+    @ConditionalOnMissingBean
+    public Feign.Builder feignBuilder(@Qualifier("api-logger") Logger logger) {
+        return new CustomerFeignClientConfig.AnnotatedErrorDecoderFeignBuilder(logger);
+    }
+
+    private static class AnnotatedErrorDecoderFeignBuilder extends Feign.Builder {
+        private final Logger logger;
+
+        AnnotatedErrorDecoderFeignBuilder(Logger logger) {
+            this.logger = logger;
+        }
+
+        public <T> T target(Target<T> target) {
+            this.errorDecoder(new AnnotatedErrorDecoder(this.logger, target.type()));
+            return this.build().newInstance(target);
+        }
+    }
+
+    @Bean
+    @Primary
+    @Scope("prototype")
+    public Encoder feignEncoder() {
+      return new CustomEncoder(new GsonEncoder(), new SpringFormEncoder());
+    }
+
+    @Bean
+    @Primary
+    @Scope("prototype")
+    public Decoder feignDecoder() {
+        return new CustomDecoder(new GsonDecoder());
+    }
+
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/config/encoder/CustomDecoder.java b/api/src/main/java/io/mifos/customer/api/v1/config/encoder/CustomDecoder.java
new file mode 100644
index 0000000..afce39d
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/config/encoder/CustomDecoder.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 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.customer.api.v1.config.encoder;
+
+import feign.FeignException;
+import feign.Response;
+import feign.codec.Decoder;
+import feign.gson.GsonDecoder;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+@Component
+public class CustomDecoder implements Decoder {
+
+  private final Decoder defaultDecoder;
+  private final GsonDecoder gsonDecoder;
+
+  public CustomDecoder(final GsonDecoder gsonDecoder) {
+    this.gsonDecoder = gsonDecoder;
+    this.defaultDecoder = new Decoder.Default();
+  }
+
+  @Override
+  public Object decode(Response response, Type type) throws IOException, FeignException {
+    if (byte[].class.equals(type)) {
+      return this.defaultDecoder.decode(response, type);
+    }
+
+    return this.gsonDecoder.decode(response, type);
+  }
+
+}
diff --git a/api/src/main/java/io/mifos/customer/api/v1/config/encoder/CustomEncoder.java b/api/src/main/java/io/mifos/customer/api/v1/config/encoder/CustomEncoder.java
new file mode 100644
index 0000000..6e88d62
--- /dev/null
+++ b/api/src/main/java/io/mifos/customer/api/v1/config/encoder/CustomEncoder.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016 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.customer.api.v1.config.encoder;
+
+import feign.RequestTemplate;
+import feign.codec.EncodeException;
+import feign.codec.Encoder;
+import feign.form.spring.SpringFormEncoder;
+import feign.gson.GsonEncoder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.lang.reflect.Type;
+
+@Component
+public class CustomEncoder implements Encoder {
+
+  private final Encoder defaultEncoder;
+  private final GsonEncoder gsonEncoder;
+  private final SpringFormEncoder springFormEncoder;
+
+  public CustomEncoder(final GsonEncoder gsonEncoder, final SpringFormEncoder springFormEncoder) {
+    this.gsonEncoder = gsonEncoder;
+    this.springFormEncoder = springFormEncoder;
+    this.defaultEncoder = new Encoder.Default();
+  }
+
+  @Override
+  public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
+    if (bodyType.equals(MultipartFile.class)) {
+      this.springFormEncoder.encode(object, bodyType, template);
+    }else {
+      this.gsonEncoder.encode(object, bodyType, template);
+    }
+  }
+}
diff --git a/component-test/src/main/java/io/mifos/customer/TestCustomer.java b/component-test/src/main/java/io/mifos/customer/TestCustomer.java
index 7ce969a..4973bf3 100644
--- a/component-test/src/main/java/io/mifos/customer/TestCustomer.java
+++ b/component-test/src/main/java/io/mifos/customer/TestCustomer.java
@@ -24,22 +24,10 @@ import io.mifos.core.test.fixture.mariadb.MariaDBInitializer;
 import io.mifos.core.test.listener.EnableEventRecording;
 import io.mifos.core.test.listener.EventRecorder;
 import io.mifos.customer.api.v1.CustomerEventConstants;
-import io.mifos.customer.api.v1.client.CustomerAlreadyExistsException;
-import io.mifos.customer.api.v1.client.CustomerManager;
-import io.mifos.customer.api.v1.client.CustomerNotFoundException;
-import io.mifos.customer.api.v1.client.CustomerValidationException;
-import io.mifos.customer.api.v1.domain.Address;
-import io.mifos.customer.api.v1.domain.Command;
-import io.mifos.customer.api.v1.domain.ContactDetail;
-import io.mifos.customer.api.v1.domain.Customer;
-import io.mifos.customer.api.v1.domain.CustomerPage;
-import io.mifos.customer.api.v1.domain.IdentificationCard;
+import io.mifos.customer.api.v1.client.*;
+import io.mifos.customer.api.v1.domain.*;
 import io.mifos.customer.service.rest.config.CustomerRestConfiguration;
-import io.mifos.customer.util.AddressGenerator;
-import io.mifos.customer.util.CommandGenerator;
-import io.mifos.customer.util.ContactDetailGenerator;
-import io.mifos.customer.util.CustomerGenerator;
-import io.mifos.customer.util.IdentificationCardGenerator;
+import io.mifos.customer.util.*;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.junit.*;
 import org.junit.rules.RuleChain;
@@ -52,6 +40,8 @@ import org.springframework.cloud.netflix.ribbon.RibbonClient;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockMultipartFile;
 import org.springframework.test.context.junit4.SpringRunner;
 
 import java.util.Collections;
@@ -416,4 +406,85 @@ public class TestCustomer {
     Assert.assertEquals(newIdentificationCard.getIssuer(), changedIdentificationCard.getIssuer());
     Assert.assertEquals(newIdentificationCard.getNumber(), changedIdentificationCard.getNumber());
   }
+
+  @Test
+  public void shouldUploadPortrait() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+    this.customerManager.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    this.customerManager.findCustomer(customer.getIdentifier());
+
+    final MockMultipartFile file = new MockMultipartFile("portrait", "test.png", MediaType.IMAGE_PNG_VALUE, "i don't care".getBytes());
+
+    this.customerManager.putPortrait(customer.getIdentifier(), file);
+
+    this.eventRecorder.wait(CustomerEventConstants.PUT_PORTRAIT, customer.getIdentifier());
+
+    byte[] portrait = this.customerManager.getPortrait(customer.getIdentifier());
+
+    Assert.assertArrayEquals(file.getBytes(), portrait);
+  }
+
+  @Test
+  public void shouldReplacePortrait() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+
+    this.customerManager.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    final MockMultipartFile firstFile = new MockMultipartFile("portrait", "test.png", MediaType.IMAGE_PNG_VALUE, "i don't care".getBytes());
+
+    this.customerManager.putPortrait(customer.getIdentifier(), firstFile);
+
+    this.eventRecorder.wait(CustomerEventConstants.PUT_PORTRAIT, customer.getIdentifier());
+
+    this.eventRecorder.clear();
+
+    final MockMultipartFile secondFile = new MockMultipartFile("portrait", "test.png", MediaType.IMAGE_PNG_VALUE, "i do care".getBytes());
+
+    this.customerManager.putPortrait(customer.getIdentifier(), secondFile);
+
+    this.eventRecorder.wait(CustomerEventConstants.PUT_PORTRAIT, customer.getIdentifier());
+
+    final byte[] portrait = this.customerManager.getPortrait(customer.getIdentifier());
+
+    Assert.assertArrayEquals(secondFile.getBytes(), portrait);
+  }
+
+  @Test(expected = PortraitValidationException.class)
+  public void shouldThrowIfPortraitExceedsMaxSize() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+
+    this.customerManager.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    final MockMultipartFile firstFile = new MockMultipartFile("portrait", "test.png", MediaType.IMAGE_PNG_VALUE, RandomStringUtils.randomAlphanumeric(750000).getBytes());
+
+    this.customerManager.putPortrait(customer.getIdentifier(), firstFile);
+  }
+
+  @Test(expected = PortraitNotFoundException.class)
+  public void shouldDeletePortrait() throws Exception {
+    final Customer customer = CustomerGenerator.createRandomCustomer();
+
+    this.customerManager.createCustomer(customer);
+
+    this.eventRecorder.wait(CustomerEventConstants.POST_CUSTOMER, customer.getIdentifier());
+
+    final MockMultipartFile firstFile = new MockMultipartFile("portrait", "test.png", MediaType.IMAGE_PNG_VALUE, "i don't care".getBytes());
+
+    this.customerManager.putPortrait(customer.getIdentifier(), firstFile);
+
+    this.eventRecorder.wait(CustomerEventConstants.PUT_PORTRAIT, customer.getIdentifier());
+
+    this.customerManager.deletePortrait(customer.getIdentifier());
+
+    this.eventRecorder.wait(CustomerEventConstants.DELETE_PORTRAIT, customer.getIdentifier());
+
+    this.customerManager.getPortrait(customer.getIdentifier());
+  }
 }
diff --git a/component-test/src/main/java/io/mifos/customer/listener/CustomerEventListener.java b/component-test/src/main/java/io/mifos/customer/listener/CustomerEventListener.java
index 51f711f..3d7af63 100644
--- a/component-test/src/main/java/io/mifos/customer/listener/CustomerEventListener.java
+++ b/component-test/src/main/java/io/mifos/customer/listener/CustomerEventListener.java
@@ -123,4 +123,22 @@ public class CustomerEventListener {
                                              final String payload) {
     this.eventRecorder.event(tenant, CustomerEventConstants.PUT_IDENTIFICATION_CARD, payload, String.class);
   }
+
+  @JmsListener(
+          destination = CustomerEventConstants.DESTINATION,
+          selector = CustomerEventConstants.SELECTOR_PUT_PORTRAIT
+  )
+  public void portraitPutEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                                             final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.PUT_PORTRAIT, payload, String.class);
+  }
+
+  @JmsListener(
+          destination = CustomerEventConstants.DESTINATION,
+          selector = CustomerEventConstants.SELECTOR_DELETE_PORTRAIT
+  )
+  public void portraitDeleteEvent(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+                               final String payload) {
+    this.eventRecorder.event(tenant, CustomerEventConstants.DELETE_PORTRAIT, payload, String.class);
+  }
 }
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/CreatePortraitCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/CreatePortraitCommand.java
new file mode 100644
index 0000000..2173212
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/CreatePortraitCommand.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 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.customer.service.internal.command;
+
+import org.springframework.web.multipart.MultipartFile;
+
+public class CreatePortraitCommand {
+
+  private final String identifier;
+  private final MultipartFile portrait;
+
+  public CreatePortraitCommand(final String identifier, final MultipartFile portrait) {
+    super();
+    this.identifier = identifier;
+    this.portrait = portrait;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+  public MultipartFile portrait() {
+    return this.portrait;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/DeletePortraitCommand.java b/service/src/main/java/io/mifos/customer/service/internal/command/DeletePortraitCommand.java
new file mode 100644
index 0000000..5987dfc
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/DeletePortraitCommand.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 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.customer.service.internal.command;
+
+public class DeletePortraitCommand {
+
+  private final String identifier;
+
+  public DeletePortraitCommand(final String identifier) {
+    super();
+    this.identifier = identifier;
+  }
+
+  public String identifier() {
+    return this.identifier;
+  }
+
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/command/handler/CustomerAggregate.java b/service/src/main/java/io/mifos/customer/service/internal/command/handler/CustomerAggregate.java
index 661f34b..d8e392a 100644
--- a/service/src/main/java/io/mifos/customer/service/internal/command/handler/CustomerAggregate.java
+++ b/service/src/main/java/io/mifos/customer/service/internal/command/handler/CustomerAggregate.java
@@ -29,34 +29,13 @@ import io.mifos.customer.catalog.service.internal.repository.FieldEntity;
 import io.mifos.customer.catalog.service.internal.repository.FieldRepository;
 import io.mifos.customer.catalog.service.internal.repository.FieldValueEntity;
 import io.mifos.customer.catalog.service.internal.repository.FieldValueRepository;
-import io.mifos.customer.service.internal.command.ActivateCustomerCommand;
-import io.mifos.customer.service.internal.command.CloseCustomerCommand;
-import io.mifos.customer.service.internal.command.CreateCustomerCommand;
-import io.mifos.customer.service.internal.command.LockCustomerCommand;
-import io.mifos.customer.service.internal.command.ReopenCustomerCommand;
-import io.mifos.customer.service.internal.command.UnlockCustomerCommand;
-import io.mifos.customer.service.internal.command.UpdateAddressCommand;
-import io.mifos.customer.service.internal.command.UpdateContactDetailsCommand;
-import io.mifos.customer.service.internal.command.UpdateCustomerCommand;
-import io.mifos.customer.service.internal.command.UpdateIdentificationCardCommand;
-import io.mifos.customer.service.internal.mapper.AddressMapper;
-import io.mifos.customer.service.internal.mapper.CommandMapper;
-import io.mifos.customer.service.internal.mapper.ContactDetailMapper;
-import io.mifos.customer.service.internal.mapper.CustomerMapper;
-import io.mifos.customer.service.internal.mapper.FieldValueMapper;
-import io.mifos.customer.service.internal.mapper.IdentificationCardMapper;
-import io.mifos.customer.service.internal.repository.AddressEntity;
-import io.mifos.customer.service.internal.repository.AddressRepository;
-import io.mifos.customer.service.internal.repository.CommandRepository;
-import io.mifos.customer.service.internal.repository.ContactDetailEntity;
-import io.mifos.customer.service.internal.repository.ContactDetailRepository;
-import io.mifos.customer.service.internal.repository.CustomerEntity;
-import io.mifos.customer.service.internal.repository.CustomerRepository;
-import io.mifos.customer.service.internal.repository.IdentificationCardEntity;
-import io.mifos.customer.service.internal.repository.IdentificationCardRepository;
+import io.mifos.customer.service.internal.command.*;
+import io.mifos.customer.service.internal.mapper.*;
+import io.mifos.customer.service.internal.repository.*;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.io.IOException;
 import java.sql.Date;
 import java.time.Clock;
 import java.time.LocalDate;
@@ -72,6 +51,7 @@ public class CustomerAggregate {
   private final AddressRepository addressRepository;
   private final CustomerRepository customerRepository;
   private final IdentificationCardRepository identificationCardRepository;
+  private final PortraitRepository portraitRepository;
   private final ContactDetailRepository contactDetailRepository;
   private final FieldValueRepository fieldValueRepository;
   private final CatalogRepository catalogRepository;
@@ -83,6 +63,7 @@ public class CustomerAggregate {
   public CustomerAggregate(final AddressRepository addressRepository,
                            final CustomerRepository customerRepository,
                            final IdentificationCardRepository identificationCardRepository,
+                           final PortraitRepository portraitRepository,
                            final ContactDetailRepository contactDetailRepository,
                            final FieldValueRepository fieldValueRepository,
                            final CatalogRepository catalogRepository,
@@ -93,6 +74,7 @@ public class CustomerAggregate {
     this.addressRepository = addressRepository;
     this.customerRepository = customerRepository;
     this.identificationCardRepository = identificationCardRepository;
+    this.portraitRepository = portraitRepository;
     this.contactDetailRepository = contactDetailRepository;
     this.fieldValueRepository = fieldValueRepository;
     this.catalogRepository = catalogRepository;
@@ -364,6 +346,44 @@ public class CustomerAggregate {
     return updateIdentificationCardCommand.identifier();
   }
 
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.PUT_PORTRAIT)
+  public String createPortrait(final CreatePortraitCommand createPortraitCommand) throws IOException {
+    if(createPortraitCommand.portrait() == null) {
+      return null;
+    }
+
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(createPortraitCommand.identifier());
+
+    final PortraitEntity portraitEntity = PortraitMapper.map(createPortraitCommand.portrait());
+    portraitEntity.setCustomer(customerEntity);
+    this.portraitRepository.save(portraitEntity);
+
+    customerEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+    customerEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+
+    this.customerRepository.save(customerEntity);
+
+    return createPortraitCommand.identifier();
+  }
+
+  @Transactional
+  @CommandHandler
+  @EventEmitter(selectorName = CustomerEventConstants.SELECTOR_NAME, selectorValue = CustomerEventConstants.DELETE_PORTRAIT)
+  public String deletePortrait(final DeletePortraitCommand deletePortraitCommand) throws IOException {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(deletePortraitCommand.identifier());
+
+    this.portraitRepository.deleteByCustomer(customerEntity);
+
+    customerEntity.setLastModifiedBy(UserContextHolder.checkedGetUser());
+    customerEntity.setLastModifiedOn(LocalDateTime.now(Clock.systemUTC()));
+
+    this.customerRepository.save(customerEntity);
+
+    return deletePortraitCommand.identifier();
+  }
+
   private void setCustomValues(final Customer customer, final CustomerEntity savedCustomerEntity) {
     this.fieldValueRepository.save(
         customer.getCustomValues()
diff --git a/service/src/main/java/io/mifos/customer/service/internal/mapper/PortraitMapper.java b/service/src/main/java/io/mifos/customer/service/internal/mapper/PortraitMapper.java
new file mode 100644
index 0000000..f9b66aa
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/mapper/PortraitMapper.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 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.customer.service.internal.mapper;
+
+import io.mifos.customer.service.internal.repository.PortraitEntity;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+
+public class PortraitMapper {
+
+  private PortraitMapper() {
+    super();
+  }
+
+  public static PortraitEntity map(final MultipartFile multipartFile) throws IOException {
+    final PortraitEntity portraitEntity = new PortraitEntity();
+    portraitEntity.setImage(multipartFile.getBytes());
+    portraitEntity.setSize(multipartFile.getSize());
+    portraitEntity.setContentType(multipartFile.getContentType());
+    return portraitEntity;
+  }
+
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/PortraitEntity.java b/service/src/main/java/io/mifos/customer/service/internal/repository/PortraitEntity.java
new file mode 100644
index 0000000..e77b86f
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/PortraitEntity.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2016 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.customer.service.internal.repository;
+
+import javax.persistence.*;
+
+@Entity
+@Table(name = "maat_portraits")
+public class PortraitEntity {
+
+  @Id
+  @GeneratedValue(strategy = GenerationType.IDENTITY)
+  @Column(name = "id")
+  private Long id;
+
+  @OneToOne(fetch = FetchType.LAZY, optional = false)
+  @JoinColumn(name = "customer_id")
+  private CustomerEntity customer;
+
+  @Lob
+  @Column(name = "image")
+  private byte[] image;
+
+  @Column(name = "size")
+  private Long size;
+
+  @Column(name = "content_type")
+  private String contentType;
+
+  public PortraitEntity() {
+    super();
+  }
+
+  public Long getId() {
+    return this.id;
+  }
+
+  public void setId(final Long id) {
+    this.id = id;
+  }
+
+  public CustomerEntity getCustomer() {
+    return this.customer;
+  }
+
+  public void setCustomer(final CustomerEntity customer) {
+    this.customer = customer;
+  }
+
+  public byte[] getImage() {
+    return image;
+  }
+
+  public void setImage(byte[] image) {
+    this.image = image;
+  }
+
+  public Long getSize() {
+    return size;
+  }
+
+  public void setSize(Long size) {
+    this.size = size;
+  }
+
+  public String getContentType() {
+    return contentType;
+  }
+
+  public void setContentType(String contentType) {
+    this.contentType = contentType;
+  }
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/repository/PortraitRepository.java b/service/src/main/java/io/mifos/customer/service/internal/repository/PortraitRepository.java
new file mode 100644
index 0000000..20cc1de
--- /dev/null
+++ b/service/src/main/java/io/mifos/customer/service/internal/repository/PortraitRepository.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016 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.customer.service.internal.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface PortraitRepository extends JpaRepository<PortraitEntity, Long> {
+
+  @Query("SELECT CASE WHEN COUNT(c) > 0 THEN 'true' ELSE 'false' END FROM PortraitEntity c WHERE c.customer.identifier = :identifier")
+  Boolean existsByIdentifier(@Param("identifier") final String identifier);
+
+  PortraitEntity findByCustomer(final CustomerEntity customerEntity);
+
+  void deleteByCustomer(final CustomerEntity customerEntity);
+}
diff --git a/service/src/main/java/io/mifos/customer/service/internal/service/CustomerService.java b/service/src/main/java/io/mifos/customer/service/internal/service/CustomerService.java
index d383e04..d9dc196 100644
--- a/service/src/main/java/io/mifos/customer/service/internal/service/CustomerService.java
+++ b/service/src/main/java/io/mifos/customer/service/internal/service/CustomerService.java
@@ -27,14 +27,7 @@ import io.mifos.customer.service.internal.mapper.CommandMapper;
 import io.mifos.customer.service.internal.mapper.ContactDetailMapper;
 import io.mifos.customer.service.internal.mapper.CustomerMapper;
 import io.mifos.customer.service.internal.mapper.IdentificationCardMapper;
-import io.mifos.customer.service.internal.repository.CommandEntity;
-import io.mifos.customer.service.internal.repository.CommandRepository;
-import io.mifos.customer.service.internal.repository.ContactDetailEntity;
-import io.mifos.customer.service.internal.repository.ContactDetailRepository;
-import io.mifos.customer.service.internal.repository.CustomerEntity;
-import io.mifos.customer.service.internal.repository.CustomerRepository;
-import io.mifos.customer.service.internal.repository.IdentificationCardEntity;
-import io.mifos.customer.service.internal.repository.IdentificationCardRepository;
+import io.mifos.customer.service.internal.repository.*;
 import io.mifos.customer.service.internal.mapper.AddressMapper;
 import org.slf4j.Logger;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -55,6 +48,7 @@ public class CustomerService {
   private final Logger logger;
   private final CustomerRepository customerRepository;
   private final IdentificationCardRepository identificationCardRepository;
+  private final PortraitRepository portraitRepository;
   private final ContactDetailRepository contactDetailRepository;
   private final FieldValueRepository fieldValueRepository;
   private final CommandRepository commandRepository;
@@ -63,6 +57,7 @@ public class CustomerService {
   public CustomerService(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
                          final CustomerRepository customerRepository,
                          final IdentificationCardRepository identificationCardRepository,
+                         final PortraitRepository portraitRepository,
                          final ContactDetailRepository contactDetailRepository,
                          final FieldValueRepository fieldValueRepository,
                          final CommandRepository commandRepository) {
@@ -70,6 +65,7 @@ public class CustomerService {
     this.logger = logger;
     this.customerRepository = customerRepository;
     this.identificationCardRepository = identificationCardRepository;
+    this.portraitRepository = portraitRepository;
     this.contactDetailRepository = contactDetailRepository;
     this.fieldValueRepository = fieldValueRepository;
     this.commandRepository = commandRepository;
@@ -79,6 +75,10 @@ public class CustomerService {
     return this.customerRepository.existsByIdentifier(identifier);
   }
 
+  public Boolean portraitExists(final String identifier) {
+    return this.portraitRepository.existsByIdentifier(identifier);
+  }
+
   public Optional<Customer> findCustomer(final String identifier) {
     final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(identifier);
     if (customerEntity != null) {
@@ -159,4 +159,10 @@ public class CustomerService {
       return Collections.emptyList();
     }
   }
+
+  public final PortraitEntity findPortrait(final String identifier) {
+    final CustomerEntity customerEntity = this.customerRepository.findByIdentifier(identifier);
+
+    return this.portraitRepository.findByCustomer(customerEntity);
+  }
 }
diff --git a/service/src/main/java/io/mifos/customer/service/rest/controller/CustomerRestController.java b/service/src/main/java/io/mifos/customer/service/rest/controller/CustomerRestController.java
index 0062f22..b5c39f4 100644
--- a/service/src/main/java/io/mifos/customer/service/rest/controller/CustomerRestController.java
+++ b/service/src/main/java/io/mifos/customer/service/rest/controller/CustomerRestController.java
@@ -21,47 +21,24 @@ import io.mifos.core.api.util.UserContextHolder;
 import io.mifos.core.command.gateway.CommandGateway;
 import io.mifos.core.lang.ServiceException;
 import io.mifos.customer.PermittableGroupIds;
-import io.mifos.customer.api.v1.domain.Address;
-import io.mifos.customer.api.v1.domain.Command;
-import io.mifos.customer.api.v1.domain.ContactDetail;
-import io.mifos.customer.api.v1.domain.Customer;
-import io.mifos.customer.api.v1.domain.CustomerPage;
-import io.mifos.customer.api.v1.domain.IdentificationCard;
-import io.mifos.customer.api.v1.domain.TaskDefinition;
+import io.mifos.customer.api.v1.domain.*;
 import io.mifos.customer.catalog.service.internal.service.FieldValueValidator;
-import io.mifos.customer.service.internal.command.CreateCustomerCommand;
-import io.mifos.customer.service.internal.command.ExecuteTaskForCustomerCommand;
-import io.mifos.customer.service.internal.command.InitializeServiceCommand;
-import io.mifos.customer.service.internal.command.LockCustomerCommand;
-import io.mifos.customer.service.internal.command.UnlockCustomerCommand;
-import io.mifos.customer.service.internal.command.UpdateAddressCommand;
-import io.mifos.customer.service.internal.command.UpdateCustomerCommand;
 import io.mifos.customer.service.ServiceConstants;
-import io.mifos.customer.service.internal.command.ActivateCustomerCommand;
-import io.mifos.customer.service.internal.command.AddTaskDefinitionToCustomerCommand;
-import io.mifos.customer.service.internal.command.CloseCustomerCommand;
-import io.mifos.customer.service.internal.command.CreateTaskDefinitionCommand;
-import io.mifos.customer.service.internal.command.ReopenCustomerCommand;
-import io.mifos.customer.service.internal.command.UpdateContactDetailsCommand;
-import io.mifos.customer.service.internal.command.UpdateIdentificationCardCommand;
-import io.mifos.customer.service.internal.command.UpdateTaskDefinitionCommand;
+import io.mifos.customer.service.internal.command.*;
+import io.mifos.customer.service.internal.repository.PortraitEntity;
 import io.mifos.customer.service.internal.service.CustomerService;
 import io.mifos.customer.service.internal.service.TaskService;
 import org.slf4j.Logger;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.env.Environment;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.ResponseBody;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.validation.Valid;
 import java.util.List;
@@ -76,19 +53,22 @@ public class CustomerRestController {
   private final CustomerService customerService;
   private final FieldValueValidator fieldValueValidator;
   private final TaskService taskService;
+  private final Environment environment;
 
   @Autowired
   public CustomerRestController(@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger,
                                 final CommandGateway commandGateway,
                                 final CustomerService customerService,
                                 final FieldValueValidator fieldValueValidator,
-                                final TaskService taskService) {
+                                final TaskService taskService,
+                                final Environment environment) {
     super();
     this.logger = logger;
     this.commandGateway = commandGateway;
     this.customerService = customerService;
     this.fieldValueValidator = fieldValueValidator;
     this.taskService = taskService;
+    this.environment = environment;
   }
 
   @Permittable(value = AcceptedTokenType.SYSTEM)
@@ -412,6 +392,74 @@ public class CustomerRestController {
     return ResponseEntity.accepted().build();
   }
 
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CUSTOMER)
+  @RequestMapping(
+      value = "/customers/{identifier}/portrait",
+      method = RequestMethod.GET,
+      consumes = MediaType.ALL_VALUE
+  )
+  public ResponseEntity<byte[]> getPortrait(@PathVariable("identifier") final String identifier) {
+    this.throwIfPortraitNotExists(identifier);
+
+    final PortraitEntity portrait = this.customerService.findPortrait(identifier);
+
+    return ResponseEntity
+            .ok()
+            .contentType(MediaType.parseMediaType(portrait.getContentType()))
+            .contentLength(portrait.getImage().length)
+            .body(portrait.getImage());
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CUSTOMER)
+  @RequestMapping(
+      value = "/customers/{identifier}/portrait",
+      method = RequestMethod.PUT,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.MULTIPART_FORM_DATA_VALUE
+  )
+  public @ResponseBody ResponseEntity<Void> putPortrait(@PathVariable("identifier") final String identifier,
+                                          @RequestBody final MultipartFile portrait) {
+    if(portrait == null) {
+      throw ServiceException.badRequest("Portrait not found");
+    }
+
+    this.throwIfCustomerNotExists(identifier);
+
+    final Long maxSize = this.environment.getProperty("upload.image.max-size", Long.class);
+
+    if(portrait.getSize() > maxSize) {
+      throw ServiceException.badRequest("Portrait can't exceed size of {0}", maxSize);
+    }
+
+    if(!portrait.getContentType().contains(MediaType.IMAGE_JPEG_VALUE)
+            && !portrait.getContentType().contains(MediaType.IMAGE_PNG_VALUE)) {
+      throw ServiceException.badRequest("Only content type {0} and {1} allowed", MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE);
+    }
+
+    try {
+      this.commandGateway.process(new DeletePortraitCommand(identifier), String.class).get();
+    } catch (Throwable e) {
+      logger.warn("Could not delete portrait: {0}", e.getMessage());
+    }
+
+    this.commandGateway.process(new CreatePortraitCommand(identifier, portrait));
+
+    return ResponseEntity.accepted().build();
+  }
+
+  @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CUSTOMER)
+  @RequestMapping(
+      value = "/customers/{identifier}/portrait",
+      method = RequestMethod.DELETE,
+      produces = MediaType.APPLICATION_JSON_VALUE,
+      consumes = MediaType.ALL_VALUE
+  )
+  public @ResponseBody ResponseEntity<Void> deletePortrait(@PathVariable("identifier") final String identifier) {
+    this.commandGateway.process(new DeletePortraitCommand(identifier));
+
+    return ResponseEntity.accepted().build();
+  }
+
   @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.TASK)
   @RequestMapping(
       value = "/tasks",
@@ -486,4 +534,17 @@ public class CustomerRestController {
     final Sort.Direction direction = sortDirection != null ? Sort.Direction.valueOf(sortDirection.toUpperCase()) : Sort.Direction.ASC;
     return new PageRequest(pageIndexToUse, sizeToUse, direction, sortColumnToUse);
   }
+
+  private void throwIfCustomerNotExists(final String identifier) {
+    if (!this.customerService.customerExists(identifier)) {
+      throw ServiceException.notFound("Customer {0} not found.", identifier);
+    }
+  }
+
+  private void throwIfPortraitNotExists(final String identifier) {
+    if (!this.customerService.portraitExists(identifier)) {
+      throw ServiceException.notFound("Portrait for Customer {0} not found.", identifier);
+    }
+  }
+
 }
diff --git a/service/src/main/resources/application.yml b/service/src/main/resources/application.yml
index 99db241..24a4e94 100644
--- a/service/src/main/resources/application.yml
+++ b/service/src/main/resources/application.yml
@@ -64,3 +64,7 @@ async:
 
 flyway:
   enabled: false
+
+upload:
+  image:
+    max-size: 524288
diff --git a/service/src/main/resources/db/migrations/mariadb/V2__customer_portrait.sql b/service/src/main/resources/db/migrations/mariadb/V2__customer_portrait.sql
new file mode 100644
index 0000000..d7c3386
--- /dev/null
+++ b/service/src/main/resources/db/migrations/mariadb/V2__customer_portrait.sql
@@ -0,0 +1,31 @@
+--
+-- 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.
+--
+-- CONSTRAINT maat_id_portraits_customers_fk FOREIGN KEY (customer_id) REFERENCES maat_customers (id) ON UPDATE RESTRICT
+
+
+CREATE TABLE maat_portraits (
+  id BIGINT NOT NULL AUTO_INCREMENT,
+  customer_id BIGINT NOT NULL,
+  content_type VARCHAR(256) NOT NULL,
+  size BIGINT NOT NULL,
+  image BLOB NOT NULL,
+  CONSTRAINT maat_portraits_pk PRIMARY KEY (id),
+  CONSTRAINT maat_id_portraits_customers_fk FOREIGN KEY (customer_id) REFERENCES maat_customers (id) ON UPDATE RESTRICT
+);
+
+-- ALTER TABLE maat_customers ADD COLUMN portrait_id BIGINT NULL;
+
+-- ALTER TABLE maat_customers ADD CONSTRAINT maat_customers_portraits_fk FOREIGN KEY (portrait_id) REFERENCES maat_portraits (id);

-- 
To stop receiving notification emails like this one, please contact
myrle@apache.org.

Mime
View raw message