diff --git a/registry-integration-tests/src/test/java/org/gbif/registry/database/TestCaseDatabaseInitializer.java b/registry-integration-tests/src/test/java/org/gbif/registry/database/TestCaseDatabaseInitializer.java index 8bc51a6052..9a41329340 100644 --- a/registry-integration-tests/src/test/java/org/gbif/registry/database/TestCaseDatabaseInitializer.java +++ b/registry-integration-tests/src/test/java/org/gbif/registry/database/TestCaseDatabaseInitializer.java @@ -64,7 +64,7 @@ public class TestCaseDatabaseInitializer implements BeforeEachCallback { "collection_tag", "institution_collection_person", "institution_identifier", "institution_tag", "institution_occurrence_mapping", "collection_occurrence_mapping", "collection_person", "collection", "institution", "address", "gbif_doi", "pipeline_step", - "pipeline_process", "pipeline_execution", "derived_dataset"); + "pipeline_process", "pipeline_execution", "derived_dataset", "change_suggestion"); private final DataSource dataSource; diff --git a/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/suggestions/BaseChangeSuggestionServiceIT.java b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/suggestions/BaseChangeSuggestionServiceIT.java new file mode 100644 index 0000000000..e982516c74 --- /dev/null +++ b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/suggestions/BaseChangeSuggestionServiceIT.java @@ -0,0 +1,366 @@ +/* + * Copyright 2020 Global Biodiversity Information Facility (GBIF) + * + * 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 org.gbif.registry.ws.it.collections.suggestions; + +import org.gbif.api.model.collections.Address; +import org.gbif.api.model.collections.Collection; +import org.gbif.api.model.collections.CollectionEntity; +import org.gbif.api.model.collections.Contactable; +import org.gbif.api.model.collections.Institution; +import org.gbif.api.model.collections.suggestions.ChangeSuggestion; +import org.gbif.api.model.collections.suggestions.ChangeSuggestionService; +import org.gbif.api.model.collections.suggestions.Status; +import org.gbif.api.model.collections.suggestions.Type; +import org.gbif.api.model.common.paging.Pageable; +import org.gbif.api.model.common.paging.PagingRequest; +import org.gbif.api.model.common.paging.PagingResponse; +import org.gbif.api.model.registry.LenientEquals; +import org.gbif.api.service.collections.CrudService; +import org.gbif.api.vocabulary.Country; +import org.gbif.registry.database.TestCaseDatabaseInitializer; +import org.gbif.registry.search.test.EsManageServer; +import org.gbif.registry.service.collections.suggestions.InstitutionChangeSuggestionService; +import org.gbif.registry.ws.it.BaseItTest; +import org.gbif.ws.client.filter.SimplePrincipalProvider; + +import java.util.Collections; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** Tests the {@link InstitutionChangeSuggestionService}. */ +public abstract class BaseChangeSuggestionServiceIT< + T extends CollectionEntity & Contactable & LenientEquals> + extends BaseItTest { + + protected static final String PROPOSER = "proposer"; + protected static final Pageable DEFAULT_PAGE = new PagingRequest(0L, 5); + + @RegisterExtension + protected TestCaseDatabaseInitializer databaseRule = + TestCaseDatabaseInitializer.builder().dataSource(database.getTestDatabase()).build(); + + private final ChangeSuggestionService changeSuggestionService; + private final CrudService crudService; + + protected BaseChangeSuggestionServiceIT( + SimplePrincipalProvider simplePrincipalProvider, + EsManageServer esServer, + ChangeSuggestionService changeSuggestionService, + CrudService crudService) { + super(simplePrincipalProvider, esServer); + this.changeSuggestionService = changeSuggestionService; + this.crudService = crudService; + } + + @Test + public void newEntitySuggestionTest() { + // State + T entity = createEntity(); + + Address address = new Address(); + address.setCountry(Country.DENMARK); + entity.setAddress(address); + + ChangeSuggestion suggestion = createEmptyChangeSuggestion(); + suggestion.setSuggestedEntity(entity); + suggestion.setType(Type.CREATE); + suggestion.setProposedBy(PROPOSER); + suggestion.setComments(Collections.singletonList("comment")); + + // When + int suggKey = changeSuggestionService.createChangeSuggestion(suggestion); + + // Then + suggestion = changeSuggestionService.getChangeSuggestion(suggKey); + assertCreatedSuggestion(suggestion); + assertEquals(Type.CREATE, suggestion.getType()); + assertEquals(Country.DENMARK, suggestion.getCountry()); + assertTrue(suggestion.getChanges().isEmpty()); + + // When - update the suggestion (e.g.: the reviewer does some changes) + int numberChanges = reviewEntity(entity); + suggestion.setSuggestedEntity(entity); + suggestion.getComments().add("Review"); + changeSuggestionService.updateChangeSuggestion(suggestion); + + // Then + suggestion = changeSuggestionService.getChangeSuggestion(suggKey); + assertTrue(entity.lenientEquals(suggestion.getSuggestedEntity())); + assertEquals(numberChanges, suggestion.getChanges().size()); + assertEquals(2, suggestion.getComments().size()); + + // When + changeSuggestionService.applyChangeSuggestion(suggKey); + + // Then + suggestion = changeSuggestionService.getChangeSuggestion(suggKey); + assertEquals(Status.APPLIED, suggestion.getStatus()); + assertNotNull(suggestion.getEntityKey()); + assertNotNull(suggestion.getApplied()); + assertNotNull(suggestion.getAppliedBy()); + + T appliedEntity = crudService.get(suggestion.getEntityKey()); + T expected = suggestion.getSuggestedEntity(); + expected.setKey(suggestion.getEntityKey()); + expected.getAddress().setKey(appliedEntity.getAddress().getKey()); + assertTrue(appliedEntity.lenientEquals(expected)); + } + + @Test + public void changeInstitutionSuggestionTest() { + // State + T entity = createEntity(); + + Address address = new Address(); + address.setCountry(Country.DENMARK); + entity.setAddress(address); + + UUID entityKey = crudService.create(entity); + + // suggested changes + int numberChanges = updateEntity(entity); + address.setCity("city"); + + ChangeSuggestion suggestion = createEmptyChangeSuggestion(); + suggestion.setSuggestedEntity(entity); + suggestion.setType(Type.UPDATE); + suggestion.setEntityKey(entityKey); + suggestion.setProposedBy(PROPOSER); + suggestion.setComments(Collections.singletonList("comment")); + + // When + int suggKey = changeSuggestionService.createChangeSuggestion(suggestion); + + // Then + suggestion = changeSuggestionService.getChangeSuggestion(suggKey); + assertCreatedSuggestion(suggestion); + assertEquals(Type.UPDATE, suggestion.getType()); + assertEquals(Country.DENMARK, suggestion.getCountry()); + assertEquals(address.getCity(), suggestion.getSuggestedEntity().getAddress().getCity()); + assertTrue(entity.lenientEquals(suggestion.getSuggestedEntity())); + assertEquals(numberChanges, suggestion.getChanges().size()); + + // When - update the suggestion (e.g.: the reviewer does some changes) + numberChanges += reviewEntity(entity); + suggestion.setSuggestedEntity(entity); + suggestion.getComments().add("Review"); + changeSuggestionService.updateChangeSuggestion(suggestion); + + // Then + suggestion = changeSuggestionService.getChangeSuggestion(suggKey); + assertTrue(entity.lenientEquals(suggestion.getSuggestedEntity())); + assertEquals(numberChanges, suggestion.getChanges().size()); + assertEquals(2, suggestion.getComments().size()); + + // When + changeSuggestionService.applyChangeSuggestion(suggKey); + + // Then + suggestion = changeSuggestionService.getChangeSuggestion(suggKey); + assertEquals(Status.APPLIED, suggestion.getStatus()); + assertNotNull(suggestion.getApplied()); + assertNotNull(suggestion.getAppliedBy()); + + T appliedEntity = crudService.get(entityKey); + assertTrue(appliedEntity.lenientEquals(suggestion.getSuggestedEntity())); + } + + @Test + public void deleteInstitutionSuggestionTest() { + // State + T entity = createEntity(); + UUID entityKey = crudService.create(entity); + + ChangeSuggestion suggestion = createEmptyChangeSuggestion(); + suggestion.setSuggestedEntity(entity); + suggestion.setType(Type.DELETE); + suggestion.setEntityKey(entityKey); + suggestion.setProposedBy(PROPOSER); + suggestion.setComments(Collections.singletonList("comment")); + + // When + int suggKey = changeSuggestionService.createChangeSuggestion(suggestion); + + // Then + suggestion = changeSuggestionService.getChangeSuggestion(suggKey); + assertCreatedSuggestion(suggestion); + assertEquals(Type.DELETE, suggestion.getType()); + + // When + changeSuggestionService.applyChangeSuggestion(suggKey); + + // Then + suggestion = changeSuggestionService.getChangeSuggestion(suggKey); + assertEquals(Status.APPLIED, suggestion.getStatus()); + assertNotNull(suggestion.getApplied()); + assertNotNull(suggestion.getAppliedBy()); + T appliedEntity = crudService.get(entityKey); + assertNotNull(appliedEntity.getDeleted()); + } + + @Test + public void mergeInstitutionSuggestionTest() { + // State + T entity = createEntity(); + UUID entityKey = crudService.create(entity); + + T entity2 = createEntity(); + UUID entity2Key = crudService.create(entity2); + + ChangeSuggestion suggestion = createEmptyChangeSuggestion(); + suggestion.setSuggestedEntity(entity); + suggestion.setType(Type.MERGE); + suggestion.setEntityKey(entityKey); + suggestion.setProposedBy(PROPOSER); + suggestion.setMergeTargetKey(entity2Key); + suggestion.setComments(Collections.singletonList("comment")); + + // When + int suggKey = changeSuggestionService.createChangeSuggestion(suggestion); + + // Then + suggestion = changeSuggestionService.getChangeSuggestion(suggKey); + assertCreatedSuggestion(suggestion); + assertEquals(Type.MERGE, suggestion.getType()); + + // When + changeSuggestionService.applyChangeSuggestion(suggKey); + + // Then + suggestion = changeSuggestionService.getChangeSuggestion(suggKey); + assertEquals(Status.APPLIED, suggestion.getStatus()); + assertNotNull(suggestion.getApplied()); + assertNotNull(suggestion.getAppliedBy()); + + T appliedEntity = crudService.get(entityKey); + assertEquals(entity2Key, getReplacedByValue(appliedEntity)); + assertNotNull(appliedEntity.getDeleted()); + } + + @Test + public void discardSuggestionTest() { + // State + T entity = createEntity(); + + ChangeSuggestion suggestion = createEmptyChangeSuggestion(); + suggestion.setSuggestedEntity(entity); + suggestion.setType(Type.CREATE); + suggestion.setProposedBy(PROPOSER); + suggestion.setComments(Collections.singletonList("comment")); + + // When + int suggKey = changeSuggestionService.createChangeSuggestion(suggestion); + + // Then + suggestion = changeSuggestionService.getChangeSuggestion(suggKey); + assertCreatedSuggestion(suggestion); + assertEquals(Type.CREATE, suggestion.getType()); + + // When + changeSuggestionService.discardChangeSuggestion(suggKey); + + // Then + suggestion = changeSuggestionService.getChangeSuggestion(suggKey); + assertEquals(Status.DISCARDED, suggestion.getStatus()); + assertNotNull(suggestion.getDiscarded()); + assertNotNull(suggestion.getDiscardedBy()); + } + + @Test + public void listTest() { + // State + T entity = createEntity(); + ChangeSuggestion suggestion = createEmptyChangeSuggestion(); + suggestion.setSuggestedEntity(entity); + suggestion.setType(Type.CREATE); + suggestion.setProposedBy(PROPOSER); + suggestion.setComments(Collections.singletonList("comment")); + + int suggKey1 = changeSuggestionService.createChangeSuggestion(suggestion); + + T entity2 = createEntity(); + UUID entity2Key = crudService.create(entity2); + ChangeSuggestion suggestion2 = createEmptyChangeSuggestion(); + suggestion2.setSuggestedEntity(entity2); + suggestion2.setEntityKey(entity2Key); + suggestion2.setType(Type.UPDATE); + suggestion2.setProposedBy(PROPOSER); + suggestion2.setComments(Collections.singletonList("comment")); + + int suggKey2 = changeSuggestionService.createChangeSuggestion(suggestion2); + + // When + PagingResponse> results = + changeSuggestionService.list(Status.APPLIED, null, null, null, null, DEFAULT_PAGE); + // Then + assertEquals(0, results.getResults().size()); + assertEquals(0, results.getCount()); + + // When + results = changeSuggestionService.list(null, Type.CREATE, null, null, null, DEFAULT_PAGE); + // Then + assertEquals(1, results.getResults().size()); + assertEquals(1, results.getCount()); + + // When + results = changeSuggestionService.list(null, null, null, null, entity2Key, DEFAULT_PAGE); + // Then + assertEquals(1, results.getResults().size()); + assertEquals(1, results.getCount()); + + // When + results = + changeSuggestionService.list( + null, null, Country.AFGHANISTAN, null, entity2Key, DEFAULT_PAGE); + // Then + assertEquals(0, results.getResults().size()); + assertEquals(0, results.getCount()); + } + + protected void assertCreatedSuggestion(ChangeSuggestion created) { + assertEquals(Status.PENDING, created.getStatus()); + assertNull(created.getApplied()); + assertNull(created.getDiscarded()); + assertEquals(getSimplePrincipalProvider().get().getName(), created.getModifiedBy()); + assertEquals(PROPOSER, created.getProposedBy()); + assertEquals(1, created.getComments().size()); + } + + protected UUID getReplacedByValue(T entity) { + if (entity instanceof Institution) { + return ((Institution) entity).getReplacedBy(); + } else if (entity instanceof Collection) { + return ((Collection) entity).getReplacedBy(); + } else { + throw new UnsupportedOperationException(); + } + } + + abstract T createEntity(); + + abstract int updateEntity(T entity); + + abstract int reviewEntity(T entity); + + abstract ChangeSuggestion createEmptyChangeSuggestion(); +} diff --git a/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/suggestions/CollectionChangeSuggestionServiceIT.java b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/suggestions/CollectionChangeSuggestionServiceIT.java new file mode 100644 index 0000000000..ae2836cb3b --- /dev/null +++ b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/suggestions/CollectionChangeSuggestionServiceIT.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020 Global Biodiversity Information Facility (GBIF) + * + * 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 org.gbif.registry.ws.it.collections.suggestions; + +import org.gbif.api.model.collections.AlternativeCode; +import org.gbif.api.model.collections.Collection; +import org.gbif.api.model.collections.suggestions.CollectionChangeSuggestion; +import org.gbif.api.model.registry.Identifier; +import org.gbif.api.service.collections.CollectionService; +import org.gbif.api.vocabulary.IdentifierType; +import org.gbif.registry.search.test.EsManageServer; +import org.gbif.registry.service.collections.suggestions.CollectionChangeSuggestionService; +import org.gbif.ws.client.filter.SimplePrincipalProvider; + +import java.net.URI; +import java.util.Collections; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; + +/** Tests the {@link CollectionChangeSuggestionService}. */ +public class CollectionChangeSuggestionServiceIT extends BaseChangeSuggestionServiceIT { + + @Autowired + public CollectionChangeSuggestionServiceIT( + SimplePrincipalProvider simplePrincipalProvider, + EsManageServer esServer, + CollectionChangeSuggestionService collectionChangeSuggestionService, + CollectionService collectionService) { + super(simplePrincipalProvider, esServer, collectionChangeSuggestionService, collectionService); + } + + @Override + Collection createEntity() { + Collection c1 = new Collection(); + c1.setCode(UUID.randomUUID().toString()); + c1.setName(UUID.randomUUID().toString()); + return c1; + } + + @Override + int updateEntity(Collection entity) { + entity.setCode(UUID.randomUUID().toString()); + entity.setActive(true); + entity.setApiUrl(URI.create("http://test.com")); + entity.setIdentifiers(Collections.singletonList(new Identifier(IdentifierType.LSID, "test"))); + entity.setAlternativeCodes( + Collections.singletonList(new AlternativeCode(UUID.randomUUID().toString(), "test"))); + return 5; + } + + @Override + int reviewEntity(Collection entity) { + entity.setCode(UUID.randomUUID().toString()); + entity.setDescription(UUID.randomUUID().toString()); + return 2; + } + + @Override + CollectionChangeSuggestion createEmptyChangeSuggestion() { + return new CollectionChangeSuggestion(); + } +} diff --git a/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/suggestions/InstitutionChangeSuggestionServiceIT.java b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/suggestions/InstitutionChangeSuggestionServiceIT.java new file mode 100644 index 0000000000..9ed37882bf --- /dev/null +++ b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/suggestions/InstitutionChangeSuggestionServiceIT.java @@ -0,0 +1,136 @@ +/* + * Copyright 2020 Global Biodiversity Information Facility (GBIF) + * + * 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 org.gbif.registry.ws.it.collections.suggestions; + +import org.gbif.api.model.collections.AlternativeCode; +import org.gbif.api.model.collections.Collection; +import org.gbif.api.model.collections.Institution; +import org.gbif.api.model.collections.suggestions.InstitutionChangeSuggestion; +import org.gbif.api.model.collections.suggestions.Type; +import org.gbif.api.model.registry.Identifier; +import org.gbif.api.service.collections.CollectionService; +import org.gbif.api.service.collections.InstitutionService; +import org.gbif.api.vocabulary.IdentifierType; +import org.gbif.registry.search.test.EsManageServer; +import org.gbif.registry.service.collections.suggestions.InstitutionChangeSuggestionService; +import org.gbif.ws.client.filter.SimplePrincipalProvider; + +import java.util.Collections; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** Tests the {@link InstitutionChangeSuggestionService}. */ +public class InstitutionChangeSuggestionServiceIT + extends BaseChangeSuggestionServiceIT { + + private final InstitutionChangeSuggestionService institutionChangeSuggestionService; + private final InstitutionService institutionService; + private final CollectionService collectionService; + + @Autowired + public InstitutionChangeSuggestionServiceIT( + SimplePrincipalProvider simplePrincipalProvider, + EsManageServer esServer, + InstitutionChangeSuggestionService institutionChangeSuggestionService, + InstitutionService institutionService, + CollectionService collectionService) { + super( + simplePrincipalProvider, esServer, institutionChangeSuggestionService, institutionService); + this.institutionChangeSuggestionService = institutionChangeSuggestionService; + this.institutionService = institutionService; + this.collectionService = collectionService; + } + + @Test + public void convertInstitutionToCollectionSuggestionTest() { + // State + Institution i1 = new Institution(); + i1.setCode("i1"); + i1.setName("institution 1"); + UUID i1Key = institutionService.create(i1); + + InstitutionChangeSuggestion suggestion = new InstitutionChangeSuggestion(); + suggestion.setSuggestedEntity(i1); + suggestion.setType(Type.CONVERSION_TO_COLLECTION); + suggestion.setEntityKey(i1Key); + suggestion.setProposedBy(PROPOSER); + suggestion.setNameForNewInstitutionForConvertedCollection("newInstitution"); + suggestion.setComments(Collections.singletonList("comment")); + + // When + int suggKey = institutionChangeSuggestionService.createChangeSuggestion(suggestion); + + // Then + suggestion = institutionChangeSuggestionService.getChangeSuggestion(suggKey); + assertCreatedSuggestion(suggestion); + assertEquals(Type.CONVERSION_TO_COLLECTION, suggestion.getType()); + + // When + institutionChangeSuggestionService.applyChangeSuggestion(suggKey); + + // Then + suggestion = institutionChangeSuggestionService.getChangeSuggestion(suggKey); + assertNotNull(suggestion.getApplied()); + assertNotNull(suggestion.getAppliedBy()); + + Institution appliedInstitution = institutionService.get(i1Key); + assertNotNull(appliedInstitution.getDeleted()); + UUID collectionKey = appliedInstitution.getConvertedToCollection(); + assertNotNull(collectionKey); + Collection collectionCreated = collectionService.get(collectionKey); + assertNotNull(collectionCreated); + Institution newInstitution = institutionService.get(collectionCreated.getInstitutionKey()); + assertNotNull(newInstitution); + assertEquals( + suggestion.getNameForNewInstitutionForConvertedCollection(), newInstitution.getName()); + } + + @Override + Institution createEntity() { + Institution i1 = new Institution(); + i1.setCode(UUID.randomUUID().toString()); + i1.setName(UUID.randomUUID().toString()); + return i1; + } + + @Override + int updateEntity(Institution entity) { + entity.setCode(UUID.randomUUID().toString()); + entity.setActive(true); + entity.setAdditionalNames(Collections.singletonList(UUID.randomUUID().toString())); + entity.setIdentifiers(Collections.singletonList(new Identifier(IdentifierType.LSID, "test"))); + entity.setAlternativeCodes( + Collections.singletonList(new AlternativeCode(UUID.randomUUID().toString(), "test"))); + return 5; + } + + @Override + int reviewEntity(Institution entity) { + entity.setCode(UUID.randomUUID().toString()); + entity.setDescription(UUID.randomUUID().toString()); + return 2; + } + + @Override + InstitutionChangeSuggestion createEmptyChangeSuggestion() { + return new InstitutionChangeSuggestion(); + } +} diff --git a/registry-persistence/src/main/java/org/gbif/registry/persistence/config/MyBatisConfiguration.java b/registry-persistence/src/main/java/org/gbif/registry/persistence/config/MyBatisConfiguration.java index 3474b66b0c..6ccb6b1d79 100644 --- a/registry-persistence/src/main/java/org/gbif/registry/persistence/config/MyBatisConfiguration.java +++ b/registry-persistence/src/main/java/org/gbif/registry/persistence/config/MyBatisConfiguration.java @@ -60,6 +60,7 @@ import org.gbif.registry.domain.ws.DerivedDatasetUsage; import org.gbif.registry.persistence.mapper.auxhandler.AlternativeCodesTypeHandler; import org.gbif.registry.persistence.mapper.auxhandler.CollectionSummaryTypeHandler; +import org.gbif.registry.persistence.mapper.collections.dto.ChangeSuggestionDto; import org.gbif.registry.persistence.mapper.collections.dto.CollectionDto; import org.gbif.registry.persistence.mapper.collections.dto.CollectionMatchedDto; import org.gbif.registry.persistence.mapper.collections.dto.DuplicateDto; @@ -78,6 +79,7 @@ import org.gbif.registry.persistence.mapper.handler.PredicateTypeHandler; import org.gbif.registry.persistence.mapper.handler.PreservationTypeArrayTypeHandler; import org.gbif.registry.persistence.mapper.handler.StepTypeArrayTypeHandler; +import org.gbif.registry.persistence.mapper.handler.SuggestedChangesTypeHandler; import java.net.URI; import java.util.UUID; @@ -156,6 +158,9 @@ ConfigurationCustomizer mybatisConfigCustomizer() { configuration .getTypeAliasRegistry() .registerAlias("CollectionMatchedDto", CollectionMatchedDto.class); + configuration + .getTypeAliasRegistry() + .registerAlias("ChangeSuggestionDto", ChangeSuggestionDto.class); configuration.getTypeAliasRegistry().registerAlias("UriTypeHandler", UriTypeHandler.class); configuration.getTypeAliasRegistry().registerAlias("UuidTypeHandler", UuidTypeHandler.class); @@ -214,6 +219,9 @@ ConfigurationCustomizer mybatisConfigCustomizer() { configuration .getTypeAliasRegistry() .registerAlias("AlternativeCodesTypeHandler", AlternativeCodesTypeHandler.class); + configuration + .getTypeAliasRegistry() + .registerAlias("SuggestedChangesTypeHandler", SuggestedChangesTypeHandler.class); // external iDigBio configuration.getTypeAliasRegistry().registerAlias("MachineTagDto", MachineTagDto.class); diff --git a/registry-persistence/src/main/java/org/gbif/registry/persistence/mapper/collections/ChangeSuggestionMapper.java b/registry-persistence/src/main/java/org/gbif/registry/persistence/mapper/collections/ChangeSuggestionMapper.java new file mode 100644 index 0000000000..e1f5b43f72 --- /dev/null +++ b/registry-persistence/src/main/java/org/gbif/registry/persistence/mapper/collections/ChangeSuggestionMapper.java @@ -0,0 +1,43 @@ +package org.gbif.registry.persistence.mapper.collections; + +import org.gbif.api.model.collections.EntityType; +import org.gbif.api.model.collections.suggestions.Status; +import org.gbif.api.model.collections.suggestions.Type; +import org.gbif.api.model.common.paging.Pageable; +import org.gbif.api.vocabulary.Country; +import org.gbif.registry.persistence.mapper.collections.dto.ChangeSuggestionDto; + +import java.util.List; +import java.util.UUID; + +import javax.annotation.Nullable; + +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface ChangeSuggestionMapper { + + void create(ChangeSuggestionDto suggestion); + + ChangeSuggestionDto get(@Param("key") int key); + + List list( + @Param("status") Status status, + @Param("type") Type type, + @Param("entityType") EntityType entityType, + @Param("country") Country country, + @Param("proposer") String proposer, + @Param("entityKey") UUID entityKey, + @Nullable @Param("page") Pageable page); + + long count( + @Param("status") Status status, + @Param("type") Type type, + @Param("entityType") EntityType entityType, + @Param("country") Country country, + @Param("proposer") String proposer, + @Param("entityKey") UUID entityKey); + + void update(ChangeSuggestionDto suggestion); +} diff --git a/registry-persistence/src/main/java/org/gbif/registry/persistence/mapper/collections/dto/ChangeDto.java b/registry-persistence/src/main/java/org/gbif/registry/persistence/mapper/collections/dto/ChangeDto.java new file mode 100644 index 0000000000..207c6de19e --- /dev/null +++ b/registry-persistence/src/main/java/org/gbif/registry/persistence/mapper/collections/dto/ChangeDto.java @@ -0,0 +1,107 @@ +package org.gbif.registry.persistence.mapper.collections.dto; + +/* + * Copyright 2020 Global Biodiversity Information Facility (GBIF) + * + * 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 java.io.Serializable; +import java.util.Date; +import java.util.Objects; + +public class ChangeDto implements Serializable { + private String fieldName; + private Class fieldType; + private String fieldGenericTypeName; + private transient Object suggested; + private transient Object previous; + private Date created; + private String author; + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public Class getFieldType() { + return fieldType; + } + + public void setFieldType(Class fieldType) { + this.fieldType = fieldType; + } + + public String getFieldGenericTypeName() { + return fieldGenericTypeName; + } + + public void setFieldGenericTypeName(String fieldGenericTypeName) { + this.fieldGenericTypeName = fieldGenericTypeName; + } + + public Object getSuggested() { + return suggested; + } + + public void setSuggested(Object suggested) { + this.suggested = suggested; + } + + public Object getPrevious() { + return previous; + } + + public void setPrevious(Object previous) { + this.previous = previous; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChangeDto changeDto = (ChangeDto) o; + return Objects.equals(fieldName, changeDto.fieldName) + && Objects.equals(fieldType, changeDto.fieldType) + && Objects.equals(fieldGenericTypeName, changeDto.fieldGenericTypeName) + && Objects.equals(suggested, changeDto.suggested) + && Objects.equals(previous, changeDto.previous) + && Objects.equals(created, changeDto.created) + && Objects.equals(author, changeDto.author); + } + + @Override + public int hashCode() { + return Objects.hash( + fieldName, fieldType, fieldGenericTypeName, suggested, previous, created, author); + } +} diff --git a/registry-persistence/src/main/java/org/gbif/registry/persistence/mapper/collections/dto/ChangeSuggestionDto.java b/registry-persistence/src/main/java/org/gbif/registry/persistence/mapper/collections/dto/ChangeSuggestionDto.java new file mode 100644 index 0000000000..5c39fa3bb6 --- /dev/null +++ b/registry-persistence/src/main/java/org/gbif/registry/persistence/mapper/collections/dto/ChangeSuggestionDto.java @@ -0,0 +1,198 @@ +package org.gbif.registry.persistence.mapper.collections.dto; + +import org.gbif.api.model.collections.EntityType; +import org.gbif.api.model.collections.suggestions.Status; +import org.gbif.api.model.collections.suggestions.Type; +import org.gbif.api.vocabulary.Country; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public class ChangeSuggestionDto { + + private Integer key; + private EntityType entityType; + private UUID entityKey; + private Type type; + private Status status; + private String proposedBy; + private Date proposed; + private String appliedBy; + private Date applied; + private String discardedBy; + private Date discarded; + private Country country; + private String suggestedEntity; + private Set changes = new HashSet<>(); + private List comments = new ArrayList<>(); + private UUID mergeTargetKey; + private UUID institutionConvertedCollection; + private String nameNewInstitutionConvertedCollection; + private Date modified; + private String modifiedBy; + + public Integer getKey() { + return key; + } + + public void setKey(Integer key) { + this.key = key; + } + + public EntityType getEntityType() { + return entityType; + } + + public void setEntityType(EntityType entityType) { + this.entityType = entityType; + } + + public UUID getEntityKey() { + return entityKey; + } + + public void setEntityKey(UUID entityKey) { + this.entityKey = entityKey; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getProposedBy() { + return proposedBy; + } + + public void setProposedBy(String proposedBy) { + this.proposedBy = proposedBy; + } + + public Date getProposed() { + return proposed; + } + + public void setProposed(Date proposed) { + this.proposed = proposed; + } + + public String getAppliedBy() { + return appliedBy; + } + + public void setAppliedBy(String appliedBy) { + this.appliedBy = appliedBy; + } + + public Date getApplied() { + return applied; + } + + public void setApplied(Date applied) { + this.applied = applied; + } + + public String getDiscardedBy() { + return discardedBy; + } + + public void setDiscardedBy(String discardedBy) { + this.discardedBy = discardedBy; + } + + public Date getDiscarded() { + return discarded; + } + + public void setDiscarded(Date discarded) { + this.discarded = discarded; + } + + public Country getCountry() { + return country; + } + + public void setCountry(Country country) { + this.country = country; + } + + public String getSuggestedEntity() { + return suggestedEntity; + } + + public void setSuggestedEntity(String suggestedEntity) { + this.suggestedEntity = suggestedEntity; + } + + public Set getChanges() { + return changes; + } + + public void setChanges(Set changes) { + this.changes = changes; + } + + public List getComments() { + return comments; + } + + public void setComments(List comments) { + this.comments = comments; + } + + public UUID getMergeTargetKey() { + return mergeTargetKey; + } + + public void setMergeTargetKey(UUID mergeTargetKey) { + this.mergeTargetKey = mergeTargetKey; + } + + public UUID getInstitutionConvertedCollection() { + return institutionConvertedCollection; + } + + public void setInstitutionConvertedCollection(UUID institutionConvertedCollection) { + this.institutionConvertedCollection = institutionConvertedCollection; + } + + public String getNameNewInstitutionConvertedCollection() { + return nameNewInstitutionConvertedCollection; + } + + public void setNameNewInstitutionConvertedCollection( + String nameNewInstitutionConvertedCollection) { + this.nameNewInstitutionConvertedCollection = nameNewInstitutionConvertedCollection; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + public String getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(String modifiedBy) { + this.modifiedBy = modifiedBy; + } +} diff --git a/registry-persistence/src/main/java/org/gbif/registry/persistence/mapper/handler/SuggestedChangesTypeHandler.java b/registry-persistence/src/main/java/org/gbif/registry/persistence/mapper/handler/SuggestedChangesTypeHandler.java new file mode 100644 index 0000000000..481c0eb573 --- /dev/null +++ b/registry-persistence/src/main/java/org/gbif/registry/persistence/mapper/handler/SuggestedChangesTypeHandler.java @@ -0,0 +1,138 @@ +/* + * Copyright 2020 Global Biodiversity Information Facility (GBIF) + * + * 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 org.gbif.registry.persistence.mapper.handler; + +import org.gbif.api.model.collections.suggestions.Change; +import org.gbif.registry.persistence.mapper.collections.dto.ChangeDto; + +import java.io.IOException; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.CollectionLikeType; +import com.google.common.base.Strings; + +/** {@link org.apache.ibatis.type.TypeHandler} for arrays of {@link Change}. */ +public class SuggestedChangesTypeHandler extends BaseTypeHandler> { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Override + public void setNonNullParameter( + PreparedStatement ps, int i, Set parameter, JdbcType jdbcType) + throws SQLException { + ps.setString(i, toString(parameter)); + } + + @Override + public Set getNullableResult(ResultSet resultSet, String columnName) + throws SQLException { + return fromString(resultSet.getString(columnName)); + } + + @Override + public Set getNullableResult(ResultSet resultSet, int columnIndex) + throws SQLException { + return fromString(resultSet.getString(columnIndex)); + } + + @Override + public Set getNullableResult(CallableStatement callableStatement, int columnIndex) + throws SQLException { + return fromString(callableStatement.getString(columnIndex)); + } + + private String toString(Set changesList) { + try { + return OBJECT_MAPPER.writeValueAsString(changesList); + } catch (JsonProcessingException e) { + throw new IllegalStateException( + "Couldn't convert changes list to JSON: " + changesList.toString(), e); + } + } + + private Set fromString(String json) { + if (Strings.isNullOrEmpty(json)) { + return new HashSet<>(); + } + + try { + JsonNode root = OBJECT_MAPPER.readTree(json); + Set dtos = new HashSet<>(); + for (JsonNode element : root) { + ChangeDto changeDto = new ChangeDto(); + changeDto.setFieldName(element.get("fieldName").asText()); + + Class fieldType = parseFieldType(element.get("fieldType").asText()); + changeDto.setFieldType(fieldType); + + if (Collection.class.isAssignableFrom(fieldType) + && element.hasNonNull("fieldGenericTypeName")) { + changeDto.setFieldGenericTypeName(element.get("fieldGenericTypeName").asText()); + Class genericType = Class.forName(element.get("fieldGenericTypeName").asText()); + CollectionLikeType collectionLikeType = + OBJECT_MAPPER.getTypeFactory().constructCollectionLikeType(fieldType, genericType); + changeDto.setSuggested( + OBJECT_MAPPER.readValue(element.get("suggested").toString(), collectionLikeType)); + changeDto.setPrevious( + OBJECT_MAPPER.readValue(element.get("previous").toString(), collectionLikeType)); + } else { + changeDto.setSuggested( + OBJECT_MAPPER.readValue( + element.get("suggested").toString(), changeDto.getFieldType())); + changeDto.setPrevious( + OBJECT_MAPPER.readValue( + element.get("previous").toString(), changeDto.getFieldType())); + } + + changeDto.setCreated( + OBJECT_MAPPER.readValue(element.get("created").toString(), Date.class)); + changeDto.setAuthor(element.get("author").asText()); + + dtos.add(changeDto); + } + + return dtos; + } catch (IOException | ClassNotFoundException e) { + throw new IllegalStateException("Couldn't deserialize JSON from DB: " + json, e); + } + } + + private Class parseFieldType(String type) throws ClassNotFoundException { + switch (type) { + case "boolean": + return boolean.class; + case "int": + return int.class; + case "long": + return long.class; + default: + return Class.forName(type); + } + } +} diff --git a/registry-persistence/src/main/resources/liquibase/094-grscicoll-change-suggestions.xml b/registry-persistence/src/main/resources/liquibase/094-grscicoll-change-suggestions.xml new file mode 100644 index 0000000000..13bbfb3d0d --- /dev/null +++ b/registry-persistence/src/main/resources/liquibase/094-grscicoll-change-suggestions.xml @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/registry-persistence/src/main/resources/liquibase/master.xml b/registry-persistence/src/main/resources/liquibase/master.xml index 04fd1dfbfe..6e597b642c 100644 --- a/registry-persistence/src/main/resources/liquibase/master.xml +++ b/registry-persistence/src/main/resources/liquibase/master.xml @@ -99,4 +99,5 @@ + diff --git a/registry-persistence/src/main/resources/org/gbif/registry/persistence/mapper/collections/ChangeSuggestionMapper.xml b/registry-persistence/src/main/resources/org/gbif/registry/persistence/mapper/collections/ChangeSuggestionMapper.xml new file mode 100644 index 0000000000..8ddf991b70 --- /dev/null +++ b/registry-persistence/src/main/resources/org/gbif/registry/persistence/mapper/collections/ChangeSuggestionMapper.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + entity_type, entity_key, type, status, proposed, proposed_by, country, changes, comments, suggested_entity, + merge_target_key, institution_converted_collection, name_new_institution_converted_collection, modified, modified_by + + + + cs.key, cs.entity_type, cs.entity_key, cs.type, cs.status, cs.proposed, cs.proposed_by, cs. applied, + cs.applied_by, cs.discarded_by, cs.discarded, cs.country, cs.suggested_entity, cs.comments, + cs.merge_target_key, cs.changes, cs.institution_converted_collection, cs.name_new_institution_converted_collection, + cs.modified, cs.modified_by + + + + #{entityType,jdbcType=OTHER}, + #{entityKey,jdbcType=OTHER}, + #{type,jdbcType=OTHER}, + #{status,jdbcType=OTHER}, + now(), + #{proposedBy,jdbcType=VARCHAR}, + #{country,jdbcType=VARCHAR}, + #{changes,jdbcType=OTHER,typeHandler=SuggestedChangesTypeHandler}::jsonb, + #{comments,jdbcType=OTHER,typeHandler=StringArrayTypeHandler}, + #{suggestedEntity,jdbcType=OTHER}::jsonb, + #{mergeTargetKey,jdbcType=OTHER}, + #{institutionConvertedCollection,jdbcType=OTHER}, + #{nameNewInstitutionConvertedCollection,jdbcType=VARCHAR}, + now(), + #{modifiedBy,jdbcType=VARCHAR} + + + + suggested_entity = #{suggestedEntity,jdbcType=OTHER}::jsonb, + comments = #{comments,jdbcType=OTHER,typeHandler=StringArrayTypeHandler}, + changes = #{changes,jdbcType=OTHER,typeHandler=SuggestedChangesTypeHandler}::jsonb, + modified = now(), + modified_by = #{modifiedBy,jdbcType=VARCHAR}, + discarded = #{discarded,jdbcType=OTHER}, + discarded_by = #{discardedBy,jdbcType=INTEGER}, + applied = #{applied,jdbcType=OTHER}, + applied_by = #{appliedBy,jdbcType=INTEGER}, + entity_key = #{entityKey,jdbcType=INTEGER} + + + + INSERT INTO change_suggestion() + VALUES() + + + + + + + + + + + UPDATE change_suggestion + SET + WHERE key = #{key,jdbcType=INTEGER} + + + diff --git a/registry-security/src/main/java/org/gbif/registry/security/grscicoll/GrSciCollEditorAuthorizationFilter.java b/registry-security/src/main/java/org/gbif/registry/security/grscicoll/GrSciCollEditorAuthorizationFilter.java index dde78bc98d..1ab9607653 100644 --- a/registry-security/src/main/java/org/gbif/registry/security/grscicoll/GrSciCollEditorAuthorizationFilter.java +++ b/registry-security/src/main/java/org/gbif/registry/security/grscicoll/GrSciCollEditorAuthorizationFilter.java @@ -101,7 +101,9 @@ protected void doFilterInternal( final String path = request.getRequestURI(); // skip GET and OPTIONS requests and only check requests to grscicoll - if (isNotGetOrOptionsRequest(request) && path.contains(GRSCICOLL_PATH)) { + if (isNotGetOrOptionsRequest(request) + && path.contains(GRSCICOLL_PATH) + && !isChangeSuggestionCreation(request, path)) { // user must NOT be null if the resource requires editor rights restrictions ensureUserSetInSecurityContext(authentication, HttpStatus.FORBIDDEN); final String username = authentication.getName(); @@ -130,6 +132,11 @@ protected void doFilterInternal( filterChain.doFilter(request, response); } + // it's a POST but can be done by anyone + private boolean isChangeSuggestionCreation(HttpServletRequest request, String path) { + return "POST".equals(request.getMethod()) && path.endsWith("/changeSuggestion"); + } + private void checkInstitutionAndCollectionUpdatePermissions( HttpServletRequest request, String path, String username) { boolean isDelete = "DELETE".equals(request.getMethod()); diff --git a/registry-service/src/main/java/org/gbif/registry/service/collections/BaseCollectionsService.java b/registry-service/src/main/java/org/gbif/registry/service/collections/BaseCollectionsService.java new file mode 100644 index 0000000000..142303520e --- /dev/null +++ b/registry-service/src/main/java/org/gbif/registry/service/collections/BaseCollectionsService.java @@ -0,0 +1,111 @@ +package org.gbif.registry.service.collections; + +import org.gbif.api.model.collections.Collection; +import org.gbif.api.model.collections.CollectionEntity; +import org.gbif.api.model.collections.Institution; +import org.gbif.api.model.registry.Commentable; +import org.gbif.api.model.registry.Identifiable; +import org.gbif.api.model.registry.MachineTaggable; +import org.gbif.api.model.registry.Taggable; +import org.gbif.registry.persistence.mapper.collections.BaseMapper; + +import java.util.UUID; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import static com.google.common.base.Preconditions.checkArgument; + +// TODO: move events here too? +// TODO: this should be implement the CrudService instead of the web resource +public abstract class BaseCollectionsService< + T extends CollectionEntity & Taggable & Identifiable & MachineTaggable & Commentable> { + + private final BaseMapper baseMapper; + + protected BaseCollectionsService(BaseMapper baseMapper) { + this.baseMapper = baseMapper; + } + + public T get(UUID key) { + return baseMapper.get(key); + } + + public void delete(UUID key) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + T entityToDelete = get(key); + checkArgument(entityToDelete != null, "Entity to delete doesn't exist"); + + entityToDelete.setModifiedBy(authentication.getName()); + update(entityToDelete); + + baseMapper.delete(key); + } + + protected void preCreate(T entity) { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + final String username = authentication.getName(); + entity.setCreatedBy(username); + entity.setModifiedBy(username); + } + + protected void preUpdate(T entity) { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + entity.setModifiedBy(authentication.getName()); + } + + /** + * Some iDigBio collections and institutions don't have code and we allow that in the DB but not + * in the API. + */ + protected void checkCodeUpdate(T newEntity, T oldEntity) { + if (newEntity instanceof Institution) { + Institution newInstitution = (Institution) newEntity; + Institution oldInstitution = (Institution) oldEntity; + + if (newInstitution.getCode() == null && oldInstitution.getCode() != null) { + throw new IllegalArgumentException("Not allowed to delete the code of an institution"); + } + } else if (newEntity instanceof Collection) { + Collection newCollection = (Collection) newEntity; + Collection oldCollection = (Collection) oldEntity; + + if (newCollection.getCode() == null && oldCollection.getCode() != null) { + throw new IllegalArgumentException("Not allowed to delete the code of a collection"); + } + } + } + + /** + * Replaced and converted entities cannot be updated or restored. Also, they can't be replaced or + * converted in an update + */ + protected void checkReplacedEntitiesUpdate(T newEntity, T oldEntity) { + if (newEntity instanceof Institution) { + Institution newInstitution = (Institution) newEntity; + Institution oldInstitution = (Institution) oldEntity; + + if (oldInstitution.getReplacedBy() != null + || oldInstitution.getConvertedToCollection() != null) { + throw new IllegalArgumentException( + "Not allowed to update a replaced or converted institution"); + } else if (newInstitution.getReplacedBy() != null + || newInstitution.getConvertedToCollection() != null) { + throw new IllegalArgumentException( + "Not allowed to replace or convert an institution while updating"); + } + } else if (newEntity instanceof Collection) { + Collection newCollection = (Collection) newEntity; + Collection oldCollection = (Collection) oldEntity; + + if (oldCollection.getReplacedBy() != null) { + throw new IllegalArgumentException("Not allowed to update a replaced collection"); + } else if (newCollection.getReplacedBy() != null) { + throw new IllegalArgumentException("Not allowed to replace a collection while updating"); + } + } + } + + protected abstract void update(T entity); +} diff --git a/registry-service/src/main/java/org/gbif/registry/service/collections/DefaultCollectionService.java b/registry-service/src/main/java/org/gbif/registry/service/collections/DefaultCollectionService.java new file mode 100644 index 0000000000..985ccd50ab --- /dev/null +++ b/registry-service/src/main/java/org/gbif/registry/service/collections/DefaultCollectionService.java @@ -0,0 +1,25 @@ +package org.gbif.registry.service.collections; + +import org.gbif.api.model.collections.Collection; +import org.gbif.registry.persistence.mapper.IdentifierMapper; +import org.gbif.registry.persistence.mapper.MachineTagMapper; +import org.gbif.registry.persistence.mapper.TagMapper; +import org.gbif.registry.persistence.mapper.collections.AddressMapper; +import org.gbif.registry.persistence.mapper.collections.BaseMapper; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DefaultCollectionService extends ExtendedCollectionService { + + @Autowired + protected DefaultCollectionService( + BaseMapper baseMapper, + AddressMapper addressMapper, + MachineTagMapper machineTagMapper, + TagMapper tagMapper, + IdentifierMapper identifierMapper) { + super(baseMapper, addressMapper, machineTagMapper, tagMapper, identifierMapper); + } +} diff --git a/registry-service/src/main/java/org/gbif/registry/service/collections/DefaultInstitutionService.java b/registry-service/src/main/java/org/gbif/registry/service/collections/DefaultInstitutionService.java new file mode 100644 index 0000000000..b4e662e34c --- /dev/null +++ b/registry-service/src/main/java/org/gbif/registry/service/collections/DefaultInstitutionService.java @@ -0,0 +1,25 @@ +package org.gbif.registry.service.collections; + +import org.gbif.api.model.collections.Institution; +import org.gbif.registry.persistence.mapper.IdentifierMapper; +import org.gbif.registry.persistence.mapper.MachineTagMapper; +import org.gbif.registry.persistence.mapper.TagMapper; +import org.gbif.registry.persistence.mapper.collections.AddressMapper; +import org.gbif.registry.persistence.mapper.collections.BaseMapper; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DefaultInstitutionService extends ExtendedCollectionService { + + @Autowired + protected DefaultInstitutionService( + BaseMapper baseMapper, + AddressMapper addressMapper, + MachineTagMapper machineTagMapper, + TagMapper tagMapper, + IdentifierMapper identifierMapper) { + super(baseMapper, addressMapper, machineTagMapper, tagMapper, identifierMapper); + } +} diff --git a/registry-service/src/main/java/org/gbif/registry/service/collections/DefaultPersonService.java b/registry-service/src/main/java/org/gbif/registry/service/collections/DefaultPersonService.java new file mode 100644 index 0000000000..27741a808b --- /dev/null +++ b/registry-service/src/main/java/org/gbif/registry/service/collections/DefaultPersonService.java @@ -0,0 +1,21 @@ +package org.gbif.registry.service.collections; + +import org.gbif.api.model.collections.Person; +import org.gbif.registry.persistence.mapper.collections.BaseMapper; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DefaultPersonService extends BaseCollectionsService { + + @Autowired + protected DefaultPersonService(BaseMapper baseMapper) { + super(baseMapper); + } + + @Override + protected void update(Person entity) { + // TODO + } +} diff --git a/registry-service/src/main/java/org/gbif/registry/service/collections/ExtendedCollectionService.java b/registry-service/src/main/java/org/gbif/registry/service/collections/ExtendedCollectionService.java new file mode 100644 index 0000000000..c49db8b2a1 --- /dev/null +++ b/registry-service/src/main/java/org/gbif/registry/service/collections/ExtendedCollectionService.java @@ -0,0 +1,141 @@ +package org.gbif.registry.service.collections; + +import org.gbif.api.model.collections.Address; +import org.gbif.api.model.collections.CollectionEntity; +import org.gbif.api.model.collections.Contactable; +import org.gbif.api.model.collections.OccurrenceMappeable; +import org.gbif.api.model.registry.Commentable; +import org.gbif.api.model.registry.Identifiable; +import org.gbif.api.model.registry.Identifier; +import org.gbif.api.model.registry.MachineTag; +import org.gbif.api.model.registry.MachineTaggable; +import org.gbif.api.model.registry.Tag; +import org.gbif.api.model.registry.Taggable; +import org.gbif.registry.persistence.mapper.IdentifierMapper; +import org.gbif.registry.persistence.mapper.MachineTagMapper; +import org.gbif.registry.persistence.mapper.TagMapper; +import org.gbif.registry.persistence.mapper.collections.AddressMapper; +import org.gbif.registry.persistence.mapper.collections.BaseMapper; + +import java.util.UUID; + +import static com.google.common.base.Preconditions.checkArgument; + +// TODO: implement services +public class ExtendedCollectionService< + T extends + CollectionEntity & Taggable & Identifiable & MachineTaggable & Contactable & Commentable + & OccurrenceMappeable> + extends BaseCollectionsService { + + private final AddressMapper addressMapper; + private final BaseMapper baseMapper; + private final MachineTagMapper machineTagMapper; + private final TagMapper tagMapper; + private final IdentifierMapper identifierMapper; + + protected ExtendedCollectionService( + BaseMapper baseMapper, + AddressMapper addressMapper, + MachineTagMapper machineTagMapper, + TagMapper tagMapper, + IdentifierMapper identifierMapper) { + super(baseMapper); + this.addressMapper = addressMapper; + this.baseMapper = baseMapper; + this.machineTagMapper = machineTagMapper; + this.tagMapper = tagMapper; + this.identifierMapper = identifierMapper; + } + + public UUID create(T entity) { + checkArgument(entity.getKey() == null, "Unable to create an entity which already has a key"); + preCreate(entity); + + if (entity.getAddress() != null) { + addressMapper.create(entity.getAddress()); + } + + if (entity.getMailingAddress() != null) { + addressMapper.create(entity.getMailingAddress()); + } + + entity.setKey(UUID.randomUUID()); + baseMapper.create(entity); + + if (!entity.getMachineTags().isEmpty()) { + for (MachineTag machineTag : entity.getMachineTags()) { + machineTag.setCreatedBy(entity.getCreatedBy()); + machineTagMapper.createMachineTag(machineTag); + baseMapper.addMachineTag(entity.getKey(), machineTag.getKey()); + } + } + + if (!entity.getTags().isEmpty()) { + for (Tag tag : entity.getTags()) { + tag.setCreatedBy(entity.getCreatedBy()); + tagMapper.createTag(tag); + baseMapper.addTag(entity.getKey(), tag.getKey()); + } + } + + if (!entity.getIdentifiers().isEmpty()) { + for (Identifier identifier : entity.getIdentifiers()) { + identifier.setCreatedBy(entity.getCreatedBy()); + identifierMapper.createIdentifier(identifier); + baseMapper.addIdentifier(entity.getKey(), identifier.getKey()); + } + } + + return entity.getKey(); + } + + public void update(T entity) { + preUpdate(entity); + T entityOld = get(entity.getKey()); + checkArgument(entityOld != null, "Entity doesn't exist"); + checkCodeUpdate(entity, entityOld); + checkReplacedEntitiesUpdate(entity, entityOld); + + if (entityOld.getDeleted() != null) { + // if it's deleted we only allow to update it if we undelete it + checkArgument( + entity.getDeleted() == null, + "Unable to update a previously deleted entity unless you clear the deletion timestamp"); + } else { + // not allowed to delete when updating + checkArgument(entity.getDeleted() == null, "Can't delete an entity when updating"); + } + + // update mailing address + updateAddress(entity.getMailingAddress(), entityOld.getMailingAddress()); + + // update address + updateAddress(entity.getAddress(), entityOld.getAddress()); + + // update entity + baseMapper.update(entity); + + // check if we can delete the mailing address + if (entity.getMailingAddress() == null && entityOld.getMailingAddress() != null) { + addressMapper.delete(entityOld.getMailingAddress().getKey()); + } + + // check if we can delete the address + if (entity.getAddress() == null && entityOld.getAddress() != null) { + addressMapper.delete(entityOld.getAddress().getKey()); + } + } + + private void updateAddress(Address newAddress, Address oldAddress) { + if (newAddress != null) { + if (oldAddress == null) { + checkArgument( + newAddress.getKey() == null, "Unable to create an address which already has a key"); + addressMapper.create(newAddress); + } else { + addressMapper.update(newAddress); + } + } + } +} diff --git a/registry-service/src/main/java/org/gbif/registry/service/collections/merge/BaseMergeService.java b/registry-service/src/main/java/org/gbif/registry/service/collections/merge/BaseMergeService.java index 88fdd1b3e7..3106f5f9a8 100644 --- a/registry-service/src/main/java/org/gbif/registry/service/collections/merge/BaseMergeService.java +++ b/registry-service/src/main/java/org/gbif/registry/service/collections/merge/BaseMergeService.java @@ -45,7 +45,7 @@ public abstract class BaseMergeService< T extends CollectionEntity & Identifiable & MachineTaggable & OccurrenceMappeable & Contactable & Taggable & Commentable> - implements MergeService { + implements MergeService { protected final BaseMapper baseMapper; protected final MergeableMapper mergeableMapper; @@ -116,8 +116,9 @@ && isIDigBioRecord(replacement)) { checkMergeExtraPreconditions(entityToReplace, replacement); // delete and set the replacement - entityToReplace.setModifiedBy(authentication.getName()); - baseMapper.update(entityToReplace); + // TODO: test that this can be deleted +// entityToReplace.setModifiedBy(authentication.getName()); +// baseMapper.update(entityToReplace); mergeableMapper.replace(entityToReplaceKey, replacementKey); // merge entity fields diff --git a/registry-service/src/main/java/org/gbif/registry/service/collections/merge/MergeService.java b/registry-service/src/main/java/org/gbif/registry/service/collections/merge/MergeService.java index 13b43c2e9b..a865efa4a9 100644 --- a/registry-service/src/main/java/org/gbif/registry/service/collections/merge/MergeService.java +++ b/registry-service/src/main/java/org/gbif/registry/service/collections/merge/MergeService.java @@ -15,9 +15,11 @@ */ package org.gbif.registry.service.collections.merge; +import org.gbif.api.model.collections.CollectionEntity; + import java.util.UUID; -public interface MergeService { +public interface MergeService { void merge(UUID entityToReplaceKey, UUID replacementKey); } diff --git a/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/BaseChangeSuggestionService.java b/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/BaseChangeSuggestionService.java new file mode 100644 index 0000000000..6211f03853 --- /dev/null +++ b/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/BaseChangeSuggestionService.java @@ -0,0 +1,443 @@ +package org.gbif.registry.service.collections.suggestions; + +import org.gbif.api.model.collections.Address; +import org.gbif.api.model.collections.Collection; +import org.gbif.api.model.collections.CollectionEntity; +import org.gbif.api.model.collections.Contactable; +import org.gbif.api.model.collections.EntityType; +import org.gbif.api.model.collections.Institution; +import org.gbif.api.model.collections.OccurrenceMappeable; +import org.gbif.api.model.collections.suggestions.Change; +import org.gbif.api.model.collections.suggestions.ChangeSuggestion; +import org.gbif.api.model.collections.suggestions.ChangeSuggestionService; +import org.gbif.api.model.collections.suggestions.Status; +import org.gbif.api.model.collections.suggestions.Type; +import org.gbif.api.model.common.paging.Pageable; +import org.gbif.api.model.common.paging.PagingRequest; +import org.gbif.api.model.common.paging.PagingResponse; +import org.gbif.api.model.registry.Commentable; +import org.gbif.api.model.registry.Identifiable; +import org.gbif.api.model.registry.MachineTaggable; +import org.gbif.api.model.registry.Taggable; +import org.gbif.api.vocabulary.Country; +import org.gbif.registry.persistence.mapper.collections.BaseMapper; +import org.gbif.registry.persistence.mapper.collections.ChangeSuggestionMapper; +import org.gbif.registry.persistence.mapper.collections.dto.ChangeDto; +import org.gbif.registry.persistence.mapper.collections.dto.ChangeSuggestionDto; +import org.gbif.registry.service.collections.ExtendedCollectionService; +import org.gbif.registry.service.collections.merge.MergeService; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import static com.google.common.base.Preconditions.checkArgument; + +public abstract class BaseChangeSuggestionService< + T extends + CollectionEntity & Taggable & Identifiable & MachineTaggable & Commentable & Contactable + & OccurrenceMappeable> + implements ChangeSuggestionService { + + private static final Logger LOG = LoggerFactory.getLogger(BaseChangeSuggestionService.class); + + private static final Set FIELDS_TO_IGNORE = + new HashSet<>( + Arrays.asList( + "tags", + "identifiers", + "contacts", + "machineTags", + "comments", + "occurrenceMappings", + "replacedBy", + "createdBy", + "modifiedBy", + "created", + "modified", + "key", + "convertedToCollection")); + + private final ChangeSuggestionMapper changeSuggestionMapper; + private final BaseMapper baseMapper; + private final MergeService mergeService; + private final ExtendedCollectionService extendedCollectionService; + private final Class clazz; + private final ObjectMapper objectMapper; + private EntityType entityType; + + protected BaseChangeSuggestionService( + ChangeSuggestionMapper changeSuggestionMapper, + BaseMapper baseMapper, + MergeService mergeService, + ExtendedCollectionService extendedCollectionService, + Class clazz, + ObjectMapper objectMapper) { + this.changeSuggestionMapper = changeSuggestionMapper; + this.baseMapper = baseMapper; + this.mergeService = mergeService; + this.extendedCollectionService = extendedCollectionService; + this.clazz = clazz; + this.objectMapper = objectMapper; + + if (clazz == Institution.class) { + entityType = EntityType.INSTITUTION; + } else if (clazz == Collection.class) { + entityType = EntityType.COLLECTION; + } + } + + @Override + public int createChangeSuggestion(ChangeSuggestion changeSuggestion) { + checkArgument(!changeSuggestion.getComments().isEmpty(), "A comment is required"); + + if (changeSuggestion.getType() == Type.CREATE) { + return createNewEntitySuggestion(changeSuggestion); + } + if (changeSuggestion.getType() == Type.UPDATE) { + return createUpdateSuggestion(changeSuggestion); + } + if (changeSuggestion.getType() == Type.DELETE) { + return createDeleteSuggestion(changeSuggestion); + } + if (changeSuggestion.getType() == Type.MERGE) { + return createMergeSuggestion(changeSuggestion); + } + if (changeSuggestion.getType() == Type.CONVERSION_TO_COLLECTION) { + return createConvertToCollectionSuggestion(changeSuggestion); + } + + throw new IllegalArgumentException("Invalid suggestion type: " + changeSuggestion.getType()); + } + + protected int createUpdateSuggestion(ChangeSuggestion changeSuggestion) { + checkArgument(changeSuggestion.getEntityKey() != null); + checkArgument(changeSuggestion.getSuggestedEntity() != null); + + ChangeSuggestionDto dto = createBaseChangeSuggestionDto(changeSuggestion); + dto.setCountry(getCountry(changeSuggestion.getSuggestedEntity())); + dto.setSuggestedEntity(toJson(changeSuggestion.getSuggestedEntity())); + + T currentEntity = baseMapper.get(changeSuggestion.getEntityKey()); + dto.setChanges(extractChanges(changeSuggestion.getSuggestedEntity(), currentEntity)); + + changeSuggestionMapper.create(dto); + + return dto.getKey(); + } + + protected int createNewEntitySuggestion(ChangeSuggestion changeSuggestion) { + checkArgument(changeSuggestion.getSuggestedEntity() != null); + + ChangeSuggestionDto dto = createBaseChangeSuggestionDto(changeSuggestion); + dto.setCountry(getCountry(changeSuggestion.getSuggestedEntity())); + dto.setSuggestedEntity(toJson(changeSuggestion.getSuggestedEntity())); + + changeSuggestionMapper.create(dto); + return dto.getKey(); + } + + protected int createDeleteSuggestion(ChangeSuggestion changeSuggestion) { + checkArgument(changeSuggestion.getEntityKey() != null); + + ChangeSuggestionDto dto = createBaseChangeSuggestionDto(changeSuggestion); + + T currentEntity = baseMapper.get(changeSuggestion.getEntityKey()); + dto.setCountry(getCountry(currentEntity)); + + changeSuggestionMapper.create(dto); + return dto.getKey(); + } + + protected int createMergeSuggestion(ChangeSuggestion changeSuggestion) { + checkArgument(changeSuggestion.getEntityKey() != null); + checkArgument(changeSuggestion.getMergeTargetKey() != null); + + ChangeSuggestionDto dto = createBaseChangeSuggestionDto(changeSuggestion); + dto.setMergeTargetKey(changeSuggestion.getMergeTargetKey()); + + T currentEntity = baseMapper.get(changeSuggestion.getEntityKey()); + dto.setCountry(getCountry(currentEntity)); + + changeSuggestionMapper.create(dto); + return dto.getKey(); + } + + @Override + public void updateChangeSuggestion(ChangeSuggestion updatedChangeSuggestion) { + ChangeSuggestionDto dto = changeSuggestionMapper.get(updatedChangeSuggestion.getKey()); + + checkArgument( + updatedChangeSuggestion.getComments().size() > dto.getComments().size(), + "A comment is required"); + + if (dto.getType() == Type.CREATE || dto.getType() == Type.UPDATE) { + // we do this to update the suggested entity with the current state and minimize the risk of + // having race conditions + ChangeSuggestion changeSuggestion = dtoToChangeSuggestion(dto); + + Set newChanges = + extractChanges( + updatedChangeSuggestion.getSuggestedEntity(), changeSuggestion.getSuggestedEntity()); + dto.getChanges().addAll(newChanges); + dto.setSuggestedEntity(toJson(updatedChangeSuggestion.getSuggestedEntity())); + } + + dto.setComments(updatedChangeSuggestion.getComments()); + dto.setModifiedBy(getUsername()); + changeSuggestionMapper.update(dto); + } + + @Override + public void discardChangeSuggestion(int key) { + ChangeSuggestionDto dto = changeSuggestionMapper.get(key); + dto.setStatus(Status.DISCARDED); + dto.setDiscarded(new Date()); + dto.setDiscardedBy(getUsername()); + dto.setModifiedBy(getUsername()); + changeSuggestionMapper.update(dto); + } + + public void applyChangeSuggestion(int suggestionKey) { + ChangeSuggestionDto dto = changeSuggestionMapper.get(suggestionKey); + ChangeSuggestion changeSuggestion = dtoToChangeSuggestion(dto); + if (dto.getType() == Type.CREATE) { + UUID createdEntity = extendedCollectionService.create(changeSuggestion.getSuggestedEntity()); + dto.setEntityKey(createdEntity); + } else if (dto.getType() == Type.UPDATE) { + extendedCollectionService.update(changeSuggestion.getSuggestedEntity()); + } else if (dto.getType() == Type.DELETE) { + extendedCollectionService.delete(changeSuggestion.getEntityKey()); + } else if (dto.getType() == Type.MERGE) { + mergeService.merge(changeSuggestion.getEntityKey(), changeSuggestion.getMergeTargetKey()); + } else if (dto.getType() == Type.CONVERSION_TO_COLLECTION) { + applyConversionToCollection(dto); + } + + dto.setStatus(Status.APPLIED); + dto.setModifiedBy(getUsername()); + dto.setApplied(new Date()); + dto.setAppliedBy(getUsername()); + changeSuggestionMapper.update(dto); + } + + @Override + public PagingResponse> list( + @Nullable Status status, + @Nullable Type type, + @Nullable Country country, + @Nullable String proposedBy, + @Nullable UUID entityKey, + @Nullable Pageable pageable) { + Pageable page = pageable == null ? new PagingRequest() : pageable; + + List dtos = + changeSuggestionMapper.list( + status, + type, + entityType, + country, + newEmptyChangeSuggestion().getProposedBy(), + entityKey, + page); + + long count = + changeSuggestionMapper.count( + status, + type, + entityType, + country, + newEmptyChangeSuggestion().getProposedBy(), + entityKey); + + List> changeSuggestions = + dtos.stream().map(this::dtoToChangeSuggestion).collect(Collectors.toList()); + + return new PagingResponse<>(page, count, changeSuggestions); + } + + private Set extractChanges(T suggestedEntity, T currentEntity) { + Set changes = new HashSet<>(); + for (Field field : clazz.getDeclaredFields()) { + if (FIELDS_TO_IGNORE.contains(field.getName())) { + continue; + } + try { + String methodPrefix = field.getType().isAssignableFrom(Boolean.TYPE) ? "is" : "get"; + Object suggestedValue = + clazz + .getMethod( + methodPrefix + + field.getName().substring(0, 1).toUpperCase() + + field.getName().substring(1)) + .invoke(suggestedEntity); + + Object previousValue = + clazz + .getMethod( + methodPrefix + + field.getName().substring(0, 1).toUpperCase() + + field.getName().substring(1)) + .invoke(currentEntity); + + if (!Objects.equals(suggestedValue, previousValue)) { + ChangeDto changeDto = new ChangeDto(); + changeDto.setFieldName(field.getName()); + changeDto.setFieldType(field.getType()); + + if (field.getGenericType() instanceof ParameterizedType) { + changeDto.setFieldGenericTypeName( + ((ParameterizedType) field.getGenericType()) + .getActualTypeArguments()[0].getTypeName()); + } + + changeDto.setPrevious(previousValue); + changeDto.setSuggested(suggestedValue); + changeDto.setAuthor(getUsername()); + changeDto.setCreated(new Date()); + changes.add(changeDto); + } + } catch (Exception e) { + throw new IllegalStateException("Error while comparing field values", e); + } + } + return changes; + } + + protected ChangeSuggestionDto createBaseChangeSuggestionDto( + ChangeSuggestion changeSuggestion) { + ChangeSuggestionDto dto = new ChangeSuggestionDto(); + dto.setEntityKey(changeSuggestion.getEntityKey()); + dto.setStatus(Status.PENDING); + dto.setType(changeSuggestion.getType()); + dto.setComments(changeSuggestion.getComments()); + dto.setEntityType(entityType); + dto.setProposedBy(changeSuggestion.getProposedBy()); + dto.setModifiedBy(getUsername()); + return dto; + } + + protected Country getCountry(T entity) { + Address physicalAddress = null; + Address mailingAddress = null; + if (entity instanceof Institution) { + Institution institution = (Institution) entity; + physicalAddress = institution.getAddress(); + mailingAddress = institution.getMailingAddress(); + } else if (entity instanceof Collection) { + Collection collection = (Collection) entity; + physicalAddress = collection.getAddress(); + mailingAddress = collection.getMailingAddress(); + } + + if (physicalAddress != null && physicalAddress.getCountry() != null) { + return physicalAddress.getCountry(); + } else if (mailingAddress != null) { + return mailingAddress.getCountry(); + } else { + return null; + } + } + + protected ChangeSuggestion dtoToChangeSuggestion(ChangeSuggestionDto dto) { + ChangeSuggestion suggestion = newEmptyChangeSuggestion(); + suggestion.setKey(dto.getKey()); + suggestion.setStatus(dto.getStatus()); + suggestion.setType(dto.getType()); + suggestion.setCountry(dto.getCountry()); + suggestion.setAppliedBy(dto.getAppliedBy()); + suggestion.setApplied(dto.getApplied()); + suggestion.setDiscarded(dto.getDiscarded()); + suggestion.setDiscardedBy(dto.getDiscardedBy()); + suggestion.setEntityKey(dto.getEntityKey()); + suggestion.setComments(dto.getComments()); + suggestion.setModified(dto.getModified()); + suggestion.setModifiedBy(dto.getModifiedBy()); + suggestion.setProposed(dto.getProposed()); + suggestion.setProposedBy(dto.getProposedBy()); + suggestion.setMergeTargetKey(dto.getMergeTargetKey()); + + // changes conversion + suggestion.setChanges( + dto.getChanges().stream() + .map( + ch -> { + Change change = new Change(); + change.setField(ch.getFieldName()); + change.setPrevious(ch.getPrevious()); + change.setSuggested(ch.getSuggested()); + change.setAuthor(ch.getAuthor()); + change.setCreated(ch.getCreated()); + return change; + }) + .collect(Collectors.toList())); + + // merge view + try { + if (dto.getType() == Type.CREATE) { + suggestion.setSuggestedEntity(objectMapper.readValue(dto.getSuggestedEntity(), clazz)); + } else if (dto.getType() == Type.UPDATE) { + T entity = baseMapper.get(dto.getEntityKey()); + + // we sort the changes because we can have multiple changes in the same field. We are only + // interested in the last change so we want to apply them in order (older changes could be + // discarded but it doesn't matter much since we're not expecting too many changes in the + // same field - the original and another one if the reviewer wants to change it) + List changesSorted = + dto.getChanges().stream() + .sorted(Comparator.comparing(ChangeDto::getCreated)) + .collect(Collectors.toList()); + for (ChangeDto changeDto : changesSorted) { + clazz + .getMethod( + "set" + + changeDto.getFieldName().substring(0, 1).toUpperCase() + + changeDto.getFieldName().substring(1), + changeDto.getFieldType()) + .invoke(entity, changeDto.getSuggested()); + } + suggestion.setSuggestedEntity(entity); + } + } catch (Exception e) { + throw new IllegalStateException("Error while applying suggested change to the merge view", e); + } + + return suggestion; + } + + protected String getUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication.getName(); + } + + protected String toJson(T entity) { + try { + return objectMapper.writeValueAsString(entity); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Cannot serialize entity", e); + } + } + + protected abstract ChangeSuggestion newEmptyChangeSuggestion(); + + protected abstract int createConvertToCollectionSuggestion(ChangeSuggestion changeSuggestion); + + protected abstract void applyConversionToCollection(ChangeSuggestionDto dto); +} diff --git a/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/CollectionChangeSuggestionService.java b/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/CollectionChangeSuggestionService.java new file mode 100644 index 0000000000..ce32855a06 --- /dev/null +++ b/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/CollectionChangeSuggestionService.java @@ -0,0 +1,70 @@ +package org.gbif.registry.service.collections.suggestions; + +import org.gbif.api.model.collections.Collection; +import org.gbif.api.model.collections.EntityType; +import org.gbif.api.model.collections.suggestions.ChangeSuggestion; +import org.gbif.api.model.collections.suggestions.CollectionChangeSuggestion; +import org.gbif.api.model.collections.suggestions.Status; +import org.gbif.api.model.collections.suggestions.Type; +import org.gbif.api.model.common.paging.Pageable; +import org.gbif.api.model.common.paging.PagingRequest; +import org.gbif.api.model.common.paging.PagingResponse; +import org.gbif.api.vocabulary.Country; +import org.gbif.registry.persistence.mapper.collections.ChangeSuggestionMapper; +import org.gbif.registry.persistence.mapper.collections.CollectionMapper; +import org.gbif.registry.persistence.mapper.collections.dto.ChangeSuggestionDto; +import org.gbif.registry.service.collections.DefaultCollectionService; +import org.gbif.registry.service.collections.merge.CollectionMergeService; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@Service +public class CollectionChangeSuggestionService extends BaseChangeSuggestionService { + + private final ChangeSuggestionMapper changeSuggestionMapper; + + @Autowired + public CollectionChangeSuggestionService( + ChangeSuggestionMapper changeSuggestionMapper, + CollectionMapper collectionMapper, + DefaultCollectionService collectionService, // TODO: interfaces + CollectionMergeService collectionMergeService, + ObjectMapper objectMapper) { + super( + changeSuggestionMapper, + collectionMapper, + collectionMergeService, + collectionService, + Collection.class, + objectMapper); + this.changeSuggestionMapper = changeSuggestionMapper; + } + + @Override + public CollectionChangeSuggestion getChangeSuggestion(int key) { + return (CollectionChangeSuggestion) dtoToChangeSuggestion(changeSuggestionMapper.get(key)); + } + + @Override + protected CollectionChangeSuggestion newEmptyChangeSuggestion() { + return new CollectionChangeSuggestion(); + } + + @Override + protected int createConvertToCollectionSuggestion(ChangeSuggestion changeSuggestion) { + throw new UnsupportedOperationException(); + } + + @Override + protected void applyConversionToCollection(ChangeSuggestionDto dto) { + throw new UnsupportedOperationException(); + } +} diff --git a/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/InstitutionChangeSuggestionService.java b/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/InstitutionChangeSuggestionService.java new file mode 100644 index 0000000000..28bb2878c2 --- /dev/null +++ b/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/InstitutionChangeSuggestionService.java @@ -0,0 +1,96 @@ +package org.gbif.registry.service.collections.suggestions; + +import org.gbif.api.model.collections.Institution; +import org.gbif.api.model.collections.suggestions.ChangeSuggestion; +import org.gbif.api.model.collections.suggestions.InstitutionChangeSuggestion; +import org.gbif.api.model.collections.suggestions.Type; +import org.gbif.registry.persistence.mapper.collections.ChangeSuggestionMapper; +import org.gbif.registry.persistence.mapper.collections.InstitutionMapper; +import org.gbif.registry.persistence.mapper.collections.dto.ChangeSuggestionDto; +import org.gbif.registry.service.collections.DefaultInstitutionService; +import org.gbif.registry.service.collections.merge.InstitutionMergeService; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static com.google.common.base.Preconditions.checkArgument; + +@Service +public class InstitutionChangeSuggestionService extends BaseChangeSuggestionService { + + private static final Logger LOG = + LoggerFactory.getLogger(InstitutionChangeSuggestionService.class); + + private final ChangeSuggestionMapper changeSuggestionMapper; + private final InstitutionMapper institutionMapper; + private final InstitutionMergeService institutionMergeService; + + @Autowired + public InstitutionChangeSuggestionService( + ChangeSuggestionMapper changeSuggestionMapper, + InstitutionMapper institutionMapper, + DefaultInstitutionService institutionService, // TODO: interfaces + InstitutionMergeService institutionMergeService, + ObjectMapper objectMapper) { + super( + changeSuggestionMapper, + institutionMapper, + institutionMergeService, + institutionService, + Institution.class, + objectMapper); + this.changeSuggestionMapper = changeSuggestionMapper; + this.institutionMapper = institutionMapper; + this.institutionMergeService = institutionMergeService; + } + + protected int createConvertToCollectionSuggestion( + ChangeSuggestion changeSuggestion) { + checkArgument(changeSuggestion.getEntityKey() != null); + + InstitutionChangeSuggestion institutionChangeSuggestion = + (InstitutionChangeSuggestion) changeSuggestion; + + ChangeSuggestionDto dto = createBaseChangeSuggestionDto(changeSuggestion); + dto.setType(Type.CONVERSION_TO_COLLECTION); + dto.setInstitutionConvertedCollection( + institutionChangeSuggestion.getInstitutionForConvertedCollection()); + dto.setNameNewInstitutionConvertedCollection( + institutionChangeSuggestion.getNameForNewInstitutionForConvertedCollection()); + + Institution currentEntity = institutionMapper.get(changeSuggestion.getEntityKey()); + dto.setCountry(getCountry(currentEntity)); + + changeSuggestionMapper.create(dto); + return dto.getKey(); + } + + @Override + public InstitutionChangeSuggestion getChangeSuggestion(int key) { + ChangeSuggestionDto dto = changeSuggestionMapper.get(key); + + InstitutionChangeSuggestion suggestion = + (InstitutionChangeSuggestion) dtoToChangeSuggestion(dto); + suggestion.setInstitutionForConvertedCollection(dto.getInstitutionConvertedCollection()); + suggestion.setNameForNewInstitutionForConvertedCollection( + dto.getNameNewInstitutionConvertedCollection()); + + return suggestion; + } + + protected void applyConversionToCollection(ChangeSuggestionDto dto) { + institutionMergeService.convertToCollection( + dto.getEntityKey(), + dto.getInstitutionConvertedCollection(), + dto.getNameNewInstitutionConvertedCollection()); + } + + @Override + protected InstitutionChangeSuggestion newEmptyChangeSuggestion() { + return new InstitutionChangeSuggestion(); + } +} diff --git a/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/BaseCollectionEntityResource.java b/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/BaseCollectionEntityResource.java index f2d8f47acf..7cc113349d 100644 --- a/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/BaseCollectionEntityResource.java +++ b/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/BaseCollectionEntityResource.java @@ -45,6 +45,7 @@ import org.gbif.registry.persistence.mapper.MachineTagMapper; import org.gbif.registry.persistence.mapper.TagMapper; import org.gbif.registry.persistence.mapper.collections.BaseMapper; +import org.gbif.registry.service.collections.BaseCollectionsService; import java.util.List; import java.util.UUID; @@ -89,6 +90,7 @@ public abstract class BaseCollectionEntityResource< private final CommentMapper commentMapper; private final EventManager eventManager; private final WithMyBatis withMyBatis; + private final BaseCollectionsService baseCollectionsService; protected BaseCollectionEntityResource( BaseMapper baseMapper, @@ -98,7 +100,8 @@ protected BaseCollectionEntityResource( CommentMapper commentMapper, EventManager eventManager, Class objectClass, - WithMyBatis withMyBatis) { + WithMyBatis withMyBatis, + BaseCollectionsService baseCollectionsService) { this.baseMapper = baseMapper; this.tagMapper = tagMapper; this.machineTagMapper = machineTagMapper; @@ -107,6 +110,7 @@ protected BaseCollectionEntityResource( this.eventManager = eventManager; this.objectClass = objectClass; this.withMyBatis = withMyBatis; + this.baseCollectionsService = baseCollectionsService; } public void preCreate(T entity) { @@ -121,22 +125,16 @@ public void preCreate(T entity) { @Secured({GRSCICOLL_ADMIN_ROLE, GRSCICOLL_EDITOR_ROLE}) @Override public void delete(@PathVariable UUID key) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - T entityToDelete = get(key); - checkArgument(entityToDelete != null, "Entity to delete doesn't exist"); - - entityToDelete.setModifiedBy(authentication.getName()); - update(entityToDelete); + baseCollectionsService.delete(key); - baseMapper.delete(key); - eventManager.post(DeleteCollectionEntityEvent.newInstance(entityToDelete, objectClass)); + // TODO: move this to the service?? + eventManager.post(DeleteCollectionEntityEvent.newInstance(baseCollectionsService.get(key), objectClass)); } @Nullable @Override public T get(@PathVariable UUID key) { - return baseMapper.get(key); + return baseCollectionsService.get(key); } public void preUpdate(T entity) { diff --git a/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/CollectionResource.java b/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/CollectionResource.java index 03a9032449..e88b73ce85 100644 --- a/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/CollectionResource.java +++ b/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/CollectionResource.java @@ -32,14 +32,15 @@ import org.gbif.registry.persistence.mapper.IdentifierMapper; import org.gbif.registry.persistence.mapper.MachineTagMapper; import org.gbif.registry.persistence.mapper.TagMapper; -import org.gbif.registry.persistence.mapper.collections.AddressMapper; import org.gbif.registry.persistence.mapper.collections.CollectionMapper; import org.gbif.registry.persistence.mapper.collections.OccurrenceMappingMapper; import org.gbif.registry.persistence.mapper.collections.dto.CollectionDto; import org.gbif.registry.persistence.mapper.collections.params.CollectionSearchParams; import org.gbif.registry.persistence.mapper.collections.params.DuplicatesSearchParams; +import org.gbif.registry.service.collections.DefaultCollectionService; import org.gbif.registry.service.collections.duplicates.DuplicatesService; import org.gbif.registry.service.collections.merge.CollectionMergeService; +import org.gbif.registry.service.collections.suggestions.CollectionChangeSuggestionService; import java.util.List; import java.util.UUID; @@ -69,10 +70,10 @@ public class CollectionResource extends ExtendedCollectionEntityResource extends BaseCollectionEntityResource implements ContactService, OccurrenceMappingService { - private final BaseMapper baseMapper; - private final AddressMapper addressMapper; private final ContactableMapper contactableMapper; - private final TagMapper tagMapper; - private final MachineTagMapper machineTagMapper; - private final IdentifierMapper identifierMapper; private final OccurrenceMappingMapper occurrenceMappingMapper; private final OccurrenceMappeableMapper occurrenceMappeableMapper; - private final MergeService mergeService; + private final MergeService mergeService; + private final ExtendedCollectionService extendedCollectionService; + private final ChangeSuggestionService changeSuggestionService; private final EventManager eventManager; private final Class objectClass; protected ExtendedCollectionEntityResource( BaseMapper baseMapper, - AddressMapper addressMapper, TagMapper tagMapper, IdentifierMapper identifierMapper, ContactableMapper contactableMapper, @@ -108,7 +113,9 @@ protected ExtendedCollectionEntityResource( CommentMapper commentMapper, OccurrenceMappingMapper occurrenceMappingMapper, OccurrenceMappeableMapper occurrenceMappeableMapper, - MergeService mergeService, + MergeService mergeService, + ExtendedCollectionService extendedCollectionService, + ChangeSuggestionService changeSuggestionService, EventManager eventManager, Class objectClass, WithMyBatis withMyBatis) { @@ -120,18 +127,16 @@ protected ExtendedCollectionEntityResource( commentMapper, eventManager, objectClass, - withMyBatis); - this.baseMapper = baseMapper; - this.addressMapper = addressMapper; + withMyBatis, + extendedCollectionService); this.contactableMapper = contactableMapper; - this.tagMapper = tagMapper; - this.machineTagMapper = machineTagMapper; - this.identifierMapper = identifierMapper; this.occurrenceMappingMapper = occurrenceMappingMapper; this.occurrenceMappeableMapper = occurrenceMappeableMapper; this.mergeService = mergeService; + this.changeSuggestionService = changeSuggestionService; this.eventManager = eventManager; this.objectClass = objectClass; + this.extendedCollectionService = extendedCollectionService; } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @@ -141,46 +146,10 @@ protected ExtendedCollectionEntityResource( @Secured({GRSCICOLL_ADMIN_ROLE, GRSCICOLL_EDITOR_ROLE}) @Override public UUID create(@RequestBody @Trim T entity) { - checkArgument(entity.getKey() == null, "Unable to create an entity which already has a key"); - preCreate(entity); - - if (entity.getAddress() != null) { - addressMapper.create(entity.getAddress()); - } - - if (entity.getMailingAddress() != null) { - addressMapper.create(entity.getMailingAddress()); - } - - entity.setKey(UUID.randomUUID()); - baseMapper.create(entity); - - if (!entity.getMachineTags().isEmpty()) { - for (MachineTag machineTag : entity.getMachineTags()) { - machineTag.setCreatedBy(entity.getCreatedBy()); - machineTagMapper.createMachineTag(machineTag); - baseMapper.addMachineTag(entity.getKey(), machineTag.getKey()); - } - } - - if (!entity.getTags().isEmpty()) { - for (Tag tag : entity.getTags()) { - tag.setCreatedBy(entity.getCreatedBy()); - tagMapper.createTag(tag); - baseMapper.addTag(entity.getKey(), tag.getKey()); - } - } - - if (!entity.getIdentifiers().isEmpty()) { - for (Identifier identifier : entity.getIdentifiers()) { - identifier.setCreatedBy(entity.getCreatedBy()); - identifierMapper.createIdentifier(identifier); - baseMapper.addIdentifier(entity.getKey(), identifier.getKey()); - } - } - + UUID key = extendedCollectionService.create(entity); + entity.setKey(key); eventManager.post(CreateCollectionEntityEvent.newInstance(entity, objectClass)); - return entity.getKey(); + return key; } @PutMapping( @@ -191,55 +160,12 @@ public UUID create(@RequestBody @Trim T entity) { @Secured({GRSCICOLL_ADMIN_ROLE, GRSCICOLL_EDITOR_ROLE}) @Override public void update(@RequestBody @Trim T entity) { - preUpdate(entity); T entityOld = get(entity.getKey()); - checkArgument(entityOld != null, "Entity doesn't exist"); - checkCodeUpdate(entity, entityOld); - checkReplacedEntitiesUpdate(entity, entityOld); - - if (entityOld.getDeleted() != null) { - // if it's deleted we only allow to update it if we undelete it - checkArgument( - entity.getDeleted() == null, - "Unable to update a previously deleted entity unless you clear the deletion timestamp"); - } else { - // not allowed to delete when updating - checkArgument(entity.getDeleted() == null, "Can't delete an entity when updating"); - } - - // update mailing address - updateAddress(entity.getMailingAddress(), entityOld.getMailingAddress()); - - // update address - updateAddress(entity.getAddress(), entityOld.getAddress()); - - // update entity - baseMapper.update(entity); - - // check if we can delete the mailing address - if (entity.getMailingAddress() == null && entityOld.getMailingAddress() != null) { - addressMapper.delete(entityOld.getMailingAddress().getKey()); - } - - // check if we can delete the address - if (entity.getAddress() == null && entityOld.getAddress() != null) { - addressMapper.delete(entityOld.getAddress().getKey()); - } - + extendedCollectionService.update(entity); T newEntity = get(entity.getKey()); - eventManager.post(UpdateCollectionEntityEvent.newInstance(newEntity, entityOld, objectClass)); - } - private void updateAddress(Address newAddress, Address oldAddress) { - if (newAddress != null) { - if (oldAddress == null) { - checkArgument( - newAddress.getKey() == null, "Unable to create an address which already has a key"); - addressMapper.create(newAddress); - } else { - addressMapper.update(newAddress); - } - } + // TODO: move this to service?? + eventManager.post(UpdateCollectionEntityEvent.newInstance(newEntity, entityOld, objectClass)); } @PostMapping( @@ -324,70 +250,45 @@ public void deleteOccurrenceMapping( @PostMapping(value = "{key}/merge") @Secured({GRSCICOLL_ADMIN_ROLE, IDIGBIO_GRSCICOLL_EDITOR_ROLE}) public void merge(@PathVariable("key") UUID entityKey, @RequestBody MergeParams params) { - mergeService.merge(entityKey, params.replacementEntityKey); + mergeService.merge(entityKey, params.getReplacementEntityKey()); } - private static class MergeParams { - private UUID replacementEntityKey; - - public UUID getReplacementEntityKey() { - return replacementEntityKey; - } - - public void setReplacementEntityKey(UUID replacementEntityKey) { - this.replacementEntityKey = replacementEntityKey; - } + @PostMapping(value = "changeSuggestion") + public int createChangeSuggestion(@RequestBody ChangeSuggestion createSuggestion) { + return changeSuggestionService.createChangeSuggestion(createSuggestion); } - /** - * Some iDigBio collections and institutions don't have code and we allow that in the DB but not - * in the API. - */ - protected void checkCodeUpdate(T newEntity, T oldEntity) { - if (newEntity instanceof Institution) { - Institution newInstitution = (Institution) newEntity; - Institution oldInstitution = (Institution) oldEntity; - - if (newInstitution.getCode() == null && oldInstitution.getCode() != null) { - throw new IllegalArgumentException("Not allowed to delete the code of an institution"); - } - } else if (newEntity instanceof Collection) { - Collection newCollection = (Collection) newEntity; - Collection oldCollection = (Collection) oldEntity; + @PutMapping(value = "changeSuggestion/{key}") + @Secured({GRSCICOLL_ADMIN_ROLE, GRSCICOLL_EDITOR_ROLE}) + public void updateChangeSuggestion(@RequestBody ChangeSuggestion createSuggestion) { + changeSuggestionService.updateChangeSuggestion(createSuggestion); + } - if (newCollection.getCode() == null && oldCollection.getCode() != null) { - throw new IllegalArgumentException("Not allowed to delete the code of a collection"); - } - } + @GetMapping(value = "changeSuggestion/{key}") + public ChangeSuggestion getChangeSuggestion(@PathVariable("key") int key) { + return changeSuggestionService.getChangeSuggestion(key); } - /** - * Replaced and converted entities cannot be updated or restored. Also, they can't be replaced or - * converted in an update - */ - protected void checkReplacedEntitiesUpdate(T newEntity, T oldEntity) { - if (newEntity instanceof Institution) { - Institution newInstitution = (Institution) newEntity; - Institution oldInstitution = (Institution) oldEntity; + @GetMapping(value = "changeSuggestion") + public PagingResponse> listChangeSuggestion( + @Nullable @RequestParam(value = "status", required = false) Status status, + @Nullable @RequestParam(value = "type", required = false) Type type, + @Nullable Country country, + @Nullable @RequestParam(value = "proposedBy", required = false) String proposedBy, + @Nullable @RequestParam(value = "entityKey", required = false) UUID entityKey, + @Nullable Pageable page) { + return changeSuggestionService.list(status, type, country, proposedBy, entityKey, page); + } - if (oldInstitution.getReplacedBy() != null - || oldInstitution.getConvertedToCollection() != null) { - throw new IllegalArgumentException( - "Not allowed to update a replaced or converted institution"); - } else if (newInstitution.getReplacedBy() != null - || newInstitution.getConvertedToCollection() != null) { - throw new IllegalArgumentException( - "Not allowed to replace or convert an institution while updating"); - } - } else if (newEntity instanceof Collection) { - Collection newCollection = (Collection) newEntity; - Collection oldCollection = (Collection) oldEntity; + @PutMapping(value = "changeSuggestion/{key}/discard") + @Secured({GRSCICOLL_ADMIN_ROLE, GRSCICOLL_EDITOR_ROLE}) + public void discardChangeSuggestion(@PathVariable("key") int key) { + changeSuggestionService.discardChangeSuggestion(key); + } - if (oldCollection.getReplacedBy() != null) { - throw new IllegalArgumentException("Not allowed to update a replaced collection"); - } else if (newCollection.getReplacedBy() != null) { - throw new IllegalArgumentException("Not allowed to replace a collection while updating"); - } - } + @PutMapping(value = "changeSuggestion/{key}/apply") + @Secured({GRSCICOLL_ADMIN_ROLE, GRSCICOLL_EDITOR_ROLE}) + public void applyChangeSuggestion(@PathVariable("key") int key) { + changeSuggestionService.applyChangeSuggestion(key); } } diff --git a/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/InstitutionResource.java b/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/InstitutionResource.java index 3be872959d..490ee33ab6 100644 --- a/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/InstitutionResource.java +++ b/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/InstitutionResource.java @@ -19,6 +19,7 @@ import org.gbif.api.model.collections.Institution; import org.gbif.api.model.collections.duplicates.DuplicatesRequest; import org.gbif.api.model.collections.duplicates.DuplicatesResult; +import org.gbif.api.model.collections.merge.ConvertToCollectionParams; import org.gbif.api.model.collections.request.InstitutionSearchRequest; import org.gbif.api.model.common.paging.Pageable; import org.gbif.api.model.common.paging.PagingRequest; @@ -31,13 +32,14 @@ import org.gbif.registry.persistence.mapper.IdentifierMapper; import org.gbif.registry.persistence.mapper.MachineTagMapper; import org.gbif.registry.persistence.mapper.TagMapper; -import org.gbif.registry.persistence.mapper.collections.AddressMapper; import org.gbif.registry.persistence.mapper.collections.InstitutionMapper; import org.gbif.registry.persistence.mapper.collections.OccurrenceMappingMapper; import org.gbif.registry.persistence.mapper.collections.params.DuplicatesSearchParams; import org.gbif.registry.persistence.mapper.collections.params.InstitutionSearchParams; +import org.gbif.registry.service.collections.DefaultInstitutionService; import org.gbif.registry.service.collections.duplicates.DuplicatesService; import org.gbif.registry.service.collections.merge.InstitutionMergeService; +import org.gbif.registry.service.collections.suggestions.InstitutionChangeSuggestionService; import java.util.List; import java.util.UUID; @@ -76,7 +78,6 @@ public class InstitutionResource extends ExtendedCollectionEntityResource suggest(@RequestParam(value = "q", required = fal public UUID convertToCollection( @PathVariable("key") UUID entityKey, @RequestBody ConvertToCollectionParams params) { return institutionMergeService.convertToCollection( - entityKey, params.institutionForNewCollectionKey, params.nameForNewInstitution); + entityKey, params.getInstitutionForNewCollectionKey(), params.getNameForNewInstitution()); } @GetMapping("possibleDuplicates") @@ -182,25 +186,4 @@ public DuplicatesResult findPossibleDuplicates(DuplicatesRequest request) { .excludeKeys(request.getExcludeKeys()) .build()); } - - private static final class ConvertToCollectionParams { - UUID institutionForNewCollectionKey; - String nameForNewInstitution; - - public UUID getInstitutionForNewCollectionKey() { - return institutionForNewCollectionKey; - } - - public void setInstitutionForNewCollectionKey(UUID institutionForNewCollectionKey) { - this.institutionForNewCollectionKey = institutionForNewCollectionKey; - } - - public String getNameForNewInstitution() { - return nameForNewInstitution; - } - - public void setNameForNewInstitution(String nameForNewInstitution) { - this.nameForNewInstitution = nameForNewInstitution; - } - } } diff --git a/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/PersonResource.java b/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/PersonResource.java index 74757264bd..c23c0f4097 100644 --- a/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/PersonResource.java +++ b/registry-ws/src/main/java/org/gbif/registry/ws/resources/collections/PersonResource.java @@ -37,6 +37,7 @@ import org.gbif.registry.persistence.mapper.TagMapper; import org.gbif.registry.persistence.mapper.collections.AddressMapper; import org.gbif.registry.persistence.mapper.collections.PersonMapper; +import org.gbif.registry.service.collections.DefaultPersonService; import java.util.List; import java.util.UUID; @@ -75,6 +76,7 @@ public class PersonResource extends BaseCollectionEntityResource impleme private final TagMapper tagMapper; private final MachineTagMapper machineTagMapper; private final EventManager eventManager; + private final DefaultPersonService personService; public PersonResource( PersonMapper personMapper, @@ -84,7 +86,8 @@ public PersonResource( MachineTagMapper machineTagMapper, CommentMapper commentMapper, EventManager eventManager, - WithMyBatis withMyBatis) { + WithMyBatis withMyBatis, + DefaultPersonService personService) { super( personMapper, tagMapper, @@ -93,13 +96,15 @@ public PersonResource( commentMapper, eventManager, Person.class, - withMyBatis); + withMyBatis, + personService); this.personMapper = personMapper; this.addressMapper = addressMapper; this.identifierMapper = identifierMapper; this.tagMapper = tagMapper; this.machineTagMapper = machineTagMapper; this.eventManager = eventManager; + this.personService = personService; } @GetMapping("{key}")