Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image feature UI, updates to backend image upload implementations, some validations and tests #155

Open
wants to merge 12 commits into
base: img-feature
Choose a base branch
from
Open
20 changes: 20 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,26 @@
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
8 changes: 8 additions & 0 deletions backend/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ mvn clean package
cd target
java -jar backend.jar
```
## Context of the Backend
The context of the backend is `/api`. This is set at `server.servlet.context-path` in
the application.yaml. Since we serve the backend at `/api`, every controller context
path is appended after `/api`. eg: `https://host/api/notification`.
The admin portal ui to the backend is served at `/` as a seperate web app.
Similarly, backend is another webapp served at `/api` in the same tomcat.

## How the Portal Webapp is Served in the Tomcat Server
- pom.xml in `portal/` will build the vue app and will create a war, by including everything inside `dist`
Expand All @@ -19,4 +25,6 @@ java -jar backend.jar
- if none of them exists, backend will start without the webapp
- For more info: `lk.gov.govtech.covid19.config.WebappConfiguration`
- Content in the war will appear at localhost:8000/



Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package lk.gov.govtech.covid19.controller;

import lk.gov.govtech.covid19.dto.AlertNotificationResponse;
import lk.gov.govtech.covid19.dto.CaseNotificationResponse;
import lk.gov.govtech.covid19.dto.StatusResponse;
import lk.gov.govtech.covid19.dto.UpdateStatusRequest;
import lk.gov.govtech.covid19.dto.*;
import lk.gov.govtech.covid19.service.ApplicationService;
import lk.gov.govtech.covid19.util.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
* Controller class for all task force application related apis
*/
Expand Down Expand Up @@ -81,16 +82,9 @@ public ResponseEntity getSatus() {

//Update Covid-19 Status
@PutMapping(path = "/dashboard/status", consumes = "application/json", produces = "application/json")
public ResponseEntity updateStatus(@RequestBody UpdateStatusRequest request){

if(request==null){
log.error("Empty request found");
return ResponseEntity.noContent().build();
}else {
log.info("Dashboard status updated");
applicationService.updateStatus(request);
return ResponseEntity.accepted().build();
}

public ResponseEntity updateStatus(@RequestBody @Valid UpdateStatusRequest request){
log.info("Dashboard status updated");
applicationService.updateStatus(request);
return ResponseEntity.accepted().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,38 @@

import lk.gov.govtech.covid19.dto.StoredImage;
import lk.gov.govtech.covid19.dto.StoredImageResponse;
import lk.gov.govtech.covid19.exceptions.ImageHandlingException;
import lk.gov.govtech.covid19.service.ImageService;
import lk.gov.govtech.covid19.util.Constants;
import lk.gov.govtech.covid19.validation.AcceptableImage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@Slf4j
@RequestMapping(Constants.IMAGE_API_CONTEXT)
public class ImageController {

@Autowired
ImageService imageService;
private MultipartFile image;

@PostMapping(path = "/add")
public ResponseEntity uploadImage(@RequestParam("image") MultipartFile image){
StoredImageResponse response = null;
if (image.isEmpty()){
System.out.println("empty file");
}else {
response = imageService.addImage(image);
}
@PostMapping
public ResponseEntity uploadImage(@RequestParam("image") @AcceptableImage MultipartFile image)
throws ImageHandlingException {
StoredImageResponse response = imageService.addImage(image);
log.info("Image {} of size:{} added", image.getOriginalFilename(), image.getSize());
System.gc();
return ResponseEntity.ok().body(response);
return ResponseEntity.accepted().body(response);
}

@GetMapping(value = "/image/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
public @ResponseBody byte[] getFiles(@PathVariable("imageId") int imageId){
StoredImage si = imageService.getImage(imageId);
return si.getImage();
StoredImage storedImage = imageService.getImage(imageId);
return storedImage.getImage();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@Slf4j
@RestController
@RequestMapping(value = Constants.NOTIFICATION_API_CONTEXT)
Expand All @@ -18,15 +20,15 @@ public class NotificationController {
NotificationService notificationService;

@PostMapping(path = "/alert/add", consumes = "application/json", produces = "application/json")
public ResponseEntity addNewAlert(@RequestBody AlertNotificationRequest request){
public ResponseEntity addNewAlert(@RequestBody @Valid AlertNotificationRequest request){
log.info("New alert added with title {}", request.getTitle().getEnglish());
notificationService.addAlertNotificaiton(request);

return ResponseEntity.accepted().build();
}

@PutMapping(path = "/alert/{alertId}", consumes = "application/json")
public ResponseEntity addNewAlert(@PathVariable("alertId") String alertId, @RequestBody AlertNotificationRequest request){
public ResponseEntity addNewAlert(@PathVariable("alertId") String alertId, @RequestBody @Valid AlertNotificationRequest request){
boolean success = notificationService.updateAlertNotification(alertId, request);
if (success) {
log.info("Update alert with id:{} title:{}", alertId, request.getTitle().getEnglish());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,35 @@

import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.*;

@Data
public class AlertNotificationRequest {
@NotBlank @Size(max=45)
private String source;
@NotNull @Valid
private Title title;
@NotNull @Valid
private Message message;

@Data
public static class Title {
@NotBlank @Size(max=100)
private String english;
@Size(max=100)
private String sinhala;
@Size(max=100)
private String tamil;
}

@Data
public static class Message {
@NotBlank @Size(min=8, max=2500)
private String english;
@Size(max=2500)
private String sinhala;
@Size(max=2500)
private String tamil;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package lk.gov.govtech.covid19.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class StoredImageResponse {
private int id;
private String url;
private String name;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@

import lombok.Data;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

@Data
public class UpdateStatusRequest {
private int lk_total_case;
private int lk_recovered_case;
private int lk_total_deaths;
private int lk_total_suspect;
@NotNull @Min(0) @Max(22000000)
private Integer lk_total_case;

@NotNull @Min(0) @Max(22000000)
private Integer lk_recovered_case;

@NotNull @Min(0) @Max(22000000)
private Integer lk_total_deaths;

@NotNull @Min(0) @Max(22000000)
private Integer lk_total_suspect;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package lk.gov.govtech.covid19.exceptions;

import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@ControllerAdvice
public class CustomExceptionHandler {

@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Malformed JSON request")
@ExceptionHandler(value = {HttpMessageNotReadableException.class})
public void handleMalformedRequestRelatedExceptions(HttpServletRequest request, Exception e) {
log.warn("Malformed JSON request");
}

@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Invalid request. Value missing or invalid.")
@ExceptionHandler(value = {MethodArgumentNotValidException.class, HttpMessageConversionException.class,
DataIntegrityViolationException.class, IllegalArgumentException.class})
public void handleValidationException(HttpServletRequest request, Exception e) {
log.warn("Invalid request. Value missing or invalid.");
}

@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR, reason = "Error while processing image")
@ExceptionHandler(value = {ImageHandlingException.class})
public void handleValidationException(HttpServletRequest request, ImageHandlingException e) {
log.warn("Error while processing image");
}

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Resource not found")
@ExceptionHandler(value = {IndexOutOfBoundsException.class})
public void handleExceptionsLeadingToNotFound(HttpServletRequest request, IndexOutOfBoundsException e) {
log.warn("Resource not found");
}

@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR, reason = "Unknown error state of server")
@ExceptionHandler(value = { Exception.class })
public void defaultErrorHandler(HttpServletRequest request, Exception e) throws Exception {
log.error("Exception mapping failure. Exception: {}, message: {}",
e.getClass().getName(), e.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package lk.gov.govtech.covid19.exceptions;

public class ImageHandlingException extends Exception {
public ImageHandlingException(String message) {
super(message);
}

public ImageHandlingException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ public class StoredImageMapper implements RowMapper<StoredImage> {
@Override
public StoredImage mapRow(ResultSet resultSet, int i) throws SQLException {
StoredImage si = new StoredImage();
System.out.println(resultSet.toString());
si.setName(resultSet.getString("name"));
Blob blob = resultSet.getBlob("image");
si.setImage(blob.getBytes(1, (int) blob.length()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
import lk.gov.govtech.covid19.model.StatusEntity;
import lk.gov.govtech.covid19.model.mapper.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.PreparedStatement;
Expand Down Expand Up @@ -145,8 +147,9 @@ public void updateStatus(UpdateStatusRequest request) {
request.getLk_total_case() , request.getLk_recovered_case(), request.getLk_total_deaths(), request.getLk_total_suspect(), 1);
}

public int addImage(InputStream is, String name, long size) throws IOException {
public int addImage(byte[] bArray, String name, long size) {
KeyHolder holder = new GeneratedKeyHolder();
InputStream is = new ByteArrayInputStream(bArray);
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement("insert into images(name, image) " + "values(?,?)",
Statement.RETURN_GENERATED_KEYS);
Expand All @@ -155,6 +158,7 @@ public int addImage(InputStream is, String name, long size) throws IOException {
return ps;
},holder
);
IOUtils.closeQuietly(is);
return holder.getKey().intValue();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class SecurityConfiguration {
/*
* Endpoints with auth (either http basic auth or login based can be used)
* - /notification
* - /application PUT
* - /images POST
*
* */

Expand Down Expand Up @@ -102,6 +104,10 @@ protected void configure(final HttpSecurity http) throws Exception {
.hasAuthority(AUTHORITY_NOTIFICATION)
.antMatchers(HttpMethod.PUT, APPLICATION_API_CONTEXT + "/dashboard/status")
.hasAuthority(AUTHORITY_NOTIFICATION)
.antMatchers(HttpMethod.POST, IMAGE_API_CONTEXT)
.hasAuthority(AUTHORITY_NOTIFICATION)
.antMatchers(DHIS_API_CONTEXT + "/**")
.hasAuthority(AUTHORITY_NOTIFICATION)
.and()
.addFilter(getPasswordFilter())
.requestCache() // avoid saving anonymous requests in sessions
Expand Down
Loading