diff --git a/admin/query/pom.xml b/admin/query/pom.xml index 861a9a3f7..9fcbd8027 100644 --- a/admin/query/pom.xml +++ b/admin/query/pom.xml @@ -90,17 +90,17 @@ INSTRUCTION COVEREDRATIO - 0.71 + 0.75 BRANCH COVEREDRATIO - 0.68 + 0.74 COMPLEXITY COVEREDRATIO - 0.58 + 0.61 diff --git a/admin/query/src/main/java/org/codice/ditto/replication/admin/query/ReplicationUtils.java b/admin/query/src/main/java/org/codice/ditto/replication/admin/query/ReplicationUtils.java index c608d52f5..cd5696ee8 100644 --- a/admin/query/src/main/java/org/codice/ditto/replication/admin/query/ReplicationUtils.java +++ b/admin/query/src/main/java/org/codice/ditto/replication/admin/query/ReplicationUtils.java @@ -24,6 +24,7 @@ import org.codice.ditto.replication.admin.query.replications.fields.ReplicationField; import org.codice.ditto.replication.admin.query.sites.fields.ReplicationSiteField; import org.codice.ditto.replication.api.ReplicationException; +import org.codice.ditto.replication.api.ReplicationPersistenceException; import org.codice.ditto.replication.api.ReplicationStatus; import org.codice.ditto.replication.api.Replicator; import org.codice.ditto.replication.api.ReplicatorHistory; @@ -32,10 +33,14 @@ import org.codice.ditto.replication.api.data.ReplicatorConfig; import org.codice.ditto.replication.api.persistence.ReplicatorConfigManager; import org.codice.ditto.replication.api.persistence.SiteManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Utility class that does all the heavy lifting for the graphql operations */ public class ReplicationUtils { + private static final Logger LOGGER = LoggerFactory.getLogger(ReplicationUtils.class); + private static final long BYTES_PER_MB = 1024L * 1024L; private final SiteManager siteManager; @@ -71,12 +76,7 @@ public boolean siteExists(String name) { } public boolean siteIdExists(String id) { - try { - siteManager.get(id); - return true; - } catch (NotFoundException e) { - return false; - } + return siteManager.exists(id); } public ReplicationSiteField updateSite(String id, String name, AddressField address) { @@ -109,7 +109,7 @@ public boolean replicationConfigExists(String name) { } public boolean configExists(String id) { - return configManager.configExists(id); + return configManager.exists(id); } public ReplicationField createReplication( @@ -215,12 +215,21 @@ private ReplicationField getReplicationFieldForConfig(ReplicatorConfig config) { return field; } - public boolean deleteConfig(String id) { + public boolean markConfigDeleted(String id, boolean deleteData) { try { - configManager.remove(id); + ReplicatorConfig config = configManager.get(id); + config.setDeleted(true); + config.setDeleteData(deleteData); + config.setSuspended(true); + configManager.save(config); + replicator.cancelSyncRequest(id); return true; - } catch (NotFoundException e) { + } catch (ReplicationPersistenceException e) { + LOGGER.debug("Unable to delete replicator configuration with id {}", id, e); return false; + } catch (NotFoundException e) { + LOGGER.debug("Config with id {}", id, e); + return true; } } @@ -253,9 +262,18 @@ public ListField getSites() { return siteFields; } - public ListField getReplications() { + public ListField getReplications(boolean filterDeleted) { ListField fields = new ReplicationField.ListImpl(); - configManager.objects().map(this::getReplicationFieldForConfig).forEach(fields::add); + + if (filterDeleted) { + configManager + .objects() + .filter(c -> !c.isDeleted()) + .map(this::getReplicationFieldForConfig) + .forEach(fields::add); + } else { + configManager.objects().map(this::getReplicationFieldForConfig).forEach(fields::add); + } return fields; } } diff --git a/admin/query/src/main/java/org/codice/ditto/replication/admin/query/replications/discover/GetReplications.java b/admin/query/src/main/java/org/codice/ditto/replication/admin/query/replications/discover/GetReplications.java index 4a7c0db26..dcb6e26be 100644 --- a/admin/query/src/main/java/org/codice/ditto/replication/admin/query/replications/discover/GetReplications.java +++ b/admin/query/src/main/java/org/codice/ditto/replication/admin/query/replications/discover/GetReplications.java @@ -41,7 +41,7 @@ public GetReplications(ReplicationUtils replicationUtils) { @Override public ListField performFunction() { - return replicationUtils.getReplications(); + return replicationUtils.getReplications(true); } @Override diff --git a/admin/query/src/main/java/org/codice/ditto/replication/admin/query/replications/persist/DeleteReplication.java b/admin/query/src/main/java/org/codice/ditto/replication/admin/query/replications/persist/DeleteReplication.java index 2aadcb325..2d62fa259 100644 --- a/admin/query/src/main/java/org/codice/ditto/replication/admin/query/replications/persist/DeleteReplication.java +++ b/admin/query/src/main/java/org/codice/ditto/replication/admin/query/replications/persist/DeleteReplication.java @@ -29,24 +29,32 @@ public class DeleteReplication extends BaseFunctionField { public static final String FIELD_NAME = "deleteReplication"; - public static final String DESCRIPTION = "Deletes a replication."; + public static final String DESCRIPTION = + "Deletes a Replication and its history (statistics). Optionally delete the data of the Replication. Deleting data will delete " + + "any local resources and metadata that were replicated by this Replication, but not any resources replicated to a remote Node."; public static final BooleanField RETURN_TYPE = new BooleanField(); private PidField id; + private BooleanField deleteData; + private ReplicationUtils replicationUtils; public DeleteReplication(ReplicationUtils replicationUtils) { super(FIELD_NAME, DESCRIPTION); this.replicationUtils = replicationUtils; id = new PidField("id"); + deleteData = new BooleanField("deleteData"); + deleteData.setValue(false); + + id.isRequired(); } @Override public BooleanField performFunction() { BooleanField successful = new BooleanField(); - successful.setValue(replicationUtils.deleteConfig(id.getValue())); + successful.setValue(replicationUtils.markConfigDeleted(id.getValue(), deleteData.getValue())); return successful; } @@ -70,7 +78,7 @@ public BooleanField getReturnType() { @Override public List getArguments() { - return ImmutableList.of(id); + return ImmutableList.of(id, deleteData); } @Override diff --git a/admin/query/src/main/java/org/codice/ditto/replication/admin/query/sites/persist/DeleteReplicationSite.java b/admin/query/src/main/java/org/codice/ditto/replication/admin/query/sites/persist/DeleteReplicationSite.java index 7ce052e82..83078b575 100644 --- a/admin/query/src/main/java/org/codice/ditto/replication/admin/query/sites/persist/DeleteReplicationSite.java +++ b/admin/query/src/main/java/org/codice/ditto/replication/admin/query/sites/persist/DeleteReplicationSite.java @@ -60,7 +60,7 @@ public void validate() { return; } - ListField repFields = replicationUtils.getReplications(); + ListField repFields = replicationUtils.getReplications(true); String idToDelete = id.getValue(); for (ReplicationField repField : repFields.getList()) { if (idToDelete.equals(repField.source().id()) diff --git a/admin/query/src/test/java/org/codice/ditto/replication/admin/query/ReplicationUtilsTest.java b/admin/query/src/test/java/org/codice/ditto/replication/admin/query/ReplicationUtilsTest.java index 8a16f953e..4459c7ed3 100644 --- a/admin/query/src/test/java/org/codice/ditto/replication/admin/query/ReplicationUtilsTest.java +++ b/admin/query/src/test/java/org/codice/ditto/replication/admin/query/ReplicationUtilsTest.java @@ -33,6 +33,7 @@ import org.codice.ditto.replication.admin.query.replications.fields.ReplicationField; import org.codice.ditto.replication.admin.query.sites.fields.ReplicationSiteField; import org.codice.ditto.replication.api.ReplicationException; +import org.codice.ditto.replication.api.ReplicationPersistenceException; import org.codice.ditto.replication.api.ReplicationStatus; import org.codice.ditto.replication.api.Replicator; import org.codice.ditto.replication.api.ReplicatorHistory; @@ -54,7 +55,7 @@ @RunWith(MockitoJUnitRunner.class) public class ReplicationUtilsTest { - ReplicationUtils utils; + private ReplicationUtils utils; @Mock SiteManager siteManager; @@ -292,18 +293,31 @@ public void updateReplicationActiveSyncRequest() { } @Test - public void deleteConfig() { + public void testMarkConfigDeleted() { ReplicatorConfigImpl config = new ReplicatorConfigImpl(); config.setId("id"); config.setName("name"); - assertThat(utils.deleteConfig("id"), is(true)); - verify(configManager).remove(anyString()); + + when(configManager.get("id")).thenReturn(config); + + assertThat(utils.markConfigDeleted("id", true), is(true)); + assertThat(config.shouldDeleteData(), is(true)); + assertThat(config.isDeleted(), is(true)); + assertThat(config.isSuspended(), is(true)); + verify(replicator).cancelSyncRequest("id"); + verify(configManager).save(config); + } + + @Test + public void testMarkConfigDeletedNotFound() { + doThrow(new NotFoundException()).when(configManager).get("id"); + assertThat(utils.markConfigDeleted("id", true), is(true)); } @Test - public void deleteConfigFailed() { - doThrow(new NotFoundException()).when(configManager).remove(anyString()); - assertThat(utils.deleteConfig("id"), is(false)); + public void testMarkConfigDeletedErrorRetrievingConfig() { + doThrow(new ReplicationPersistenceException("")).when(configManager).get("id"); + assertThat(utils.markConfigDeleted("id", true), is(false)); } @Test @@ -336,7 +350,7 @@ public void getReplications() throws Exception { config.setDestination("destId"); config.setBidirectional(false); when(configManager.objects()).thenReturn(Stream.of(config)); - ReplicationField field = utils.getReplications().getList().get(0); + ReplicationField field = utils.getReplications(false).getList().get(0); assertThat(field.name(), is("test")); assertThat(field.source().id(), is("srcId")); assertThat(field.destination().id(), is("destId")); @@ -346,6 +360,15 @@ public void getReplications() throws Exception { assertThat(field.dataTransferred(), is("15 MB")); } + @Test + public void testGetReplicationsFilterDeleted() { + ReplicatorConfigImpl config = new ReplicatorConfigImpl(); + config.setDeleted(true); + + when(configManager.objects()).thenReturn(Stream.of(config)); + assertThat(utils.getReplications(true).getList().size(), is(0)); + } + @Test public void cancelConfig() { assertThat(utils.cancelConfig("test"), is(true)); @@ -393,7 +416,7 @@ public void enableConfig() { @Test public void siteIdExists() { - when(siteManager.get(anyString())).thenReturn(new ReplicationSiteImpl()); + when(siteManager.exists(anyString())).thenReturn(true); assertThat(utils.siteIdExists("id"), is(true)); } diff --git a/admin/query/src/test/java/org/codice/ditto/replication/admin/query/replications/persist/DeleteReplicationTest.java b/admin/query/src/test/java/org/codice/ditto/replication/admin/query/replications/persist/DeleteReplicationTest.java new file mode 100644 index 000000000..b39a60d91 --- /dev/null +++ b/admin/query/src/test/java/org/codice/ditto/replication/admin/query/replications/persist/DeleteReplicationTest.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) Connexta + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ +package org.codice.ditto.replication.admin.query.replications.persist; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.codice.ddf.admin.api.report.ErrorMessage; +import org.codice.ddf.admin.api.report.FunctionReport; +import org.codice.ditto.replication.admin.query.ReplicationMessages; +import org.codice.ditto.replication.admin.query.ReplicationUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DeleteReplicationTest { + + private static final String ID = "abc123"; + + private DeleteReplication deleteReplication; + + private Map args; + + @Mock ReplicationUtils utils; + + @Before + public void setup() { + deleteReplication = new DeleteReplication(utils); + args = new HashMap<>(); + args.put("id", ID); + } + + @Test + public void testValidateNoConfig() { + when(utils.configExists(ID)).thenReturn(false); + FunctionReport report = + deleteReplication.execute(args, Collections.singletonList(DeleteReplication.FIELD_NAME)); + assertThat(report.getErrorMessages().size(), is(1)); + assertThat( + ((ErrorMessage) report.getErrorMessages().get(0)).getCode(), + is(ReplicationMessages.CONFIG_DOES_NOT_EXIST)); + } + + @Test + public void testGetFunctionErrorCodes() { + assertThat( + deleteReplication + .getFunctionErrorCodes() + .contains(ReplicationMessages.CONFIG_DOES_NOT_EXIST), + is(true)); + } + + @Test + public void testDeleteDataArgDefaultsToFalse() { + when(utils.configExists(ID)).thenReturn(true); + FunctionReport report = + deleteReplication.execute(args, Collections.singletonList(DeleteReplication.FIELD_NAME)); + assertThat(report.getErrorMessages().size(), is(0)); + verify(utils, times(1)).markConfigDeleted(ID, false); + } +} diff --git a/dependency-check-maven-config.xml b/dependency-check-maven-config.xml index 8d1088a96..eaaa09c86 100644 --- a/dependency-check-maven-config.xml +++ b/dependency-check-maven-config.xml @@ -141,6 +141,12 @@ CVE-2018-11788 + + + Not using the hadoop-auth feature. + + CVE-2018-11766 + diff --git a/replication-api-impl/pom.xml b/replication-api-impl/pom.xml index cfc34b4d8..272d7a5b6 100644 --- a/replication-api-impl/pom.xml +++ b/replication-api-impl/pom.xml @@ -331,17 +331,17 @@ INSTRUCTION COVEREDRATIO - 0.54 + 0.57 BRANCH COVEREDRATIO - 0.44 + 0.49 COMPLEXITY COVEREDRATIO - 0.47 + 0.52 diff --git a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/MetacardHelper.java b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/Metacards.java similarity index 61% rename from replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/MetacardHelper.java rename to replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/Metacards.java index 80992bb21..f3bc1ac32 100644 --- a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/MetacardHelper.java +++ b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/Metacards.java @@ -20,18 +20,21 @@ import ddf.catalog.data.Result; import ddf.catalog.data.impl.AttributeImpl; import ddf.catalog.data.types.Core; -import ddf.catalog.federation.FederationException; import ddf.catalog.filter.FilterBuilder; import ddf.catalog.filter.impl.SortByImpl; import ddf.catalog.operation.QueryRequest; +import ddf.catalog.operation.impl.DeleteRequestImpl; import ddf.catalog.operation.impl.QueryImpl; import ddf.catalog.operation.impl.QueryRequestImpl; +import ddf.catalog.source.IngestException; import ddf.catalog.source.SourceUnavailableException; -import ddf.catalog.source.UnsupportedQueryException; import ddf.catalog.util.impl.ResultIterable; import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import org.opengis.filter.Filter; @@ -39,14 +42,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class MetacardHelper { - private static final Logger LOGGER = LoggerFactory.getLogger(MetacardHelper.class); +/** Provides common operations when dealing with {@link Metacard}s. */ +public class Metacards { + + private static final Logger LOGGER = LoggerFactory.getLogger(Metacards.class); + + private static final int DEFAULT_BATCH_SIZE = 250; private final CatalogFramework framework; private final FilterBuilder filterBuilder; - public MetacardHelper(CatalogFramework framework, FilterBuilder filterBuilder) { + public Metacards(CatalogFramework framework, FilterBuilder filterBuilder) { this.framework = framework; this.filterBuilder = filterBuilder; } @@ -78,27 +85,6 @@ public T getAttributeValueOrDefault(Metacard mcard, String attribute, T defa return defaultValue; } - public Metacard getMetacardById(String id) { - if (id == null) { - return null; - } - QueryRequest request = - new QueryRequestImpl( - new QueryImpl( - filterBuilder.allOf( - filterBuilder.attribute(Core.ID).is().equalTo().text(id), - filterBuilder.attribute(Metacard.TAGS).is().like().text("*")))); - try { - List results = framework.query(request).getResults(); - if (results.size() == 1) { - return results.get(0).getMetacard(); - } - } catch (UnsupportedQueryException | SourceUnavailableException | FederationException e) { - LOGGER.warn("Unable to retrieve replication metacard for {}", id, e); - } - return null; - } - public List getTypeForFilter(Filter filter, Function function) { QueryRequest request = new QueryRequestImpl( @@ -118,4 +104,64 @@ public List getTypeForFilter(Filter filter, Function functio .filter(Objects::nonNull) .collect(Collectors.toList()); } + + public Set getIdsOfMetacardsInCatalog(Set ids) { + List filters = new ArrayList<>(); + + for (String idString : ids) { + filters.add(filterBuilder.attribute(Core.ID).is().equalTo().text(idString)); + } + Filter filter = filterBuilder.anyOf(filters); + + QueryRequest request = + new QueryRequestImpl( + new QueryImpl( + filter, 1, ids.size(), new SortByImpl(Core.ID, SortOrder.ASCENDING), false, 0L)); + + ResultIterable results = ResultIterable.resultIterable(framework::query, request); + return results + .stream() + .map(Result::getMetacard) + .map(Metacard::getId) + .collect(Collectors.toSet()); + } + + public void doDelete(String[] idsToDelete) throws SourceUnavailableException { + doDelete(idsToDelete, DEFAULT_BATCH_SIZE); + } + + public void doDelete(String[] idsToDelete, int batchSize) throws SourceUnavailableException { + if (idsToDelete.length > 0) { + int start = 0; + int end; + + while (start < idsToDelete.length) { + end = start + batchSize; + if (end > idsToDelete.length) { + end = idsToDelete.length; + } + deleteBatch(Arrays.copyOfRange(idsToDelete, start, end)); + start += batchSize; + } + } + } + + private void deleteBatch(String[] idsToDelete) throws SourceUnavailableException { + try { + framework.delete(new DeleteRequestImpl(idsToDelete)); + } catch (IngestException ie) { + // nothing in batch was deleted, so... + deleteSequentially(idsToDelete); + } + } + + private void deleteSequentially(String[] ids) throws SourceUnavailableException { + for (String id : ids) { + try { + framework.delete(new DeleteRequestImpl(id)); + } catch (IngestException e) { + LOGGER.debug("Failed to delete metacard with id: {}", id, e); + } + } + } } diff --git a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicationPersistentStoreImpl.java b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicationItemManagerImpl.java similarity index 86% rename from replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicationPersistentStoreImpl.java rename to replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicationItemManagerImpl.java index f6e9b3a86..5be3ec611 100644 --- a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicationPersistentStoreImpl.java +++ b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicationItemManagerImpl.java @@ -19,18 +19,19 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.codice.ddf.persistence.PersistenceException; import org.codice.ddf.persistence.PersistentItem; import org.codice.ddf.persistence.PersistentStore; import org.codice.ditto.replication.api.ReplicationItem; -import org.codice.ditto.replication.api.ReplicationPersistentStore; +import org.codice.ditto.replication.api.ReplicationItemManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ReplicationPersistentStoreImpl implements ReplicationPersistentStore { +public class ReplicationItemManagerImpl implements ReplicationItemManager { - private static final Logger LOGGER = - LoggerFactory.getLogger(ReplicationPersistentStoreImpl.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ReplicationItemManagerImpl.class); private static final String ID_KEY = "id"; @@ -54,15 +55,16 @@ public class ReplicationPersistentStoreImpl implements ReplicationPersistentStor private final PersistentStore persistentStore; - public ReplicationPersistentStoreImpl(PersistentStore persistentStore) { + public ReplicationItemManagerImpl(PersistentStore persistentStore) { this.persistentStore = persistentStore; } @Override - public Optional getItem(String id, String source, String destination) { + public Optional getItem(String metacardId, String source, String destination) { String cqlFilter = String.format( - "'id' = '%s' AND 'source' = '%s' AND 'destination' = '%s'", id, source, destination); + "'id' = '%s' AND 'source' = '%s' AND 'destination' = '%s'", + metacardId, source, destination); List> matchingPersistentItems; try { @@ -70,23 +72,23 @@ public Optional getItem(String id, String source, String destin } catch (PersistenceException e) { LOGGER.debug( "failed to retrieve item with id: {}, source: {}, and destination: {}", - id, + metacardId, source, destination); return Optional.empty(); } - if (matchingPersistentItems == null || matchingPersistentItems.isEmpty()) { + if (CollectionUtils.isEmpty(matchingPersistentItems)) { LOGGER.debug( "couldn't find persisted item with id: {}, source: {}, and destination: {}. This is expected during initial replication.", - id, + metacardId, source, destination); return Optional.empty(); } else if (matchingPersistentItems.size() > 1) { throw new IllegalStateException( "Found multiple persistent items with id: " - + id + + metacardId + ", source: " + source + ", and destination: " @@ -102,7 +104,7 @@ public void deleteAllItems() throws PersistenceException { // translation currently does not work. String cql = ""; int index = DEFAULT_START_INDEX; - long itemsDeleted = 0; + long itemsDeleted; do { itemsDeleted = persistentStore.delete(PERSISTENCE_TYPE, cql, index, DEFAULT_PAGE_SIZE); index += DEFAULT_PAGE_SIZE; @@ -112,7 +114,12 @@ public void deleteAllItems() throws PersistenceException { @Override public List getItemsForConfig(String configId, int startIndex, int pageSize) throws PersistenceException { - String cql = String.format("'config-id' = '%s'", configId); + String cql; + if (StringUtils.isNotEmpty(configId)) { + cql = String.format("'%s' = '%s'", CONFIGURATION_ID_KEY, configId); + } else { + cql = ""; + } List> matchingPersistentItems; matchingPersistentItems = persistentStore.get(PERSISTENCE_TYPE, cql, startIndex, pageSize); @@ -133,16 +140,17 @@ public void saveItem(ReplicationItem replicationItem) { } @Override - public void deleteItem(String id, String source, String destination) { + public void deleteItem(String metacardId, String source, String destination) { String cqlFilter = String.format( - "'id' = '%s' AND 'source' = '%s' AND 'destination' = '%s'", id, source, destination); + "'id' = '%s' AND 'source' = '%s' AND 'destination' = '%s'", + metacardId, source, destination); try { persistentStore.delete(PERSISTENCE_TYPE, cqlFilter); } catch (PersistenceException e) { LOGGER.error( "error deleting persisted item with id: {}, source: {}, and destination: {}", - id, + metacardId, source, destination); } @@ -175,7 +183,7 @@ public List getFailureList(int maximumFailureCount, String source, Strin @Override public void deleteItemsForConfig(String configId) throws PersistenceException { - String cql = String.format("'config-id' = '%s'", configId); + String cql = String.format("'%s' = '%s'", CONFIGURATION_ID_KEY, configId); int itemsDeleted; do { diff --git a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicatorHistoryImpl.java b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicatorHistoryImpl.java index c2d06c1b4..cb5f52522 100644 --- a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicatorHistoryImpl.java +++ b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicatorHistoryImpl.java @@ -58,7 +58,7 @@ public class ReplicatorHistoryImpl implements ReplicatorHistory { private final FilterBuilder filterBuilder; - private final MetacardHelper helper; + private final Metacards helper; private final MetacardType metacardType; @@ -66,7 +66,7 @@ public ReplicatorHistoryImpl( CatalogFramework framework, CatalogProvider provider, FilterBuilder filterBuilder, - MetacardHelper helper, + Metacards helper, MetacardType metacardType) { this(framework, provider, filterBuilder, helper, metacardType, Security.getInstance()); } @@ -76,7 +76,7 @@ public ReplicatorHistoryImpl( CatalogFramework framework, CatalogProvider provider, FilterBuilder filterBuilder, - MetacardHelper helper, + Metacards helper, MetacardType metacardType, Security security) { this.framework = framework; @@ -113,7 +113,7 @@ public List getReplicationEvents() { } @Override - public List getReplicationEvents(String replicatorid) { + public List getReplicationEvents(String replicationConfigId) { return helper.getTypeForFilter( filterBuilder.allOf( filterBuilder @@ -121,7 +121,11 @@ public List getReplicationEvents(String replicatorid) { .is() .equalTo() .text(ReplicationHistory.METACARD_TAG), - filterBuilder.attribute(ReplicationConfig.NAME).is().equalTo().text(replicatorid)), + filterBuilder + .attribute(ReplicationConfig.NAME) + .is() + .equalTo() + .text(replicationConfigId)), this::getStatusFromMetacard); } @@ -164,7 +168,7 @@ public void removeReplicationEvent(ReplicationStatus replicationStatus) { @Override public void removeReplicationEvents(Set ids) { try { - provider.delete(new DeleteRequestImpl(ids.toArray(new String[ids.size()]))); + provider.delete(new DeleteRequestImpl(ids.toArray(new String[0]))); } catch (IngestException e) { throw new ReplicationPersistenceException( "Error deleting replication history items " + ids, e); diff --git a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicatorImpl.java b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicatorImpl.java index f8e954595..f8d543518 100644 --- a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicatorImpl.java +++ b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ReplicatorImpl.java @@ -38,7 +38,7 @@ import org.apache.commons.collections4.queue.UnmodifiableQueue; import org.codice.ddf.security.common.Security; import org.codice.ditto.replication.api.ReplicationException; -import org.codice.ditto.replication.api.ReplicationPersistentStore; +import org.codice.ditto.replication.api.ReplicationItemManager; import org.codice.ditto.replication.api.ReplicationStatus; import org.codice.ditto.replication.api.ReplicationStore; import org.codice.ditto.replication.api.Replicator; @@ -60,7 +60,7 @@ public class ReplicatorImpl implements Replicator { private final ReplicatorHistory history; - private final ReplicationPersistentStore persistentStore; + private final ReplicationItemManager persistentStore; private final SiteManager siteManager; @@ -81,7 +81,7 @@ public class ReplicatorImpl implements Replicator { public ReplicatorImpl( ReplicatorStoreFactory replicatorStoreFactory, ReplicatorHistory history, - ReplicationPersistentStore persistentStore, + ReplicationItemManager persistentStore, SiteManager siteManager, ExecutorService executor, FilterBuilder builder) { @@ -98,7 +98,7 @@ public ReplicatorImpl( public ReplicatorImpl( ReplicatorStoreFactory replicatorStoreFactory, ReplicatorHistory history, - ReplicationPersistentStore persistentStore, + ReplicationItemManager persistentStore, SiteManager siteManager, ExecutorService executor, FilterBuilder builder, diff --git a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ScheduledReplicatorDeleter.java b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ScheduledReplicatorDeleter.java new file mode 100644 index 000000000..ffe0de5e0 --- /dev/null +++ b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/ScheduledReplicatorDeleter.java @@ -0,0 +1,288 @@ +/** + * Copyright (c) Connexta + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ +package org.codice.ditto.replication.api.impl; + +import com.google.common.annotations.VisibleForTesting; +import ddf.catalog.source.SourceUnavailableException; +import ddf.security.service.SecurityServiceException; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.codice.ddf.configuration.SystemInfo; +import org.codice.ddf.persistence.PersistenceException; +import org.codice.ddf.security.common.Security; +import org.codice.ditto.replication.api.ReplicationItem; +import org.codice.ditto.replication.api.ReplicationItemManager; +import org.codice.ditto.replication.api.ReplicationPersistenceException; +import org.codice.ditto.replication.api.ReplicationStatus; +import org.codice.ditto.replication.api.ReplicatorHistory; +import org.codice.ditto.replication.api.data.ReplicatorConfig; +import org.codice.ditto.replication.api.persistence.ReplicatorConfigManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Periodically polls for available {@link ReplicatorConfig}s and deletes them based on the {@link + * ReplicatorConfig#isDeleted()} and {@link ReplicatorConfig#shouldDeleteData()} properties. A + * {@link ReplicatorConfig} marked as deleted always has its history deleted. + */ +public class ScheduledReplicatorDeleter { + + private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledReplicatorDeleter.class); + + private static final int DEFAULT_PAGE_SIZE = 1000; + + private final ReplicatorConfigManager replicatorConfigManager; + + private final ReplicationItemManager replicationItemManager; + + private final ReplicatorHistory replicatorHistory; + + private final Metacards metacards; + + private final ScheduledExecutorService scheduledExecutorService; + + private final Security security; + + private final int pageSize; + + public ScheduledReplicatorDeleter( + ReplicatorConfigManager replicatorConfigManager, + ScheduledExecutorService scheduledExecutorService, + ReplicationItemManager replicationItemManager, + ReplicatorHistory replicatorHistory, + Metacards metacards) { + this( + replicatorConfigManager, + scheduledExecutorService, + replicationItemManager, + replicatorHistory, + metacards, + Security.getInstance(), + TimeUnit.MINUTES.toMillis(1), + DEFAULT_PAGE_SIZE); + } + + @VisibleForTesting + @SuppressWarnings("squid:S00107" /* Only for testing */) + ScheduledReplicatorDeleter( + ReplicatorConfigManager replicatorConfigManager, + ScheduledExecutorService scheduledExecutorService, + ReplicationItemManager replicationItemManager, + ReplicatorHistory replicatorHistory, + Metacards metacards, + Security security, + long pollPeriod, + int pageSize) { + this.replicatorConfigManager = replicatorConfigManager; + this.scheduledExecutorService = scheduledExecutorService; + this.replicationItemManager = replicationItemManager; + this.replicatorHistory = replicatorHistory; + this.metacards = metacards; + this.security = security; + this.pageSize = pageSize; + + LOGGER.info( + "Scheduling replicator config cleanup every {} seconds", + TimeUnit.MILLISECONDS.toSeconds(pollPeriod)); + scheduledExecutorService.scheduleAtFixedRate( + this::cleanup, 0, pollPeriod, TimeUnit.MILLISECONDS); + } + + public void destroy() { + scheduledExecutorService.shutdownNow(); + } + + void cleanup() { + security.runAsAdmin( + () -> { + try { + security.runWithSubjectOrElevate( + () -> { + List replicatorConfigs = + replicatorConfigManager.objects().collect(Collectors.toList()); + + cleanupOrphanedReplicationItems(replicatorConfigs); + cleanupDeletedConfigs(replicatorConfigs); + + return null; + }); + } catch (SecurityServiceException | InvocationTargetException e) { + LOGGER.debug("Failed scheduled cleanup of deleted replicator configs", e); + } + + return null; + }); + } + + /** + * Deletes {@link ReplicationItem}s which have no corresponding data store entry or {@link + * ReplicatorConfig}. This occurs when a {@link ReplicatorConfig} was deleted without cleaning up + * data, but the data was then deleted manually afterwards. + */ + private void cleanupOrphanedReplicationItems(List replicatorConfigs) { + Set replicatorConfigIds = + replicatorConfigs.stream().map(ReplicatorConfig::getId).collect(Collectors.toSet()); + + int startIndex = 0; + List replicationItems; + + do { + try { + + replicationItems = replicationItemManager.getItemsForConfig("", startIndex, pageSize); + + Set configlessItems = + replicationItems + .stream() + .filter(item -> !replicatorConfigIds.contains(item.getConfigurationId())) + .collect(Collectors.toSet()); + + if (!configlessItems.isEmpty()) { + Set itemMetacardIds = + configlessItems + .stream() + .map(ReplicationItem::getMetacardId) + .collect(Collectors.toSet()); + + Set idsInTheCatalog = metacards.getIdsOfMetacardsInCatalog(itemMetacardIds); + + Set orphanedItems = + configlessItems + .stream() + .filter(item -> itemNotInCatalog(item, idsInTheCatalog)) + .collect(Collectors.toSet()); + + orphanedItems.forEach( + item -> + replicationItemManager.deleteItem( + item.getMetacardId(), item.getSource(), item.getDestination())); + + startIndex += replicationItems.size() - orphanedItems.size(); + } else { + startIndex += replicationItems.size(); + } + } catch (PersistenceException e) { + LOGGER.debug( + "Failed to delete orphaned replication items. Deletion will be retried next poll interval.", + e); + return; + } + } while (!replicationItems.isEmpty()); + } + + private boolean itemNotInCatalog(ReplicationItem item, Set idsInCatalog) { + return !idsInCatalog.contains(item.getMetacardId()); + } + + private void cleanupDeletedConfigs(List replicatorConfigs) { + List deletedConfigs = + replicatorConfigs.stream().filter(ReplicatorConfig::isDeleted).collect(Collectors.toList()); + + for (ReplicatorConfig config : deletedConfigs) { + final String configId = config.getId(); + final String configName = config.getName(); + + if (config.shouldDeleteData()) { + try { + deleteMetacards(configId); + } catch (PersistenceException e) { + LOGGER.debug( + "Failed to retrieve replication items for config: {}. Deletion will be retried next poll interval.", + configName, + e); + break; + } catch (SourceUnavailableException e) { + LOGGER.debug( + "Failed to delete metacards replicated by config: {}. Deletion will be retried next poll interval.", + configName, + e); + break; + } + + try { + replicationItemManager.deleteItemsForConfig(configId); + } catch (PersistenceException e) { + LOGGER.debug( + "Removed metacards for replicator {}, but failed to delete replication items. Deletion will be tried next poll interval.", + configName); + break; + } + } + + if (deleteHistory(config)) { + replicatorConfigManager.remove(configId); + } + } + } + + private boolean deleteHistory(ReplicatorConfig config) { + try { + deleteReplicatorHistory(config); + return true; + } catch (ReplicationPersistenceException e) { + LOGGER.debug( + "History for replicator configuration {} could not be deleted. Deletion will be retried next polling interval.", + config.getName(), + e); + return false; + } + } + + private void deleteMetacards(String configId) + throws PersistenceException, SourceUnavailableException { + int startIndex = 0; + List replicationItems; + + do { + replicationItems = replicationItemManager.getItemsForConfig(configId, startIndex, pageSize); + + Set idsToDelete = + replicationItems + .stream() + .filter(item -> item.getDestination().equals(getSiteName())) + .map(ReplicationItem::getMetacardId) + .collect(Collectors.toSet()); + + if (!idsToDelete.isEmpty()) { + Set idsInTheCatalog = metacards.getIdsOfMetacardsInCatalog(idsToDelete); + metacards.doDelete(idsInTheCatalog.toArray(new String[0])); + startIndex += idsToDelete.size(); + } else { + startIndex += pageSize; + } + + } while (!replicationItems.isEmpty()); + } + + private void deleteReplicatorHistory(ReplicatorConfig config) { + Set eventIds = + replicatorHistory + .getReplicationEvents(config.getName()) + .stream() + .map(ReplicationStatus::getId) + .collect(Collectors.toSet()); + + replicatorHistory.removeReplicationEvents(eventIds); + } + + /** Solely for mocking out a static call. */ + @VisibleForTesting + String getSiteName() { + return SystemInfo.getSiteName(); + } +} diff --git a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/SyncHelper.java b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/SyncHelper.java index 6ca1741bb..f20a7033a 100644 --- a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/SyncHelper.java +++ b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/SyncHelper.java @@ -67,7 +67,7 @@ import org.apache.shiro.SecurityUtils; import org.codice.ditto.replication.api.ReplicationException; import org.codice.ditto.replication.api.ReplicationItem; -import org.codice.ditto.replication.api.ReplicationPersistentStore; +import org.codice.ditto.replication.api.ReplicationItemManager; import org.codice.ditto.replication.api.ReplicationStatus; import org.codice.ditto.replication.api.ReplicationStore; import org.codice.ditto.replication.api.ReplicatorHistory; @@ -95,7 +95,7 @@ class SyncHelper { private final ReplicatorConfig config; - private final ReplicationPersistentStore persistentStore; + private final ReplicationItemManager persistentStore; private final ReplicatorHistory history; @@ -120,7 +120,7 @@ public SyncHelper( ReplicationStore destination, ReplicatorConfig config, ReplicationStatus status, - ReplicationPersistentStore persistentStore, + ReplicationItemManager persistentStore, ReplicatorHistory history, FilterBuilder builder) { this.source = source; diff --git a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/data/ReplicatorConfigImpl.java b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/data/ReplicatorConfigImpl.java index 997fff770..ed0d86238 100644 --- a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/data/ReplicatorConfigImpl.java +++ b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/data/ReplicatorConfigImpl.java @@ -41,12 +41,29 @@ public class ReplicatorConfigImpl extends AbstractPersistable implements Replica private static final String SUSPENDED_KEY = "suspended"; + private static final String DELETED_KEY = "deleted"; + + private static final String DELETE_DATA_KEY = "deleteData"; + /** - * 0/No Version - initial version of configs which were saved in the catalog framework. 1 - the - * first version of configs to be saved in the replication persistent store. + * Field specifying the version of the configuration. Possible versions include: + * + *

    + *
  1. 0 (No version) - initial version of configs which were saved in the catalog framework + *
  2. 1 - The first version of configs to be saved in the replication persistent store + *
      + *
    • Add suspended field of type boolean with default of false + *
    • Add deleted field of type boolean with default of false + *
    • Add deleteData field of type boolean with default of false + *
    + *
*/ public static final int CURRENT_VERSION = 1; + private boolean deleted = false; + + private boolean deleteData = false; + private String name; private boolean bidirectional; @@ -78,6 +95,8 @@ public Map toMap() { result.put(RETRY_COUNT_KEY, getFailureRetryCount()); result.put(DESCRIPTION_KEY, getDescription()); result.put(SUSPENDED_KEY, Boolean.toString(isSuspended())); + result.put(DELETED_KEY, Boolean.toString(isDeleted())); + result.put(DELETE_DATA_KEY, Boolean.toString(shouldDeleteData())); return result; } @@ -92,6 +111,8 @@ public void fromMap(Map properties) { setFailureRetryCount((int) properties.get(RETRY_COUNT_KEY)); setDescription((String) properties.get(DESCRIPTION_KEY)); setSuspended(Boolean.valueOf((String) properties.get(SUSPENDED_KEY))); + setDeleted(Boolean.valueOf((String) properties.get(DELETED_KEY))); + setDeleteData(Boolean.valueOf((String) properties.get(DELETE_DATA_KEY))); } @Override @@ -173,4 +194,24 @@ public boolean isSuspended() { public void setSuspended(boolean suspended) { this.suspended = suspended; } + + @Override + public boolean isDeleted() { + return deleted; + } + + @Override + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } + + @Override + public boolean shouldDeleteData() { + return deleteData; + } + + @Override + public void setDeleteData(boolean deleteData) { + this.deleteData = deleteData; + } } diff --git a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/legacy/LegacyDataConverter.java b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/legacy/LegacyDataConverter.java index d714bdc5c..b3ab914bb 100644 --- a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/legacy/LegacyDataConverter.java +++ b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/legacy/LegacyDataConverter.java @@ -42,7 +42,7 @@ import org.codice.ditto.replication.api.ReplicationPersistenceException; import org.codice.ditto.replication.api.data.ReplicationSite; import org.codice.ditto.replication.api.data.ReplicatorConfig; -import org.codice.ditto.replication.api.impl.MetacardHelper; +import org.codice.ditto.replication.api.impl.Metacards; import org.codice.ditto.replication.api.impl.data.ReplicatorConfigImpl; import org.codice.ditto.replication.api.impl.persistence.ReplicationPersistentStore; import org.codice.ditto.replication.api.mcard.ReplicationConfig; @@ -65,7 +65,7 @@ public class LegacyDataConverter { private final FilterBuilder filterBuilder; - private final MetacardHelper helper; + private final Metacards metacards; private final SiteManager siteManager; @@ -78,14 +78,14 @@ public class LegacyDataConverter { public LegacyDataConverter( CatalogFramework framework, FilterBuilder filterBuilder, - MetacardHelper helper, + Metacards metacards, SiteManager siteManager, ReplicatorConfigManager newConfigManager, ReplicationPersistentStore persistentStore) { this( framework, filterBuilder, - helper, + metacards, siteManager, newConfigManager, persistentStore, @@ -96,14 +96,14 @@ public LegacyDataConverter( LegacyDataConverter( CatalogFramework framework, FilterBuilder filterBuilder, - MetacardHelper helper, + Metacards metacards, SiteManager siteManager, ReplicatorConfigManager newConfigManager, ReplicationPersistentStore persistentStore, Security security) { this.framework = framework; this.filterBuilder = filterBuilder; - this.helper = helper; + this.metacards = metacards; this.siteManager = siteManager; this.newConfigManager = newConfigManager; this.persistentStore = persistentStore; @@ -169,7 +169,7 @@ private void convertConfigs() { } List getAllConfigs() { - return helper.getTypeForFilter( + return metacards.getTypeForFilter( filterBuilder.attribute(Metacard.TAGS).is().like().text(ReplicationConfig.METACARD_TAG), this::getConfigFromMetacard); } @@ -190,17 +190,17 @@ ReplicatorConfig getConfigFromMetacard(Metacard mcard) { ReplicatorConfigImpl config = new ReplicatorConfigImpl(); Direction direction; config.setId(mcard.getId()); - config.setName(helper.getAttributeValueOrDefault(mcard, ReplicationConfig.NAME, null)); + config.setName(metacards.getAttributeValueOrDefault(mcard, ReplicationConfig.NAME, null)); config.setDescription( - helper.getAttributeValueOrDefault(mcard, ReplicationConfig.DESCRIPTION, null)); + metacards.getAttributeValueOrDefault(mcard, ReplicationConfig.DESCRIPTION, null)); direction = Direction.valueOf( - helper.getAttributeValueOrDefault( + metacards.getAttributeValueOrDefault( mcard, ReplicationConfig.DIRECTION, Direction.PUSH.name())); config.setFailureRetryCount( - helper.getAttributeValueOrDefault( + metacards.getAttributeValueOrDefault( mcard, ReplicationConfig.FAILURE_RETRY_COUNT, DEFAULT_FAILURE_RETRY_COUNT)); - String oldUrl = helper.getAttributeValueOrDefault(mcard, ReplicationConfig.URL, null); + String oldUrl = metacards.getAttributeValueOrDefault(mcard, ReplicationConfig.URL, null); if (oldUrl != null) { config = setSourceAndDestinationWithOldUrl(config, direction, oldUrl); @@ -209,11 +209,11 @@ ReplicatorConfig getConfigFromMetacard(Metacard mcard) { } } else { config.setDestination( - helper.getAttributeValueOrDefault(mcard, ReplicationConfig.DESTINATION, null)); - config.setSource(helper.getAttributeValueOrDefault(mcard, ReplicationConfig.SOURCE, null)); + metacards.getAttributeValueOrDefault(mcard, ReplicationConfig.DESTINATION, null)); + config.setSource(metacards.getAttributeValueOrDefault(mcard, ReplicationConfig.SOURCE, null)); } - config.setFilter(helper.getAttributeValueOrDefault(mcard, ReplicationConfig.CQL, null)); + config.setFilter(metacards.getAttributeValueOrDefault(mcard, ReplicationConfig.CQL, null)); config.setBidirectional(direction == Direction.BOTH); if (config.getDestination() == null @@ -229,8 +229,9 @@ ReplicatorConfig getConfigFromMetacard(Metacard mcard) { ReplicationConfig.CQL); return null; } - config.setVersion(helper.getAttributeValueOrDefault(mcard, ReplicationConfig.VERSION, 1)); - config.setSuspended(helper.getAttributeValueOrDefault(mcard, ReplicationConfig.SUSPEND, false)); + config.setVersion(metacards.getAttributeValueOrDefault(mcard, ReplicationConfig.VERSION, 1)); + config.setSuspended( + metacards.getAttributeValueOrDefault(mcard, ReplicationConfig.SUSPEND, false)); return config; } diff --git a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/persistence/ReplicatorConfigManagerImpl.java b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/persistence/ReplicatorConfigManagerImpl.java index ff27b1a2f..d6be31607 100644 --- a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/persistence/ReplicatorConfigManagerImpl.java +++ b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/persistence/ReplicatorConfigManagerImpl.java @@ -2,7 +2,6 @@ import java.util.stream.Stream; import javax.ws.rs.NotFoundException; -import org.codice.ditto.replication.api.ReplicationPersistenceException; import org.codice.ditto.replication.api.data.ReplicatorConfig; import org.codice.ditto.replication.api.impl.data.ReplicatorConfigImpl; import org.codice.ditto.replication.api.persistence.ReplicatorConfigManager; @@ -47,10 +46,10 @@ public void remove(String id) { } @Override - public boolean configExists(String configId) { + public boolean exists(String id) { try { - persistentStore.get(ReplicatorConfigImpl.class, configId); - } catch (NotFoundException | ReplicationPersistenceException e) { + persistentStore.get(ReplicatorConfigImpl.class, id); + } catch (NotFoundException e) { return false; } return true; diff --git a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/persistence/SiteManagerImpl.java b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/persistence/SiteManagerImpl.java index 118ead9dd..da3d1a298 100644 --- a/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/persistence/SiteManagerImpl.java +++ b/replication-api-impl/src/main/java/org/codice/ditto/replication/api/impl/persistence/SiteManagerImpl.java @@ -92,4 +92,14 @@ public void save(ReplicationSite site) { public void remove(String id) { persistentStore.delete(ReplicationSiteImpl.class, id); } + + @Override + public boolean exists(String id) { + try { + persistentStore.get(ReplicationSiteImpl.class, id); + } catch (NotFoundException e) { + return false; + } + return true; + } } diff --git a/replication-api-impl/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/replication-api-impl/src/main/resources/OSGI-INF/blueprint/blueprint.xml index 86c855ccc..d29cbc8a6 100644 --- a/replication-api-impl/src/main/resources/OSGI-INF/blueprint/blueprint.xml +++ b/replication-api-impl/src/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -30,13 +30,32 @@ - + + + + + + + + + + + + + + + + + class="org.codice.ditto.replication.api.impl.persistence.ReplicationPersistentStore"> @@ -45,13 +64,13 @@ init-method="init"> - + - + @@ -92,10 +111,10 @@ - + - + @@ -110,7 +129,7 @@ - + @@ -120,7 +139,7 @@ destroy-method="cleanUp"> - + @@ -137,7 +156,8 @@ - + diff --git a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/MetacardsTest.java b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/MetacardsTest.java new file mode 100644 index 000000000..9b17d0924 --- /dev/null +++ b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/MetacardsTest.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) Connexta + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ +package org.codice.ditto.replication.api.impl; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.Sets; +import ddf.catalog.CatalogFramework; +import ddf.catalog.data.Metacard; +import ddf.catalog.data.Result; +import ddf.catalog.federation.FederationException; +import ddf.catalog.filter.FilterBuilder; +import ddf.catalog.filter.proxy.builder.GeotoolsFilterBuilder; +import ddf.catalog.operation.DeleteRequest; +import ddf.catalog.operation.DeleteResponse; +import ddf.catalog.operation.QueryRequest; +import ddf.catalog.operation.QueryResponse; +import ddf.catalog.source.IngestException; +import ddf.catalog.source.SourceUnavailableException; +import ddf.catalog.source.UnsupportedQueryException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class MetacardsTest { + + private Metacards metacards; + + @Mock CatalogFramework catalogFramework; + + @Before + public void setup() { + FilterBuilder filterBuilder = new GeotoolsFilterBuilder(); + metacards = new Metacards(catalogFramework, filterBuilder); + } + + @Test + public void testGetIdsOfMetacardsInCatalog() + throws UnsupportedQueryException, FederationException, SourceUnavailableException { + Set ids = Sets.newHashSet(getIds(5)); + QueryResponse queryResponse = mock(QueryResponse.class); + List results = mockResults(ids); + when(queryResponse.getResults()).thenReturn(results); + when(catalogFramework.query(any(QueryRequest.class))).thenReturn(queryResponse); + + assertThat(metacards.getIdsOfMetacardsInCatalog(ids), equalTo(ids)); + } + + @Test + public void testBatchDelete() throws SourceUnavailableException, IngestException { + final int batchSize = 5; + String[] idsToDelete = getIds(batchSize * 3 + 1); + + metacards.doDelete(idsToDelete, batchSize); + verify(catalogFramework, times(4)).delete(any(DeleteRequest.class)); + } + + @Test + public void testBatchDeleteWithSomeFailures() throws SourceUnavailableException, IngestException { + final int batchSize = 5; + String[] idsToDelete = getIds(batchSize * 3); + + when(catalogFramework.delete(any(DeleteRequest.class))) + .thenReturn(mock(DeleteResponse.class)) + .thenThrow(IngestException.class) + .thenReturn(mock(DeleteResponse.class)); + + metacards.doDelete(idsToDelete, batchSize); + verify(catalogFramework, times(8)).delete(any(DeleteRequest.class)); + } + + private String[] getIds(int size) { + String[] ids = new String[size]; + for (int i = 0; i < size; i++) { + ids[i] = i + ""; + } + return ids; + } + + private List mockResults(Set ids) { + List results = new ArrayList<>(); + ids.forEach(id -> results.add(mockResult(id))); + return results; + } + + private Result mockResult(String metacardId) { + Result result = mock(Result.class); + Metacard metacard = mock(Metacard.class); + when(metacard.getId()).thenReturn(metacardId); + when(result.getMetacard()).thenReturn(metacard); + return result; + } +} diff --git a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ReplicatorHistoryImplTest.java b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ReplicatorHistoryImplTest.java index f1c7335ca..de5b6727b 100644 --- a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ReplicatorHistoryImplTest.java +++ b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ReplicatorHistoryImplTest.java @@ -70,7 +70,7 @@ public class ReplicatorHistoryImplTest { @Mock CatalogProvider provider; - @Mock MetacardHelper helper; + @Mock Metacards metacards; @Before public void setUp() throws Exception { @@ -80,10 +80,10 @@ public void setUp() throws Exception { types.add(new ReplicationHistoryAttributes()); MetacardType type = new MetacardTypeImpl("replication-history", types); doCallRealMethod() - .when(helper) + .when(metacards) .setIfPresent(any(Metacard.class), any(String.class), any(Serializable.class)); doCallRealMethod() - .when(helper) + .when(metacards) .setIfPresentOrDefault( any(Metacard.class), any(String.class), @@ -94,7 +94,7 @@ public void setUp() throws Exception { .thenAnswer(invocation -> invocation.getArgumentAt(0, Callable.class).call()); when(security.runAsAdmin(any(PrivilegedAction.class))) .thenAnswer(invocation -> invocation.getArgumentAt(0, PrivilegedAction.class).run()); - history = new ReplicatorHistoryImpl(framework, provider, builder, helper, type, security); + history = new ReplicatorHistoryImpl(framework, provider, builder, metacards, type, security); } @Test @@ -103,7 +103,7 @@ public void init() throws Exception { generateOldStatus( "test", new Date(0), new Date(TimeUnit.DAYS.toMillis(1)), TimeUnit.MINUTES.toMillis(5)); - when(helper.getTypeForFilter(any(Filter.class), any(Function.class))).thenReturn(events); + when(metacards.getTypeForFilter(any(Filter.class), any(Function.class))).thenReturn(events); history.init(); ArgumentCaptor captor = ArgumentCaptor.forClass(CreateRequest.class); verify(framework).create(captor.capture()); @@ -140,7 +140,7 @@ public void initPreviouslyCondensedEvents() throws Exception { oldStatus.setLastRun(origTime); oldStatus.setLastSuccess(origTime); oldStatus.setStatus(Status.SUCCESS); - when(helper.getTypeForFilter(any(Filter.class), any(Function.class))) + when(metacards.getTypeForFilter(any(Filter.class), any(Function.class))) .thenReturn(Collections.singletonList(oldStatus)); history.init(); verify(framework, never()).create(any(CreateRequest.class)); @@ -149,7 +149,7 @@ public void initPreviouslyCondensedEvents() throws Exception { @Test public void addReplicationEventNoPreviousEventsSuccess() throws Exception { - when(helper.getTypeForFilter(any(Filter.class), any(Function.class))) + when(metacards.getTypeForFilter(any(Filter.class), any(Function.class))) .thenReturn(new ArrayList()); Date start = new Date(); ReplicationStatus status = new ReplicationStatusImpl("test"); @@ -166,7 +166,7 @@ public void addReplicationEventNoPreviousEventsSuccess() throws Exception { @Test public void addReplicationEventNoPreviousEventsFailure() throws Exception { - when(helper.getTypeForFilter(any(Filter.class), any(Function.class))) + when(metacards.getTypeForFilter(any(Filter.class), any(Function.class))) .thenReturn(new ArrayList()); Date start = new Date(); ReplicationStatus status = new ReplicationStatusImpl("test"); @@ -190,7 +190,7 @@ public void addReplicationEventPreviousEvents() throws Exception { oldStatus.setLastSuccess(origTime); oldStatus.setStatus(Status.SUCCESS); - when(helper.getTypeForFilter(any(Filter.class), any(Function.class))) + when(metacards.getTypeForFilter(any(Filter.class), any(Function.class))) .thenReturn(Collections.singletonList(oldStatus)); Date start = new Date(); ReplicationStatus status = new ReplicationStatusImpl("test"); @@ -210,7 +210,7 @@ public void addReplicationEventPreviousEvents() throws Exception { @Test(expected = ReplicationPersistenceException.class) public void addReplicationEventStorageException() throws Exception { - when(helper.getTypeForFilter(any(Filter.class), any(Function.class))) + when(metacards.getTypeForFilter(any(Filter.class), any(Function.class))) .thenReturn(new ArrayList()); when(framework.create(any(CreateRequest.class))).thenThrow(new IngestException()); Date start = new Date(); diff --git a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ReplicatorImplTest.java b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ReplicatorImplTest.java index 0afbf49e1..1ea9bfcfb 100644 --- a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ReplicatorImplTest.java +++ b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ReplicatorImplTest.java @@ -28,7 +28,7 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import org.codice.ddf.security.common.Security; -import org.codice.ditto.replication.api.ReplicationPersistentStore; +import org.codice.ditto.replication.api.ReplicationItemManager; import org.codice.ditto.replication.api.ReplicationStatus; import org.codice.ditto.replication.api.ReplicationStore; import org.codice.ditto.replication.api.ReplicatorHistory; @@ -56,7 +56,7 @@ public class ReplicatorImplTest { @Mock ReplicatorStoreFactory replicatorStoreFactory; @Mock ReplicatorHistory history; - @Mock ReplicationPersistentStore persistentStore; + @Mock ReplicationItemManager persistentStore; @Mock SiteManager siteManager; @Mock ExecutorService executor; @Mock Security security; diff --git a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ReplicatorStoreFactoryImplTest.java b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ReplicatorStoreFactoryImplTest.java index 087f2da1f..0fb9367a4 100644 --- a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ReplicatorStoreFactoryImplTest.java +++ b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ReplicatorStoreFactoryImplTest.java @@ -14,7 +14,7 @@ package org.codice.ditto.replication.api.impl; import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.*; +import static org.junit.Assert.assertThat; import static org.mockito.Mockito.spy; import com.thoughtworks.xstream.converters.Converter; diff --git a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ScheduledReplicatorDeleterTest.java b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ScheduledReplicatorDeleterTest.java new file mode 100644 index 000000000..ac9e1a7f5 --- /dev/null +++ b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/ScheduledReplicatorDeleterTest.java @@ -0,0 +1,385 @@ +/** + * Copyright (c) Connexta + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ +package org.codice.ditto.replication.api.impl; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anySet; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import ddf.catalog.source.SourceUnavailableException; +import ddf.security.service.SecurityServiceException; +import java.lang.reflect.InvocationTargetException; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.codice.ddf.configuration.SystemInfo; +import org.codice.ddf.persistence.PersistenceException; +import org.codice.ddf.security.common.Security; +import org.codice.ditto.replication.api.ReplicationItem; +import org.codice.ditto.replication.api.ReplicationItemManager; +import org.codice.ditto.replication.api.ReplicationPersistenceException; +import org.codice.ditto.replication.api.ReplicationStatus; +import org.codice.ditto.replication.api.ReplicatorHistory; +import org.codice.ditto.replication.api.data.ReplicatorConfig; +import org.codice.ditto.replication.api.persistence.ReplicatorConfigManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ScheduledReplicatorDeleterTest { + + private static final String SITE_NAME = "siteName"; + + private TestSheduledReplicatorDeleter scheduledReplicatorDeleter; + + private final int pageSize = 1; + + private final String SOURCE = "source"; + + @Mock ReplicatorConfigManager replicatorConfigManager; + + @Mock ScheduledExecutorService scheduledExecutorService; + + @Mock ReplicationItemManager replicationItemManager; + + @Mock ReplicatorHistory replicatorHistory; + + @Mock Metacards metacards; + + @Mock Security security; + + @Before + public void setup() throws SecurityServiceException, InvocationTargetException { + when(security.runWithSubjectOrElevate(any(Callable.class))) + .thenAnswer(invocation -> invocation.getArgumentAt(0, Callable.class).call()); + when(security.runAsAdmin(any(PrivilegedAction.class))) + .thenAnswer(invocation -> invocation.getArgumentAt(0, PrivilegedAction.class).run()); + + scheduledReplicatorDeleter = + new TestSheduledReplicatorDeleter( + replicatorConfigManager, + scheduledExecutorService, + replicationItemManager, + replicatorHistory, + metacards, + security, + 1, + pageSize); + } + + @Test + public void testCleanupOrphanedItems() throws PersistenceException { + final int pageSize = 2; + scheduledReplicatorDeleter = + new TestSheduledReplicatorDeleter( + replicatorConfigManager, + scheduledExecutorService, + replicationItemManager, + replicatorHistory, + metacards, + security, + 1, + pageSize); + + final String configId = "configId"; + final String configName = "configName"; + ReplicatorConfig config = mockConfig(configId, configName, false, false); + when(replicatorConfigManager.objects()).thenReturn(Stream.of(config)); + + final String metacardId1 = "mId1"; + final String metacardId2 = "mId2"; + final String metacardId3 = "mId3"; + final String metacardId4 = "mId4"; + final String metacardId5 = "mId5"; + final String metacardId6 = "mId6"; + + ReplicationItem rep1 = mockRepItem(metacardId1, SITE_NAME); + ReplicationItem rep2 = mockRepItem(metacardId2, SITE_NAME); + ReplicationItem rep3 = mockRepItem(metacardId3, SITE_NAME); + ReplicationItem rep4 = mockRepItem(metacardId4, SITE_NAME); + ReplicationItem rep5 = mockRepItem(metacardId5, SITE_NAME); + ReplicationItem rep6 = mockRepItem(metacardId6, SITE_NAME); + when(rep1.getConfigurationId()).thenReturn("noExistingConfigId"); + when(rep2.getConfigurationId()).thenReturn(configId); + when(rep3.getConfigurationId()).thenReturn(configId); + when(rep4.getConfigurationId()).thenReturn(configId); + when(rep5.getConfigurationId()).thenReturn("noExistingConfigId"); + when(rep6.getConfigurationId()).thenReturn("noExistingConfigId"); + + when(replicationItemManager.getItemsForConfig("", 0, pageSize)) + .thenReturn(ImmutableList.of(rep1, rep2)); + when(replicationItemManager.getItemsForConfig("", 1, pageSize)) + .thenReturn(ImmutableList.of(rep3, rep4)); + when(replicationItemManager.getItemsForConfig("", 3, pageSize)) + .thenReturn(ImmutableList.of(rep5, rep6)); + + when(metacards.getIdsOfMetacardsInCatalog(ImmutableSet.of(metacardId1))) + .thenReturn(Collections.emptySet()); + when(metacards.getIdsOfMetacardsInCatalog(ImmutableSet.of(metacardId5, metacardId6))) + .thenReturn(Collections.singleton(metacardId5)); + + scheduledReplicatorDeleter.cleanup(); + + verify(replicationItemManager, times(1)).deleteItem(metacardId1, SOURCE, SITE_NAME); + verify(replicationItemManager, never()).deleteItem(metacardId2, SOURCE, SITE_NAME); + verify(replicationItemManager, never()).deleteItem(metacardId3, SOURCE, SITE_NAME); + verify(replicationItemManager, never()).deleteItem(metacardId4, SOURCE, SITE_NAME); + verify(replicationItemManager, never()).deleteItem(metacardId5, SOURCE, SITE_NAME); + verify(replicationItemManager, times(1)).deleteItem(metacardId6, SOURCE, SITE_NAME); + } + + @Test + public void testOrphanedItemsToFailGetItems() throws PersistenceException { + final String configId = "configId"; + final String configName = "configName"; + ReplicatorConfig config = mockConfig(configId, configName, true, true); + + when(replicatorConfigManager.objects()).thenReturn(Stream.of(config)); + + doThrow(PersistenceException.class).when(replicationItemManager).getItemsForConfig("", 0, 1); + verify(replicationItemManager, never()).deleteItem(anyString(), anyString(), anyString()); + + scheduledReplicatorDeleter.cleanup(); + } + + @Test + public void testCleanupDeletedConfigs() throws PersistenceException, SourceUnavailableException { + final String deletedConfigId = "deletedConfigId"; + final String deletedConfigName = "deletedConfigName"; + final String metacardId1 = "mId1"; + final String metacardId2 = "mId2"; + final String metacardId3 = "mId3"; + ReplicatorConfig deletedConfig = mockConfig(deletedConfigId, deletedConfigName, true, true); + + final String configId = "configId"; + final String configName = "configName"; + ReplicatorConfig config = mockConfig(configId, configName, false, false); + + when(replicatorConfigManager.objects()).thenReturn(Stream.of(config, deletedConfig)); + + ReplicationItem rep1 = mockRepItem(metacardId1, SITE_NAME); + ReplicationItem rep2 = mockRepItem(metacardId2, "anotherSite"); + ReplicationItem rep3 = mockRepItem(metacardId3, SITE_NAME); + + when(replicationItemManager.getItemsForConfig(deletedConfigId, 0, pageSize)) + .thenReturn(Collections.singletonList(rep1)); + when(replicationItemManager.getItemsForConfig(deletedConfigId, 1, pageSize)) + .thenReturn(Collections.singletonList(rep2)); + when(replicationItemManager.getItemsForConfig(deletedConfigId, 2, pageSize)) + .thenReturn(Collections.singletonList(rep3)); + + when(metacards.getIdsOfMetacardsInCatalog(Collections.singleton(metacardId1))) + .thenReturn(Collections.singleton(metacardId1)); + when(metacards.getIdsOfMetacardsInCatalog(Collections.singleton(metacardId3))) + .thenReturn(Collections.singleton(metacardId3)); + + ReplicationStatus mockStatus = mockRepStatus("id"); + List replicationStatuses = Collections.singletonList(mockStatus); + + when(replicatorHistory.getReplicationEvents(deletedConfigName)).thenReturn(replicationStatuses); + Set eventIds = + replicationStatuses.stream().map(ReplicationStatus::getId).collect(Collectors.toSet()); + + scheduledReplicatorDeleter.cleanup(); + + // deleted config + verify(metacards, times(1)).doDelete(Collections.singleton(metacardId1).toArray(new String[0])); + verify(metacards, times(1)).doDelete(Collections.singleton(metacardId3).toArray(new String[0])); + verify(replicatorHistory, times(1)).getReplicationEvents(deletedConfigName); + verify(replicatorHistory, times(1)).removeReplicationEvents(eventIds); + verify(replicatorConfigManager, times(1)).remove(deletedConfigId); + + // not deleted config + verify(replicationItemManager, never()).getItemsForConfig(configId, 0, pageSize); + verify(replicatorHistory, never()).getReplicationEvents(configName); + verify(replicationItemManager, times(1)).deleteItemsForConfig(deletedConfigId); + verify(replicatorConfigManager, times(1)).remove(deletedConfigId); + } + + @Test + public void testConfigNotMarkedForDeleteData() + throws PersistenceException, SourceUnavailableException { + final String configId = "configId"; + final String configName = "configName"; + final String metacardId = "metacardId"; + ReplicatorConfig config = mockConfig(configId, configName, true, false); + when(replicatorConfigManager.objects()).thenReturn(Stream.of(config)); + + List mockItems = new ArrayList<>(); + mockItems.add(mockRepItem(metacardId, SITE_NAME)); + when(replicationItemManager.getItemsForConfig(configId, 0, pageSize)).thenReturn(mockItems); + + ReplicationStatus mockStatus = mockRepStatus("id"); + List replicationStatuses = Collections.singletonList(mockStatus); + + when(replicatorHistory.getReplicationEvents(configName)).thenReturn(replicationStatuses); + Set eventIds = + replicationStatuses.stream().map(ReplicationStatus::getId).collect(Collectors.toSet()); + + scheduledReplicatorDeleter.cleanup(); + + verify(metacards, never()).doDelete(any()); + verify(replicatorHistory, times(1)).getReplicationEvents(configName); + verify(replicatorHistory, times(1)).removeReplicationEvents(eventIds); + verify(replicatorConfigManager, times(1)).remove(configId); + } + + @Test + public void testFailToRetrieveItemsDoesntRemoveMetacardsOrHistoryOrConfig() + throws SourceUnavailableException, PersistenceException { + final String configId = "configId"; + ReplicatorConfig config = mockConfig(configId, "name", true, true); + when(replicatorConfigManager.objects()).thenReturn(Stream.of(config)); + + when(replicationItemManager.getItemsForConfig(configId, 0, pageSize)) + .thenThrow(PersistenceException.class); + + scheduledReplicatorDeleter.cleanup(); + + verify(metacards, never()).doDelete(any()); + verify(replicationItemManager, never()).deleteItemsForConfig(configId); + verify(replicatorHistory, never()).getReplicationEvents(configId); + verify(replicatorHistory, never()).removeReplicationEvents(anySet()); + verify(replicatorConfigManager, never()).remove(configId); + } + + @Test + public void testFailDeleteMetacardsDoesntRemoveItemsOrConfig() + throws SourceUnavailableException, PersistenceException { + final String configId = "configId"; + ReplicatorConfig config = mockConfig(configId, "name", true, true); + when(replicatorConfigManager.objects()).thenReturn(Stream.of(config)); + + List mockItems = new ArrayList<>(); + mockItems.add(mockRepItem("metcardId", SITE_NAME)); + when(replicationItemManager.getItemsForConfig(configId, 0, pageSize)).thenReturn(mockItems); + + doThrow(SourceUnavailableException.class).when(metacards).doDelete(any()); + + scheduledReplicatorDeleter.cleanup(); + + verify(replicationItemManager, never()).deleteItemsForConfig(configId); + verify(replicatorHistory, never()).getReplicationEvents(configId); + verify(replicatorHistory, never()).removeReplicationEvents(anySet()); + verify(replicatorConfigManager, never()).remove(configId); + } + + @Test + public void testFailCleanupHistoryDoesntRemoveConfig() + throws PersistenceException, SourceUnavailableException { + final String configId = "configId"; + final String configName = "configName"; + final String metacardId1 = "mId1"; + ReplicatorConfig config = mockConfig(configId, configName, true, true); + when(replicatorConfigManager.objects()).thenReturn(Stream.of(config)); + + List mockItems = new ArrayList<>(); + mockItems.add(mockRepItem(metacardId1, SITE_NAME)); + when(replicationItemManager.getItemsForConfig(configId, 0, pageSize)).thenReturn(mockItems); + + Set itemMetacardIds = Collections.singleton(metacardId1); + Set idsInCatalog = Collections.singleton(metacardId1); + when(metacards.getIdsOfMetacardsInCatalog(itemMetacardIds)).thenReturn(idsInCatalog); + + ReplicationStatus mockStatus = mockRepStatus("id"); + List replicationStatuses = Collections.singletonList(mockStatus); + + when(replicatorHistory.getReplicationEvents(configName)).thenReturn(replicationStatuses); + Set eventIds = + replicationStatuses.stream().map(ReplicationStatus::getId).collect(Collectors.toSet()); + doThrow(ReplicationPersistenceException.class) + .when(replicatorHistory) + .removeReplicationEvents(eventIds); + + scheduledReplicatorDeleter.cleanup(); + + verify(metacards, times(1)).doDelete(idsInCatalog.toArray(new String[0])); + verify(replicatorHistory, times(1)).getReplicationEvents(configName); + verify(replicatorHistory, times(1)).removeReplicationEvents(eventIds); + verify(replicatorConfigManager, never()).remove(configId); + } + + private ReplicatorConfig mockConfig( + String id, String name, boolean isDeleted, boolean deleteData) { + ReplicatorConfig config = mock(ReplicatorConfig.class); + when(config.getId()).thenReturn(id); + when(config.getName()).thenReturn(name); + when(config.isDeleted()).thenReturn(isDeleted); + when(config.shouldDeleteData()).thenReturn(deleteData); + return config; + } + + private ReplicationStatus mockRepStatus(String id) { + ReplicationStatus status = mock(ReplicationStatus.class); + when(status.getId()).thenReturn(id); + return status; + } + + private ReplicationItem mockRepItem(String metacardId, String destination) { + ReplicationItem item = mock(ReplicationItem.class); + when(item.getMetacardId()).thenReturn(metacardId); + when(item.getDestination()).thenReturn(destination); + when(item.getSource()).thenReturn(SOURCE); + return item; + } + + /** + * {@link ScheduledReplicatorDeleter} extension solely for overriding a static method call to + * {@link SystemInfo#getSiteName()} + */ + private class TestSheduledReplicatorDeleter extends ScheduledReplicatorDeleter { + + public TestSheduledReplicatorDeleter( + ReplicatorConfigManager replicatorConfigManager, + ScheduledExecutorService scheduledExecutorService, + ReplicationItemManager replicationItemManager, + ReplicatorHistory replicatorHistory, + Metacards metacards, + Security security, + long pollPeriod, + int pageSize) { + super( + replicatorConfigManager, + scheduledExecutorService, + replicationItemManager, + replicatorHistory, + metacards, + security, + pollPeriod, + pageSize); + } + + @Override + String getSiteName() { + return SITE_NAME; + } + } +} diff --git a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/SyncHelperTest.java b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/SyncHelperTest.java index 5998fc4e4..abc02f31f 100644 --- a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/SyncHelperTest.java +++ b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/SyncHelperTest.java @@ -42,7 +42,7 @@ import java.util.Date; import java.util.Optional; import org.apache.shiro.util.ThreadContext; -import org.codice.ditto.replication.api.ReplicationPersistentStore; +import org.codice.ditto.replication.api.ReplicationItemManager; import org.codice.ditto.replication.api.ReplicationStatus; import org.codice.ditto.replication.api.ReplicationStore; import org.codice.ditto.replication.api.ReplicatorHistory; @@ -63,7 +63,7 @@ public class SyncHelperTest { @Mock ReplicationStore source; @Mock ReplicationStore destination; @Mock ReplicatorConfig config; - @Mock ReplicationPersistentStore persistentStore; + @Mock ReplicationItemManager persistentStore; @Mock ReplicatorHistory history; ReplicationStatus status; diff --git a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/legacy/LegacyDataConverterTest.java b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/legacy/LegacyDataConverterTest.java index 33cdb01b0..90507f396 100644 --- a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/legacy/LegacyDataConverterTest.java +++ b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/legacy/LegacyDataConverterTest.java @@ -19,7 +19,12 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import ddf.catalog.CatalogFramework; import ddf.catalog.data.Metacard; @@ -49,7 +54,7 @@ import org.codice.ditto.replication.api.ReplicationPersistenceException; import org.codice.ditto.replication.api.data.ReplicationSite; import org.codice.ditto.replication.api.data.ReplicatorConfig; -import org.codice.ditto.replication.api.impl.MetacardHelper; +import org.codice.ditto.replication.api.impl.Metacards; import org.codice.ditto.replication.api.impl.data.ReplicationSiteImpl; import org.codice.ditto.replication.api.impl.data.ReplicatorConfigImpl; import org.codice.ditto.replication.api.impl.mcard.ReplicationConfigAttributes; @@ -72,16 +77,14 @@ @RunWith(MockitoJUnitRunner.class) public class LegacyDataConverterTest { - // @Rule public Timeout globalTimeout = Timeout.seconds(10); - @Rule public final RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties(); - LegacyDataConverter converter; + private LegacyDataConverter converter; @Mock CatalogFramework framework; - @Mock MetacardHelper helper; + @Mock Metacards helper; @Mock SiteManager siteManager; @@ -91,7 +94,7 @@ public class LegacyDataConverterTest { @Mock Security security; - MetacardType type; + private MetacardType type; @Before public void setUp() throws Exception { @@ -122,13 +125,7 @@ public void setUp() throws Exception { oldSite.setName("oldSiteName"); oldSite.setUrl("https://oldurl:8080"); when(persistentStore.objects(eq(OldSite.class))) - .thenAnswer( - new Answer>() { - @Override - public Stream answer(InvocationOnMock invocation) throws Throwable { - return Stream.of(oldSite); - } - }); + .thenAnswer((Answer>) invocation -> Stream.of(oldSite)); when(siteManager.createSite(anyString(), anyString())).thenReturn(new ReplicationSiteImpl()); converter = new LegacyDataConverter( diff --git a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/persistence/ReplicationPersistentStoreTest.java b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/persistence/ReplicationPersistentStoreTest.java index f71aa0b8e..c698a8cfd 100644 --- a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/persistence/ReplicationPersistentStoreTest.java +++ b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/persistence/ReplicationPersistentStoreTest.java @@ -116,6 +116,8 @@ private Map createRepSyncMap(int num) { map.put(RETRY_COUNT, num); map.put(BIDIRECTIONAL, "true"); map.put(SUSPENDED, "false"); + map.put("deleted", "false"); + map.put("deleteData", "false"); map.put(DESCRIPTION, DESCRIPTION + num); map.put(VERSION, ReplicatorConfigImpl.CURRENT_VERSION); return map; diff --git a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/persistence/ReplicatorConfigManagerImplTest.java b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/persistence/ReplicatorConfigManagerImplTest.java index e36464a90..ba75544e5 100644 --- a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/persistence/ReplicatorConfigManagerImplTest.java +++ b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/persistence/ReplicatorConfigManagerImplTest.java @@ -13,6 +13,8 @@ */ package org.codice.ditto.replication.api.impl.persistence; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; @@ -21,6 +23,7 @@ import static org.mockito.Mockito.when; import java.util.stream.Stream; +import javax.ws.rs.NotFoundException; import org.codice.ditto.replication.api.data.ReplicatorConfig; import org.codice.ditto.replication.api.impl.data.ReplicatorConfigImpl; import org.junit.Before; @@ -32,7 +35,7 @@ @RunWith(MockitoJUnitRunner.class) public class ReplicatorConfigManagerImplTest { - ReplicatorConfigManagerImpl manager; + private ReplicatorConfigManagerImpl manager; @Mock ReplicationPersistentStore persistentStore; @@ -73,4 +76,15 @@ public void removeConfig() { manager.remove("id"); verify(persistentStore).delete(eq(ReplicatorConfigImpl.class), anyString()); } + + @Test + public void testExistsNotFound() { + when(persistentStore.get(ReplicatorConfigImpl.class, "id")).thenThrow(NotFoundException.class); + assertThat(manager.exists("id"), is(false)); + } + + @Test + public void testExistsConfigFound() { + assertThat(manager.exists("id"), is(true)); + } } diff --git a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/persistence/SiteManagerImplTest.java b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/persistence/SiteManagerImplTest.java index 9dc3ef21d..06142da3c 100644 --- a/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/persistence/SiteManagerImplTest.java +++ b/replication-api-impl/src/test/java/org/codice/ditto/replication/api/impl/persistence/SiteManagerImplTest.java @@ -41,20 +41,20 @@ public class SiteManagerImplTest { private static final String LOCAL_SITE_ID = "local-site-id-1234567890"; - SiteManagerImpl store; + private SiteManagerImpl siteManager; @Mock ReplicationPersistentStore persistentStore; @Before public void setUp() { System.setProperty("org.codice.ddf.system.siteName", "testSite"); - store = new SiteManagerImpl(persistentStore); + siteManager = new SiteManagerImpl(persistentStore); } @Test public void init() { when(persistentStore.get(any(Class.class), anyString())).thenThrow(new NotFoundException()); - store.init(); + siteManager.init(); ArgumentCaptor captor = ArgumentCaptor.forClass(ReplicationSiteImpl.class); verify(persistentStore).save(captor.capture()); ReplicationSite site = captor.getValue(); @@ -69,7 +69,7 @@ public void initUpdateName() { orig.setName("oldName"); orig.setUrl(SystemBaseUrl.EXTERNAL.getBaseUrl()); when(persistentStore.get(eq(ReplicationSiteImpl.class), anyString())).thenReturn(orig); - store.init(); + siteManager.init(); ArgumentCaptor captor = ArgumentCaptor.forClass(ReplicationSiteImpl.class); verify(persistentStore).save(captor.capture()); ReplicationSiteImpl site = captor.getValue(); @@ -84,7 +84,7 @@ public void initUpdateURL() { orig.setName(SystemInfo.getSiteName()); orig.setUrl("https://asdf:1234"); when(persistentStore.get(eq(ReplicationSiteImpl.class), anyString())).thenReturn(orig); - store.init(); + siteManager.init(); ArgumentCaptor captor = ArgumentCaptor.forClass(ReplicationSiteImpl.class); verify(persistentStore).save(captor.capture()); ReplicationSiteImpl site = captor.getValue(); @@ -99,7 +99,7 @@ public void initNoOp() { orig.setName(SystemInfo.getSiteName()); orig.setUrl(SystemBaseUrl.EXTERNAL.getBaseUrl()); when(persistentStore.get(eq(ReplicationSiteImpl.class), anyString())).thenReturn(orig); - store.init(); + siteManager.init(); verify(persistentStore, never()).save(any(ReplicationSiteImpl.class)); } @@ -109,12 +109,12 @@ public void getSites() { Stream siteStream = Stream.of(site); when(persistentStore.objects(eq(ReplicationSiteImpl.class))).thenReturn(siteStream); - assertThat(store.objects().anyMatch(site::equals), is(true)); + assertThat(siteManager.objects().anyMatch(site::equals), is(true)); } @Test public void createSite() { - ReplicationSite site = store.createSite("name", "url"); + ReplicationSite site = siteManager.createSite("name", "url"); assertThat(site.getName(), is("name")); assertThat(site.getUrl(), is("url")); } @@ -122,12 +122,23 @@ public void createSite() { @Test(expected = IllegalArgumentException.class) public void saveSiteBadSite() { ReplicationSite site = mock(ReplicationSite.class); - store.save(site); + siteManager.save(site); } @Test public void deleteSite() { - store.remove("id"); + siteManager.remove("id"); verify(persistentStore).delete(eq(ReplicationSiteImpl.class), eq("id")); } + + @Test + public void testExistsNotFound() { + when(persistentStore.get(ReplicationSiteImpl.class, "id")).thenThrow(NotFoundException.class); + assertThat(siteManager.exists("id"), is(false)); + } + + @Test + public void testExistsConfigFound() { + assertThat(siteManager.exists("id"), is(true)); + } } diff --git a/replication-api/src/main/java/org/codice/ditto/replication/api/ReplicationPersistentStore.java b/replication-api/src/main/java/org/codice/ditto/replication/api/ReplicationItemManager.java similarity index 85% rename from replication-api/src/main/java/org/codice/ditto/replication/api/ReplicationPersistentStore.java rename to replication-api/src/main/java/org/codice/ditto/replication/api/ReplicationItemManager.java index bd6195382..cab096760 100644 --- a/replication-api/src/main/java/org/codice/ditto/replication/api/ReplicationPersistentStore.java +++ b/replication-api/src/main/java/org/codice/ditto/replication/api/ReplicationItemManager.java @@ -17,9 +17,9 @@ import java.util.Optional; import org.codice.ddf.persistence.PersistenceException; -public interface ReplicationPersistentStore { +public interface ReplicationItemManager { - Optional getItem(String id, String source, String destination); + Optional getItem(String metacardId, String source, String destination); List getItemsForConfig(String configId, int startIndex, int pageSize) throws PersistenceException; @@ -28,7 +28,7 @@ List getItemsForConfig(String configId, int startIndex, int pag void deleteAllItems() throws PersistenceException; - void deleteItem(String id, String source, String destination); + void deleteItem(String metacardId, String source, String destination); List getFailureList(int maximumFailureCount, String source, String destination); diff --git a/replication-api/src/main/java/org/codice/ditto/replication/api/ReplicatorHistory.java b/replication-api/src/main/java/org/codice/ditto/replication/api/ReplicatorHistory.java index fa8d1edc3..0d2c20448 100644 --- a/replication-api/src/main/java/org/codice/ditto/replication/api/ReplicatorHistory.java +++ b/replication-api/src/main/java/org/codice/ditto/replication/api/ReplicatorHistory.java @@ -29,15 +29,17 @@ public interface ReplicatorHistory { /** * Get all replication events for a given replication configuration * - * @param replicatorid replication configuration id + * @param replicationConfigId replication configuration id * @return List of associated replication events */ - List getReplicationEvents(String replicatorid); + List getReplicationEvents(String replicationConfigId); /** * Add a replication event to the history * * @param replicationStatus ReplicationConfig event to store + * @throws ReplicationPersistenceException if there is an error adding the {@link + * ReplicationStatus} */ void addReplicationEvent(ReplicationStatus replicationStatus); @@ -45,6 +47,8 @@ public interface ReplicatorHistory { * Remove a replication event from the history * * @param replicationStatus replication event to remove + * @throws ReplicationPersistenceException if there is an error deleting the {@link + * ReplicationStatus} */ void removeReplicationEvent(ReplicationStatus replicationStatus); @@ -52,6 +56,7 @@ public interface ReplicatorHistory { * Remove set of replication events from the history * * @param ids replication event ids + * @throws ReplicationPersistenceException if there is an error deleting 1 or more provided ids. */ void removeReplicationEvents(Set ids); } diff --git a/replication-api/src/main/java/org/codice/ditto/replication/api/data/ReplicatorConfig.java b/replication-api/src/main/java/org/codice/ditto/replication/api/data/ReplicatorConfig.java index fc69302af..ec34113e0 100644 --- a/replication-api/src/main/java/org/codice/ditto/replication/api/data/ReplicatorConfig.java +++ b/replication-api/src/main/java/org/codice/ditto/replication/api/data/ReplicatorConfig.java @@ -136,4 +136,37 @@ public interface ReplicatorConfig extends Persistable { * @param suspended the suspended state to give this config */ void setSuspended(boolean suspended); + + /** + * See {@link #shouldDeleteData()}. + * + * @return whether or not this {@code ReplicatorConfig} should be considered as deleted + */ + boolean isDeleted(); + + /** + * Marks this {@code ReplicatorConfig} as deleted. + * + * @param deleted whether or not this {@code ReplicatorConfig} should be considered as deleted + */ + void setDeleted(boolean deleted); + + /** + * Applies only when {@link #isDeleted()} returns {@code true}. + * + *

Only data that has been replicated to this {@link ReplicationSite} from a remote {@link + * ReplicationSite} will be deleted. + * + * @return if {@code true}, delete the associated data replicated by this {@code + * ReplicatorConfig}, otherwise retain the data. + */ + boolean shouldDeleteData(); + + /** + * See {@link #shouldDeleteData()}. + * + * @param deleteData {@code true} if this {@code ReplicatorConfig}'s data should be deleted, + * otherwise false + */ + void setDeleteData(boolean deleteData); } diff --git a/replication-api/src/main/java/org/codice/ditto/replication/api/persistence/DataManager.java b/replication-api/src/main/java/org/codice/ditto/replication/api/persistence/DataManager.java index 1cdb8d9dc..3aac8dc5b 100644 --- a/replication-api/src/main/java/org/codice/ditto/replication/api/persistence/DataManager.java +++ b/replication-api/src/main/java/org/codice/ditto/replication/api/persistence/DataManager.java @@ -21,8 +21,9 @@ public interface DataManager { * * @param id The object id * @return the T object with the given id - * @throws ReplicationPersistenceException if an error occurs while trying to retrieve the object - * @throws NotFoundException if an object with the given id cannot be found + * @throws {@link org.codice.ditto.replication.api.ReplicationPersistenceException} if an error + * occurs while trying to retrieve the object + * @throws {@link javax.ws.rs.NotFoundException} if an object with the given id cannot be found * @throws IllegalStateException if multiple objects were found with the given id */ T get(String id); @@ -31,7 +32,8 @@ public interface DataManager { * Gets all the currently saved T objects * * @return Stream of all T objects - * @throws ReplicationPersistenceException if an error occurs while trying to retrieve the objects + * @throws {@link org.codice.ditto.replication.api.ReplicationPersistenceException} if an error + * occurs while trying to retrieve the objects */ Stream objects(); @@ -41,7 +43,8 @@ public interface DataManager { * method included in this interface. * * @param object The T object to save or update - * @throws ReplicationPersistenceException if an error occurs while trying to save the config + * @throws {@link org.codice.ditto.replication.api.ReplicationPersistenceException} if an error + * occurs while trying to save the config * @throws IllegalArgumentException if the T implementation is not one that can be saved */ void save(T object); @@ -50,8 +53,17 @@ public interface DataManager { * Deletes a T object with the given id * * @param id The id of the object to be removed - * @throws ReplicationPersistenceException if an error occurs while trying to delete the config - * @throws NotFoundException if an object with the given id cannot be found + * @throws {@link org.codice.ditto.replication.api.ReplicationPersistenceException} if an error + * occurs while trying to delete the config + * @throws {@link javax.ws.rs.NotFoundException} if an object with the given id cannot be found */ void remove(String id); + + /** + * @param id unique id of the {@link Persistable} + * @return {@code true} if the {@link Persistable} exists, otherwise {@code false}. + * @throws {@link org.codice.ditto.replication.api.ReplicationPersistenceException} if there is an + * error accessing storage + */ + boolean exists(String id); } diff --git a/replication-api/src/main/java/org/codice/ditto/replication/api/persistence/ReplicatorConfigManager.java b/replication-api/src/main/java/org/codice/ditto/replication/api/persistence/ReplicatorConfigManager.java index 290f02fd9..5790bdbb0 100644 --- a/replication-api/src/main/java/org/codice/ditto/replication/api/persistence/ReplicatorConfigManager.java +++ b/replication-api/src/main/java/org/codice/ditto/replication/api/persistence/ReplicatorConfigManager.java @@ -15,12 +15,5 @@ import org.codice.ditto.replication.api.data.ReplicatorConfig; -/** Performs CRUD operations for {@link ReplicatorConfig}. */ -public interface ReplicatorConfigManager extends DataManager { - - /** - * @param configId unique id of {@link ReplicatorConfig} - * @return {@code true} if the config exists, otherwise {@code false}. - */ - boolean configExists(String configId); -} +/** Performs CRUD operations for {@link ReplicatorConfig}s. */ +public interface ReplicatorConfigManager extends DataManager {} diff --git a/replication-commands/src/main/java/org/codice/ditto/replication/commands/ConfigDeleteCommand.java b/replication-commands/src/main/java/org/codice/ditto/replication/commands/ConfigDeleteCommand.java index d6782dbcd..e559c3dce 100644 --- a/replication-commands/src/main/java/org/codice/ditto/replication/commands/ConfigDeleteCommand.java +++ b/replication-commands/src/main/java/org/codice/ditto/replication/commands/ConfigDeleteCommand.java @@ -44,7 +44,7 @@ import org.codice.ddf.persistence.PersistenceException; import org.codice.ditto.replication.api.ReplicationException; import org.codice.ditto.replication.api.ReplicationItem; -import org.codice.ditto.replication.api.ReplicationPersistentStore; +import org.codice.ditto.replication.api.ReplicationItemManager; import org.codice.ditto.replication.api.data.ReplicatorConfig; import org.codice.ditto.replication.api.mcard.Replication; import org.codice.ditto.replication.api.mcard.ReplicationConfig; @@ -106,7 +106,7 @@ public class ConfigDeleteCommand extends SubjectCommands { @Reference CatalogFramework framework; - @Reference ReplicationPersistentStore store; + @Reference ReplicationItemManager store; @Reference FilterBuilder builder; @@ -281,7 +281,7 @@ private void deleteBatch(String[] idsToDelete) throws SourceUnavailableException try { framework.delete(new DeleteRequestImpl(id)); } catch (IngestException e) { - LOGGER.debug("Failed to delete metacard with id:%s because of exception {}", id, e); + LOGGER.debug("Failed to delete metacard with id: {}", id, e); } } } diff --git a/ui/package.json b/ui/package.json index f8730a8ef..1091fc921 100644 --- a/ui/package.json +++ b/ui/package.json @@ -30,7 +30,7 @@ "graphql-tag": "2.10.0", "immutable": "3.8.2", "moment": "2.24.0", - "notistack": "0.5.0", + "notistack": "0.6.1", "prop-types": "15.6.2", "react": "16.8.3", "react-apollo": "2.3.3", @@ -74,10 +74,10 @@ ], "coverageThreshold": { "global": { - "statements": 7, + "statements": 6, "branches": 6, - "lines": 7, - "functions": 2 + "lines": 6, + "functions": 1 } }, "collectCoverage": true, diff --git a/ui/src/main/webapp/app.js b/ui/src/main/webapp/app.js index 7b227bf32..ed8eae730 100644 --- a/ui/src/main/webapp/app.js +++ b/ui/src/main/webapp/app.js @@ -1,3 +1,16 @@ +/** + * Copyright (c) Connexta + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ import React from 'react' import Home from './components/Home' import Navbar from './components/Navbar' diff --git a/ui/src/main/webapp/client.js b/ui/src/main/webapp/client.js index 628575fc8..d011a59c7 100644 --- a/ui/src/main/webapp/client.js +++ b/ui/src/main/webapp/client.js @@ -1,3 +1,16 @@ +/** + * Copyright (c) Connexta + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ import ApolloClient from 'apollo-client' import { InMemoryCache } from 'apollo-cache-inmemory' import { createHttpLink } from 'apollo-link-http' diff --git a/ui/src/main/webapp/components/Home.js b/ui/src/main/webapp/components/Home.js index 01ef05fe3..529c3ff96 100644 --- a/ui/src/main/webapp/components/Home.js +++ b/ui/src/main/webapp/components/Home.js @@ -1,3 +1,16 @@ +/** + * Copyright (c) Connexta + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ import React from 'react' import ReplicationsContainer from './replications/ReplicationsContainer' diff --git a/ui/src/main/webapp/components/Navbar.js b/ui/src/main/webapp/components/Navbar.js index 9c83054f4..c3f6d22e8 100644 --- a/ui/src/main/webapp/components/Navbar.js +++ b/ui/src/main/webapp/components/Navbar.js @@ -1,3 +1,16 @@ +/** + * Copyright (c) Connexta + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ /* global localStorage */ import React from 'react' import AppBar from '@material-ui/core/AppBar' @@ -30,7 +43,7 @@ const HelpDialog = props => { return (

- {'Welcome to the Project Charleston BETA'} + {'Welcome to the Project Charleston'} @@ -90,7 +103,7 @@ class Navbar extends React.Component { > - Project Charleston BETA + Project Charleston
This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ import React from 'react' import { CircularProgress } from '@material-ui/core' diff --git a/ui/src/main/webapp/components/common/Confirmable.js b/ui/src/main/webapp/components/common/Confirmable.js new file mode 100644 index 000000000..a39930cda --- /dev/null +++ b/ui/src/main/webapp/components/common/Confirmable.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) Connexta + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ +import React from 'react' +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + DialogContentText, +} from '@material-ui/core' + +function Confirmable(props) { + const { + onConfirm, + children, + message, + subMessage, + Button: Trigger, + onClose, + } = props + const [open, setOpen] = React.useState(false) + + return ( + <> +

{ + onClose() + setOpen(false) + }} + fullWidth + > + {message} + + {subMessage} + {children} + + + + + + + setOpen(true)} /> + + ) +} + +export default Confirmable diff --git a/ui/src/main/webapp/components/common/ServerError.js b/ui/src/main/webapp/components/common/ServerError.js index 741015f12..334d3af25 100644 --- a/ui/src/main/webapp/components/common/ServerError.js +++ b/ui/src/main/webapp/components/common/ServerError.js @@ -1,3 +1,16 @@ +/** + * Copyright (c) Connexta + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ import React from 'react' import { Grid, Typography } from '@material-ui/core' diff --git a/ui/src/main/webapp/components/replications/ActionsMenu.js b/ui/src/main/webapp/components/replications/ActionsMenu.js index 18fbc20d5..51db5b473 100644 --- a/ui/src/main/webapp/components/replications/ActionsMenu.js +++ b/ui/src/main/webapp/components/replications/ActionsMenu.js @@ -1,5 +1,27 @@ +/** + * Copyright (c) Connexta + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ import React from 'react' -import { Menu, MenuItem, Typography } from '@material-ui/core' +import { + Menu, + MenuItem, + Typography, + FormGroup, + FormControlLabel, + Checkbox, + Tooltip, +} from '@material-ui/core' +import HelpIcon from '@material-ui/icons/Help' import { deleteReplication, suspendReplication, @@ -8,50 +30,98 @@ import { import { allReplications } from './gql/queries' import { Mutation } from 'react-apollo' import Replications from './replications' -import { withSnackbar } from 'notistack' +import { withSnackbar, useSnackbar } from 'notistack' +import Confirmable from '../common/Confirmable' -const DeleteReplication = withSnackbar(props => { - const { id, onClose, name, enqueueSnackbar } = props +const DeleteReplication = withSnackbar( + class extends React.Component { + state = { + deleteData: false, + } - return ( - - {deleteReplication => ( - { - deleteReplication({ - variables: { - id: id, - }, - update: store => { - const data = store.readQuery({ - query: allReplications, - }) + handleCheck = name => event => { + this.setState({ [name]: event.target.checked }) + } - data.replication.replications = data.replication.replications.filter( - r => r.id !== id - ) - store.writeQuery({ - query: allReplications, - data, - }) + render() { + const { id, onClose, name, enqueueSnackbar } = this.props - enqueueSnackbar('Deleted ' + name + '.', { variant: 'success' }) - }, - }) - onClose() - }} - > - Delete - - )} - - ) -}) + return ( + + {deleteReplication => ( + { + deleteReplication({ + variables: { + id: id, + deleteData: this.state.deleteData, + }, + update: store => { + const data = store.readQuery({ + query: allReplications, + }) + + data.replication.replications = data.replication.replications.filter( + r => r.id !== id + ) + store.writeQuery({ + query: allReplications, + data, + }) + + enqueueSnackbar('Deleted ' + name + '.', { + variant: 'success', + }) + }, + }) + onClose() + }} + message={`Are you sure you want to delete ${name}?`} + subMessage={ + 'All historical statistics associated with this Replication will be removed in addition to the Replication.' + } + onClose={onClose} + Button={props => { + const { onClick } = props + return ( + + Delete + + ) + }} + > + +

+ + } + label='Delete Data?' + /> + + + +
+ + + )} + + ) + } + } +) DeleteReplication.displayName = 'DeleteReplication' -const SuspendReplication = withSnackbar(props => { - const { id, suspend, key, label, onClose, enqueueSnackbar, name } = props +const SuspendReplication = props => { + const { id, suspend, key, label, onClose, name } = props + const { enqueueSnackbar } = useSnackbar() return ( @@ -98,11 +168,11 @@ const SuspendReplication = withSnackbar(props => { )} ) -}) -SuspendReplication.displayName = 'SuspendReplication' +} -const CancelReplication = withSnackbar(props => { - const { id, onClose, name, enqueueSnackbar } = props +const CancelReplication = props => { + const { id, onClose, name } = props + const { enqueueSnackbar } = useSnackbar() return ( @@ -149,8 +219,7 @@ const CancelReplication = withSnackbar(props => { )} ) -}) -CancelReplication.displayName = 'CancelReplication' +} const ActionsMenu = function(props) { const { menuId, anchorEl = null, onClose, replication } = props diff --git a/ui/src/main/webapp/components/replications/AddReplication.js b/ui/src/main/webapp/components/replications/AddReplication.js index d9146f9f2..396ac5b36 100644 --- a/ui/src/main/webapp/components/replications/AddReplication.js +++ b/ui/src/main/webapp/components/replications/AddReplication.js @@ -1,3 +1,16 @@ +/** + * Copyright (c) Connexta + * + *

This is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or any later version. + * + *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. A copy of the GNU Lesser General Public + * License is distributed along with this program and can be found at + * . + */ import React from 'react' import { MenuItem, @@ -83,6 +96,7 @@ const defaultFormState = { biDirectional: false, filterErrorText: '', nameErrorText: '', + disableSave: false, } class AddReplication extends React.Component { @@ -141,6 +155,18 @@ class AddReplication extends React.Component { }) } + disableSaveButton() { + this.setState({ + disableSave: true, + }) + } + + enableSaveButton() { + this.setState({ + disableSave: false, + }) + } + render() { const { Button: AddButton, classes } = this.props const { @@ -152,6 +178,7 @@ class AddReplication extends React.Component { biDirectional, filterErrorText, nameErrorText, + disableSave = false, } = this.state return ( @@ -262,6 +289,7 @@ class AddReplication extends React.Component { } else if (e.message === 'DUPLICATE_CONFIGURATION') { this.handleInvalidName() } + this.enableSaveButton() }) }} onCompleted={() => { @@ -271,9 +299,13 @@ class AddReplication extends React.Component { {(createReplication, { loading }) => (