Skip to content

Commit

Permalink
[backend/frontend] duplicate XLS mapper (#1250)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarineLeM committed Aug 29, 2024
1 parent 07a544e commit 6a711a7
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ public void importMappers(@RequestPart("file") @NotNull MultipartFile file) thro
}
}

@Secured(ROLE_ADMIN)
@PostMapping("/api/mappers/{mapperId}")
@Operation(summary = "Duplicate XLS mapper by id")
public ImportMapper duplicateMapper(@PathVariable @NotBlank final String mapperId) {
return mapperService.getDuplicateImportMapper(mapperId);
}

@Secured(ROLE_ADMIN)
@PutMapping("/api/mappers/{mapperId}")
public ImportMapper updateImportMapper(@PathVariable String mapperId, @Valid @RequestBody ImportMapperUpdateInput importMapperUpdateInput) {
Expand Down
76 changes: 49 additions & 27 deletions openbas-api/src/main/java/io/openbas/service/MapperService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,28 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.openbas.database.model.ImportMapper;
import io.openbas.database.model.InjectImporter;
import io.openbas.database.model.InjectorContract;
import io.openbas.database.model.RuleAttribute;
import io.openbas.database.model.*;
import io.openbas.database.repository.ImportMapperRepository;
import io.openbas.database.repository.InjectorContractRepository;
import io.openbas.helper.ObjectMapperHelper;
import io.openbas.rest.exception.ElementNotFoundException;
import io.openbas.rest.mapper.export.MapperExportMixins;
import io.openbas.rest.mapper.form.*;
import io.openbas.utils.CopyObjectListUtils;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import static io.openbas.utils.StringUtils.duplicateString;
import static java.util.stream.StreamSupport.stream;

@RequiredArgsConstructor
Expand Down Expand Up @@ -52,33 +51,56 @@ public ImportMapper createImportMapper(ImportMapperAddInput importMapperAddInput
importMapper.setInjectImporters(new ArrayList<>());

Map<String, InjectorContract> mapInjectorContracts = getMapOfInjectorContracts(
importMapperAddInput.getImporters()
.stream()
.map(InjectImporterAddInput::getInjectorContractId)
.toList()
importMapperAddInput.getImporters()
.stream()
.map(InjectImporterAddInput::getInjectorContractId)
.toList()
);

importMapperAddInput.getImporters().forEach(
injectImporterInput -> {
InjectImporter injectImporter = new InjectImporter();
injectImporter.setInjectorContract(mapInjectorContracts.get(injectImporterInput.getInjectorContractId()));
injectImporter.setImportTypeValue(injectImporterInput.getInjectTypeValue());
injectImporter.setRuleAttributes(new ArrayList<>());
injectImporterInput.getRuleAttributes().forEach(ruleAttributeInput -> {
RuleAttribute ruleAttribute = new RuleAttribute();
ruleAttribute.setColumns(ruleAttributeInput.getColumns());
ruleAttribute.setName(ruleAttributeInput.getName());
ruleAttribute.setDefaultValue(ruleAttributeInput.getDefaultValue());
ruleAttribute.setAdditionalConfig(ruleAttributeInput.getAdditionalConfig());
injectImporter.getRuleAttributes().add(ruleAttribute);
});
importMapper.getInjectImporters().add(injectImporter);
}
injectImporterInput -> {
InjectImporter injectImporter = new InjectImporter();
injectImporter.setInjectorContract(mapInjectorContracts.get(injectImporterInput.getInjectorContractId()));
injectImporter.setImportTypeValue(injectImporterInput.getInjectTypeValue());

injectImporter.setRuleAttributes(new ArrayList<>());
injectImporterInput.getRuleAttributes().forEach(ruleAttributeInput -> {
injectImporter.getRuleAttributes().add(CopyObjectListUtils.copyObjectWithoutId(ruleAttributeInput, RuleAttribute.class));
});
importMapper.getInjectImporters().add(injectImporter);
}
);

return importMapper;
}

/**
* Duplicate importMapper by id
* @param importMapperId id of the mapper that need to be duplicated
* @return The duplicated ImportMapper
*/
@Transactional
public ImportMapper getDuplicateImportMapper(@NotBlank String importMapperId) {
if (StringUtils.isNotBlank(importMapperId)) {
ImportMapper importMapperOrigin = importMapperRepository.findById(UUID.fromString(importMapperId)).orElseThrow();
ImportMapper importMapper = CopyObjectListUtils.copyObjectWithoutId(importMapperOrigin, ImportMapper.class);
importMapper.setName(duplicateString(importMapperOrigin.getName()));
List<InjectImporter> injectImporters = getInjectImportersDuplicated(importMapperOrigin.getInjectImporters());
importMapper.setInjectImporters(injectImporters);
return importMapperRepository.save(importMapper);
}
throw new ElementNotFoundException();

Check warning on line 92 in openbas-api/src/main/java/io/openbas/service/MapperService.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/service/MapperService.java#L92

Added line #L92 was not covered by tests
}

private List<InjectImporter> getInjectImportersDuplicated(List<InjectImporter> injectImportersOrigin) {
List<InjectImporter> injectImporters = CopyObjectListUtils.copyWithoutIds(injectImportersOrigin, InjectImporter.class);
injectImporters.forEach(injectImport -> {
List<RuleAttribute> ruleAttributes = CopyObjectListUtils.copyWithoutIds(injectImport.getRuleAttributes(), RuleAttribute.class);
injectImport.setRuleAttributes(ruleAttributes);
});
return injectImporters;
}

/**
* Update an ImportMapper object from a MapperUpdateInput one
* @param mapperId the id of the mapper that needs to be updated
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,72 @@
package io.openbas.utils;

import io.openbas.database.model.Base;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import org.apache.commons.beanutils.BeanUtils;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

public class CopyObjectListUtils {

public static <T extends Base> List<T> copyWithoutIds(@NotNull final List<T> origins, Class<T> clazz) {
List<T> destinations = new ArrayList<>();
return copyCollection(origins, clazz, destinations, true);
}
public static <T extends Base> List<T> copy(@NotNull final List<T> origins, Class<T> clazz) {
List<T> destinations = new ArrayList<>();
return copyCollection(origins, clazz, destinations);
return copyCollection(origins, clazz, destinations, false);
}

public static <T extends Base> Set<T> copy(@NotNull final Set<T> origins, Class<T> clazz) {
Set<T> destinations = new HashSet<>();
return copyCollection(origins, clazz, destinations);
return copyCollection(origins, clazz, destinations, false);
}

public static <T extends Base, C extends Collection<T>> C copyCollection(@NotNull final C origins, Class<T> clazz, C destinations) {
public static <T extends Base, C extends Collection<T>> C copyCollection(
@NotNull final C origins, Class<T> clazz, C destinations, Boolean withoutId) {
origins.forEach(origin -> {
try {
T destination = clazz.getDeclaredConstructor().newInstance();
BeanUtils.copyProperties(destination, origin);
destinations.add(destination);
if (withoutId){
destinations.add(copyObjectWithoutId(origin, clazz));
} else {
T destination = clazz.getDeclaredConstructor().newInstance();
BeanUtils.copyProperties(destination, origin);
destinations.add(destination);
}
} catch (IllegalAccessException | InvocationTargetException | InstantiationException |
NoSuchMethodException e) {
throw new RuntimeException(e);
throw new RuntimeException("Failed to copy object", e);

Check warning on line 41 in openbas-api/src/main/java/io/openbas/utils/CopyObjectListUtils.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/utils/CopyObjectListUtils.java#L41

Added line #L41 was not covered by tests
}
});
return destinations;
}

public static <T, C> T copyObjectWithoutId(C origin, Class<T> targetClass) {
try {
T target = targetClass.getDeclaredConstructor().newInstance();

// Get all declared fields from the source object
Field[] fields = origin.getClass().getDeclaredFields();

for (Field field : fields) {
field.setAccessible(true);

// Skip the 'id' field
if (field.isAnnotationPresent(Id.class)) {
continue;
}

// Copy the field value from source to target
Field targetField = target.getClass().getDeclaredField(field.getName());
targetField.setAccessible(true);
targetField.set(target, field.get(origin));
}
return target;
} catch (Exception e) {
throw new RuntimeException("Failed to copy object", e);

Check warning on line 69 in openbas-api/src/main/java/io/openbas/utils/CopyObjectListUtils.java

View check run for this annotation

Codecov / codecov/patch

openbas-api/src/main/java/io/openbas/utils/CopyObjectListUtils.java#L68-L69

Added lines #L68 - L69 were not covered by tests
}
}
}
23 changes: 23 additions & 0 deletions openbas-api/src/test/java/io/openbas/rest/MapperApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,29 @@ void createMapper() throws Exception {
assertEquals(JsonPath.read(response, "$.import_mapper_id"), importMapper.getId());
}

@DisplayName("Test duplicate a mapper")
@Test
void duplicateMapper() throws Exception {
// -- PREPARE --
ImportMapper importMapper = MockMapperUtils.createImportMapper();
ImportMapper importMapperDuplicated = MockMapperUtils.createImportMapper();
when(mapperService.getDuplicateImportMapper(any())).thenReturn(importMapperDuplicated);

// -- EXECUTE --
String response = this.mvc
.perform(MockMvcRequestBuilders.post("/api/mappers/"+ importMapper.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(importMapper)))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();

// -- ASSERT --
assertNotNull(response);
assertEquals(JsonPath.read(response, "$.import_mapper_id"), importMapperDuplicated.getId());
}

@DisplayName("Test delete a specific mapper")
@Test
void deleteSpecificMapper() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Optional;

import static io.openbas.utils.StringUtils.duplicateString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@SpringBootTest
Expand Down Expand Up @@ -78,6 +81,40 @@ void createMapper() throws Exception {
assertEquals(importMapperResponse.getId(), importMapper.getId());
}

@DisplayName("Test duplicate a mapper")
@Test
void duplicateMapper() throws Exception {
// -- PREPARE --
ImportMapper importMapper = MockMapperUtils.createImportMapper();
when(importMapperRepository.findById(any())).thenReturn(Optional.of(importMapper));
ImportMapper importMapperSaved = MockMapperUtils.createImportMapper();
when(importMapperRepository.save(any(ImportMapper.class))).thenReturn(importMapperSaved);

// -- EXECUTE --
ImportMapper response = mapperService.getDuplicateImportMapper(importMapper.getId());

// -- ASSERT --
ArgumentCaptor<ImportMapper> importMapperCaptor = ArgumentCaptor.forClass(ImportMapper.class);
verify(importMapperRepository).save(importMapperCaptor.capture());

ImportMapper capturedImportMapper= importMapperCaptor.getValue();
// verify importMapper
assertEquals(duplicateString(importMapper.getName()), capturedImportMapper.getName());
assertEquals(importMapper.getInjectTypeColumn(), capturedImportMapper.getInjectTypeColumn());
assertEquals(importMapper.getInjectImporters().size(), capturedImportMapper.getInjectImporters().size());
// verify injectImporter
assertEquals("", capturedImportMapper.getInjectImporters().get(0).getId());
assertEquals(importMapper.getInjectImporters().get(0).getImportTypeValue(), capturedImportMapper.getInjectImporters().get(0).getImportTypeValue());
assertEquals(importMapper.getInjectImporters().get(0).getRuleAttributes().size(),
capturedImportMapper.getInjectImporters().get(0).getRuleAttributes().size());
// verify ruleAttribute
assertEquals("", capturedImportMapper.getInjectImporters().get(0).getRuleAttributes().get(0).getId());
assertEquals(importMapper.getInjectImporters().get(0).getRuleAttributes().get(0).getName(),
capturedImportMapper.getInjectImporters().get(0).getRuleAttributes().get(0).getName());

assertEquals(response.getId(), importMapperSaved.getId());
}

@DisplayName("Test update a specific mapper by using new rule attributes and new inject importer")
@Test
void updateSpecificMapperWithNewElements() throws Exception {
Expand Down
4 changes: 4 additions & 0 deletions openbas-front/src/actions/mapper/mapper-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export const createMapper = (data: ImportMapperAddInput) => {
return simplePostCall(XLS_MAPPER_URI, data);
};

export const duplicateMapper = (mapperId: string) => {
return simplePostCall(`${XLS_MAPPER_URI}/${mapperId}`, mapperId);
};

export const updateMapper = (mapperId: string, data: ImportMapperUpdateInput) => {
const uri = `${XLS_MAPPER_URI}/${mapperId}`;
return simplePutCall(uri, data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,46 @@ import React, { FunctionComponent, useState } from 'react';
import { PopoverEntry } from '../../../../components/common/ButtonPopover';
import IconPopover from '../../../../components/common/IconPopover';
import type { RawPaginationImportMapper } from '../../../../utils/api-types';
import { deleteMapper, exportMapper } from '../../../../actions/mapper/mapper-actions';
import { deleteMapper, duplicateMapper, exportMapper } from '../../../../actions/mapper/mapper-actions';
import DialogDelete from '../../../../components/common/DialogDelete';
import { useFormatter } from '../../../../components/i18n';
import Drawer from '../../../../components/common/Drawer';
import XlsMapperUpdate from './xls_mapper/XlsMapperUpdate';
import { download } from '../../../../utils/utils';
import DialogDuplicate from '../../../../components/common/DialogDuplicate';

interface Props {
mapper: RawPaginationImportMapper;
onDuplicate?: (result: RawPaginationImportMapper) => void;
onUpdate?: (result: RawPaginationImportMapper) => void;
onDelete?: (result: string) => void;
onExport?: (result: string) => void;
}

const XlsMapperPopover: FunctionComponent<Props> = ({
mapper,
onDuplicate,
onUpdate,
onDelete,
onExport,
}) => {
// Standard hooks
const { t } = useFormatter();

// Duplication
const [openDuplicate, setOpenDuplicate] = useState(false);
const handleOpenDuplicate = () => setOpenDuplicate(true);
const handleCloseDuplicate = () => setOpenDuplicate(false);
const submitDuplicate = () => {
duplicateMapper(mapper.import_mapper_id).then(
(result: { data: RawPaginationImportMapper }) => {
onDuplicate?.(result.data);
return result;
},
);
handleCloseDuplicate();
};

// Edition
const [openEdit, setOpenEdit] = useState(false);

Expand Down Expand Up @@ -59,6 +76,7 @@ const XlsMapperPopover: FunctionComponent<Props> = ({
};

const entries: PopoverEntry[] = [
{ label: 'Duplicate', action: handleOpenDuplicate },
{ label: 'Update', action: handleOpenEdit },
{ label: 'Delete', action: handleOpenDelete },
{ label: 'Export', action: exportMapperAction },
Expand All @@ -78,6 +96,12 @@ const XlsMapperPopover: FunctionComponent<Props> = ({
handleClose={handleCloseEdit}
/>
</Drawer>
<DialogDuplicate
open={openDuplicate}
handleClose={handleCloseDuplicate}
handleSubmit={submitDuplicate}
text={`${t('Do you want to duplicate this XLS mapper :')} ${mapper.import_mapper_name} ?`}
/>
<DialogDelete
open={openDelete}
handleClose={handleCloseDelete}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const XlsMappers = () => {
<ListItemSecondaryAction>
<XlsMapperPopover
mapper={mapper}
onDuplicate={(result) => setMappers([result, ...mappers])}
onUpdate={(result) => setMappers(mappers.map((existing) => (existing.import_mapper_id !== result.import_mapper_id ? existing : result)))}
onDelete={(result) => setMappers(mappers.filter((existing) => (existing.import_mapper_id !== result)))}
/>
Expand Down
Loading

0 comments on commit 6a711a7

Please sign in to comment.