Skip to content

Commit

Permalink
VUU-81 layout definition as ObjectNode
Browse files Browse the repository at this point in the history
  • Loading branch information
vferraro-scottlogic committed Nov 15, 2023
1 parent 6a24965 commit c988134
Show file tree
Hide file tree
Showing 15 changed files with 1,380 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.finos.vuu.layoutserver.controller;

import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto;
import org.finos.vuu.layoutserver.service.ApplicationLayoutService;
import org.modelmapper.ModelMapper;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@RestController
@RequestMapping("/application-layouts")
public class ApplicationLayoutController {

private final ApplicationLayoutService service;
private final ModelMapper mapper;

/**
* Gets the persisted application layout for the requesting user. If the requesting user does not have an
* application layout persisted, a default layout with a null username is returned instead. No more than one
* application layout can be persisted for a given user.
*
* @return the application layout
*/
@ResponseStatus(HttpStatus.OK)
@GetMapping
public ApplicationLayoutDto getApplicationLayout(@RequestHeader("username") String username) {
return mapper.map(service.getApplicationLayout(username), ApplicationLayoutDto.class);
}

/**
* Creates or updates the unique application layout for the requesting user.
*
* @param layoutDefinition JSON representation of the application layout to be created
* @param username the user making the request
*/
@ResponseStatus(HttpStatus.CREATED)
@PutMapping
public void persistApplicationLayout(@RequestHeader("username") String username, @RequestBody ObjectNode layoutDefinition) {
service.persistApplicationLayout(username, layoutDefinition);
}

/**
* Deletes the application layout for the requesting user. A 404 will be returned if there is no existing
* application layout.
*
* @param username the user making the request
*/
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping
public void deleteApplicationLayout(@RequestHeader("username") String username) {
service.deleteApplicationLayout(username);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.finos.vuu.layoutserver.dto.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;

import javax.validation.constraints.NotNull;

@Data
public class LayoutRequestDto {

@JsonProperty(value = "definition", required = true)
@NotNull(message = "Definition must not be null")
private ObjectNode definition;

@JsonProperty(value = "metadata", required = true)
@NotNull(message = "Metadata must not be null")
private MetadataRequestDto metadata;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.finos.vuu.layoutserver.dto.response;

import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;

@Data
public class ApplicationLayoutDto {
private String username;
private ObjectNode definition;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.finos.vuu.layoutserver.dto.response;

import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
import org.finos.vuu.layoutserver.utils.ObjectNodeConverter;

import javax.persistence.Column;
import javax.persistence.Convert;
import java.util.UUID;

@Data
public class LayoutResponseDto {

private UUID id;
private ObjectNode definition;

private MetadataResponseDto metadata;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.finos.vuu.layoutserver.model;

import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.finos.vuu.layoutserver.utils.ObjectNodeConverter;

import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.Id;

@Data
@Entity
@RequiredArgsConstructor
@AllArgsConstructor
public class ApplicationLayout {
@Id
private String username;

@Convert(converter = ObjectNodeConverter.class)
@Column(columnDefinition = "JSON")
private ObjectNode definition;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.finos.vuu.layoutserver.model;

import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
import org.finos.vuu.layoutserver.utils.ObjectNodeConverter;

import javax.persistence.*;
import java.util.UUID;

@Data
@Entity
public class Layout {

@Id
@Column(columnDefinition = "BINARY(16)")
private UUID id;

@Convert(converter = ObjectNodeConverter.class)
@Column(columnDefinition = "JSON")
private ObjectNode definition;

@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "metadata_id", referencedColumnName = "id")
private Metadata metadata;

public void setId(UUID id) {
this.id = id;
this.metadata.setId(id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.finos.vuu.layoutserver.service;

import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import org.finos.vuu.layoutserver.model.ApplicationLayout;
import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository;
import org.finos.vuu.layoutserver.utils.DefaultApplicationLayoutLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.stereotype.Service;

import java.util.NoSuchElementException;

@RequiredArgsConstructor
@Service
public class ApplicationLayoutService {

private static final Logger logger = LoggerFactory.getLogger(ApplicationLayoutService.class);
private final ApplicationLayoutRepository repository;
private final DefaultApplicationLayoutLoader defaultLoader;

public void persistApplicationLayout(String username, ObjectNode layoutDefinition) {
repository.save(new ApplicationLayout(username, layoutDefinition));
}

public ApplicationLayout getApplicationLayout(String username) {
return repository.findById(username).orElseGet(() -> {
logger.info("No application layout for user, returning default");
return defaultLoader.getDefaultLayout();
});
}

public void deleteApplicationLayout(String username) {
try {
repository.deleteById(username);
} catch (EmptyResultDataAccessException e) {
throw new NoSuchElementException("No layout found for user: " + username);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.finos.vuu.layoutserver.utils;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException;
import org.finos.vuu.layoutserver.model.ApplicationLayout;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class DefaultApplicationLayoutLoader {
private static final String DEFAULT_LAYOUT_FILE = "defaultApplicationLayout.json";
private static ApplicationLayout defaultLayout;

@Bean
public ApplicationLayout getDefaultLayout() {
if (defaultLayout == null) {
loadDefaultLayout();
}
return defaultLayout;
}

private void loadDefaultLayout() {
ObjectNode definition = loadDefaultLayoutJsonFile();
defaultLayout = new ApplicationLayout(null, definition);
}

private ObjectNode loadDefaultLayoutJsonFile() {
ObjectMapper objectMapper = new ObjectMapper();
ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE);
try {
return objectMapper.readValue(resource.getInputStream(), ObjectNode.class);
} catch (IOException e) {
throw new InternalServerErrorException("Failed to read default application layout");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.finos.vuu.layoutserver.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.io.IOException;

@Converter
public class ObjectNodeConverter implements AttributeConverter<ObjectNode, String> {
private static final Logger logger = LoggerFactory.getLogger(ObjectNodeConverter.class);
private static final ObjectMapper objectMapper = new ObjectMapper();

@Override
public String convertToDatabaseColumn(ObjectNode definition) {
try {
return objectMapper.writeValueAsString(definition);
} catch (final JsonProcessingException e) {
logger.error("JSON writing error", e);
return null;
}
}

@Override
public ObjectNode convertToEntityAttribute(String definition) {
try {
return objectMapper.readValue(extractDefinition(definition), new TypeReference<>() {});
} catch (final IOException e) {
logger.error("JSON reading error", e);
return null;
}
}

private String extractDefinition(String definition) {
if (definition.startsWith("\"") && definition.endsWith("\"")) {
definition = definition.substring(1, definition.length() - 1);
}
return definition.replaceAll("\\\\", "");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.finos.vuu.layoutserver.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto;
import org.finos.vuu.layoutserver.model.ApplicationLayout;
import org.finos.vuu.layoutserver.service.ApplicationLayoutService;
import org.finos.vuu.layoutserver.utils.ObjectNodeConverter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.modelmapper.ModelMapper;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

class ApplicationLayoutControllerTest {
private static ApplicationLayoutService mockService;
private static ApplicationLayoutController controller;
private static final ModelMapper modelMapper = new ModelMapper();
private static final ObjectNodeConverter objectNodeConverter = new ObjectNodeConverter();

@BeforeEach
public void setup() {
mockService = Mockito.mock(ApplicationLayoutService.class);
controller = new ApplicationLayoutController(mockService, modelMapper);
}

@Test
public void getApplicationLayout_anyUsername_returnsLayoutFromService() throws JsonProcessingException {
String user = "user";
ObjectNode definition = objectNodeConverter.convertToEntityAttribute("{\"id\":\"main-tabs\"}");

when(mockService.getApplicationLayout(user))
.thenReturn(new ApplicationLayout(user, definition));

ApplicationLayoutDto response = controller.getApplicationLayout(user);

assertThat(response.getUsername()).isEqualTo(user);
assertThat(response.getDefinition()).isEqualTo(definition);

verify(mockService, times(1)).getApplicationLayout(user);
}

@Test
public void persistApplicationLayout_anyInput_callsService() throws JsonProcessingException {
String user = "user";
ObjectNode definition = objectNodeConverter.convertToEntityAttribute("{\"id\":\"main-tabs\"}");

controller.persistApplicationLayout(user, definition);

verify(mockService, times(1)).persistApplicationLayout(user, definition);
}

@Test
public void deleteApplicationLayout_anyUsername_callsService() {
String user = "user";

controller.deleteApplicationLayout(user);

verify(mockService, times(1)).deleteApplicationLayout(user);
}
}
Loading

0 comments on commit c988134

Please sign in to comment.