/**
* Copyright (c) Codice Foundation
* <p>
* 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.
* <p>
* 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
* <http://www.gnu.org/licenses/lgpl.html>.
**/
package org.codice.ddf.registry.federationadmin.service.impl;
import java.io.Serializable;
import java.security.PrivilegedActionException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.collections.CollectionUtils;
import org.codice.ddf.registry.api.internal.RegistryStore;
import org.codice.ddf.registry.common.RegistryConstants;
import org.codice.ddf.registry.common.metacard.RegistryObjectMetacardType;
import org.codice.ddf.registry.common.metacard.RegistryUtility;
import org.codice.ddf.registry.federationadmin.service.internal.FederationAdminException;
import org.codice.ddf.registry.federationadmin.service.internal.FederationAdminService;
import org.codice.ddf.security.common.Security;
import org.geotools.filter.SortByImpl;
import org.opengis.filter.Filter;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ddf.catalog.data.Metacard;
import ddf.catalog.data.Result;
import ddf.catalog.filter.FilterBuilder;
import ddf.catalog.filter.impl.PropertyNameImpl;
import ddf.catalog.operation.Query;
import ddf.catalog.operation.SourceResponse;
import ddf.catalog.operation.impl.QueryImpl;
import ddf.catalog.operation.impl.QueryRequestImpl;
import ddf.security.SecurityConstants;
/**
* Though refreshRegistrySubscriptions in this class is a public method, it should never be called by any
* class with external connections. This class is only meant to be accessed through a camel
* route and should avoid elevating privileges for any other service or being exposed to any
* other endpoint.
*/
public class RefreshRegistryEntries {
private static final Logger LOGGER = LoggerFactory.getLogger(RefreshRegistryEntries.class);
private static final int PAGE_SIZE = 1000;
private static final int SHUTDOWN_TIMEOUT_SECONDS = 60;
private List<RegistryStore> registryStores;
private FederationAdminService federationAdminService;
private FilterBuilder filterBuilder;
private boolean enableDelete = true;
private ScheduledExecutorService executor;
private int taskWaitTimeSeconds = 30;
private int refreshIntervalSeconds = 30;
private Future scheduledTask;
private Security security;
public RefreshRegistryEntries() {
this.security = Security.getInstance();
}
public RefreshRegistryEntries(Security security){
this.security = security;
}
public void init() {
scheduledTask = executor.scheduleWithFixedDelay(() -> {
try {
refreshRegistryEntries();
} catch (FederationAdminException e) {
LOGGER.error("Problem refreshing registry entries.", e);
}
}, 10, refreshIntervalSeconds, TimeUnit.SECONDS);
}
/**
* Do not call refreshRegistrySubscriptions directly! It is a public method, but is only meant
* to be accessed through a camel route and should avoid elevating privileges for any other
* service or being exposed to any other endpoint.
*
* @throws FederationAdminException
*/
public void refreshRegistryEntries() throws FederationAdminException {
if (!registriesAvailable()) {
return;
}
RemoteRegistryResults remoteResults = getRemoteRegistryMetacardsMap();
Map<String, Metacard> remoteRegistryMetacardsMap =
remoteResults.getRemoteRegistryMetacards();
Map<String, Metacard> registryMetacardsMap = getRegistryMetacardsMap();
List<Metacard> remoteMetacardsToUpdate = new ArrayList<>();
List<Metacard> remoteMetacardsToCreate = new ArrayList<>();
List<Metacard> remoteMetacardsToDelete = new ArrayList<>();
Map<String, List<Metacard>> remoteRegistryToMetacardMap = getMetacardRegistryIdMap(
remoteRegistryMetacardsMap.values());
Map<String, List<Metacard>> localRegistryToMetacardMap = getMetacardRegistryIdMap(
registryMetacardsMap.values());
for (String regId : remoteResults.getRegistryStoresQueried()) {
if (!localRegistryToMetacardMap.containsKey(regId) || remoteResults.getFailureList()
.contains(regId)) {
continue;
}
if (remoteRegistryToMetacardMap.containsKey(regId)) {
remoteMetacardsToDelete.addAll(localRegistryToMetacardMap.get(regId)
.stream()
.filter(e -> !hasMatch(e, remoteRegistryToMetacardMap.get(regId)))
.collect(Collectors.toList()));
} else {
remoteMetacardsToDelete.addAll(localRegistryToMetacardMap.get(regId));
}
}
for (Map.Entry<String, Metacard> remoteEntry : remoteRegistryMetacardsMap.entrySet()) {
if (registryMetacardsMap.containsKey(remoteEntry.getKey())) {
Metacard existingMetacard = registryMetacardsMap.get(remoteEntry.getKey());
if (!RegistryUtility.isLocalNode(existingMetacard) && remoteEntry.getValue()
.getModifiedDate()
.after(existingMetacard.getModifiedDate())) {
remoteMetacardsToUpdate.add(remoteEntry.getValue());
}
} else {
remoteMetacardsToCreate.add(remoteEntry.getValue());
}
}
if (CollectionUtils.isNotEmpty(remoteMetacardsToUpdate)) {
writeRemoteUpdates(remoteMetacardsToUpdate);
}
if (CollectionUtils.isNotEmpty(remoteMetacardsToCreate)) {
createRemoteEntries(remoteMetacardsToCreate);
}
if (enableDelete && !remoteMetacardsToDelete.isEmpty()) {
deleteRemoteEntries(remoteMetacardsToDelete);
}
}
private boolean hasMatch(Metacard local, List<Metacard> remoteMetacards) {
String id = RegistryUtility.getStringAttribute(local,
RegistryObjectMetacardType.REMOTE_METACARD_ID,
"");
return remoteMetacards.stream()
.filter(e -> e.getId()
.equals(id))
.findFirst()
.isPresent();
}
private boolean registriesAvailable() {
return registryStores.stream()
.filter(RegistryStore::isAvailable)
.findFirst()
.isPresent();
}
private Map<String, List<Metacard>> getMetacardRegistryIdMap(Collection<Metacard> metacards) {
Map<String, List<Metacard>> map = new HashMap<>();
for (Metacard mcard : metacards) {
String regId = RegistryUtility.getStringAttribute(mcard,
RegistryObjectMetacardType.REMOTE_REGISTRY_ID,
"");
map.computeIfAbsent(regId, k -> new ArrayList<>());
map.get(regId)
.add(mcard);
}
return map;
}
private Map<String, Metacard> getRegistryMetacardsMap() throws FederationAdminException {
try {
List<Metacard> registryMetacards =
security.runAsAdminWithException(() -> federationAdminService.getInternalRegistryMetacards());
return registryMetacards.stream()
.collect(Collectors.toMap(e -> RegistryUtility.getStringAttribute(e,
RegistryObjectMetacardType.REMOTE_METACARD_ID,
null), Function.identity()));
} catch (Exception e) {
throw new FederationAdminException("Error querying for metacards ", e);
}
}
private RemoteRegistryResults getRemoteRegistryMetacardsMap() throws FederationAdminException {
Map<String, Metacard> remoteRegistryMetacards = new HashMap<>();
List<String> failedQueries = new ArrayList<>();
List<String> storesQueried = new ArrayList<>();
List<String> localMetacardRegIds = getLocalRegistryIds();
Map<String, Serializable> queryProps = new HashMap<>();
queryProps.put(SecurityConstants.SECURITY_SUBJECT, security.runAsAdmin(() -> security.getSystemSubject()));
//Create the remote query task to be run.
List<Callable<RemoteResult>> tasks = new ArrayList<>();
for (RegistryStore store : registryStores) {
if (!store.isPullAllowed() || !store.isAvailable()) {
continue;
}
storesQueried.add(store.getRegistryId());
tasks.add(() -> {
SourceResponse response =
store.query(new QueryRequestImpl(getBasicRegistryQuery(), queryProps));
Map<String, Metacard> results = response.getResults()
.stream()
.map(Result::getMetacard)
.filter(e -> !localMetacardRegIds.contains(RegistryUtility.getRegistryId(e)))
.collect(Collectors.toMap(Metacard::getId, Function.identity()));
return new RemoteResult(store.getRegistryId(), results);
});
}
failedQueries.addAll(storesQueried);
List<RemoteResult> results = executeTasks(tasks);
results.stream()
.forEach(result -> {
failedQueries.remove(result.getRegistryId());
remoteRegistryMetacards.putAll(result.getRemoteRegistryMetacards());
});
return new RemoteRegistryResults(remoteRegistryMetacards, failedQueries, storesQueried);
}
private List<RemoteResult> executeTasks(List<Callable<RemoteResult>> tasks) {
List<RemoteResult> results = new ArrayList<>();
try {
List<Future<RemoteResult>> futures = executor.invokeAll(tasks);
for (Future<RemoteResult> future : futures) {
try {
results.add(future.get(taskWaitTimeSeconds, TimeUnit.SECONDS));
} catch (ExecutionException e) {
LOGGER.debug("Error executing query on a remote registry.", e);
} catch (TimeoutException e) {
LOGGER.debug("Timeout occurred when querying a remote registry");
}
}
} catch (InterruptedException e) {
LOGGER.debug("Remote registry queries interrupted", e);
}
return results;
}
private List<String> getLocalRegistryIds() throws FederationAdminException {
try {
List<Metacard> localMetacards =
security.runAsAdminWithException(() -> federationAdminService.getLocalRegistryMetacards());
return localMetacards.stream()
.map(e -> RegistryUtility.getRegistryId(e))
.collect(Collectors.toList());
} catch (Exception e) {
throw new FederationAdminException("Error querying for local metacards ", e);
}
}
private void writeRemoteUpdates(List<Metacard> remoteMetacardsToUpdate)
throws FederationAdminException {
try {
security.runAsAdminWithException(() -> {
for (Metacard m : remoteMetacardsToUpdate) {
federationAdminService.updateRegistryEntry(m);
}
return null;
});
} catch (PrivilegedActionException e) {
String message = "Error writing remote updates.";
LOGGER.debug("{} Metacard IDs: {}", message, remoteMetacardsToUpdate);
throw new FederationAdminException(message, e);
}
}
private void createRemoteEntries(List<Metacard> remoteMetacardsToCreate)
throws FederationAdminException {
try {
security.runAsAdminWithException(() -> federationAdminService.addRegistryEntries(
remoteMetacardsToCreate,
null));
} catch (PrivilegedActionException e) {
throw new FederationAdminException("Error creating remote entries", e);
}
}
private void deleteRemoteEntries(List<Metacard> remoteMetacardsToDelete)
throws FederationAdminException {
try {
security.runAsAdminWithException(() -> {
federationAdminService.deleteRegistryEntriesByMetacardIds(remoteMetacardsToDelete.stream()
.map(Metacard::getId)
.collect(Collectors.toList()));
return null;
});
} catch (PrivilegedActionException e) {
String message = "Error deleting remote entries.";
LOGGER.debug("{} Metacard IDs: {}", message, remoteMetacardsToDelete);
throw new FederationAdminException(message, e);
}
}
private Query getBasicRegistryQuery() {
List<Filter> filters = new ArrayList<>();
filters.add(filterBuilder.attribute(Metacard.TAGS)
.is()
.equalTo()
.text(RegistryConstants.REGISTRY_TAG));
PropertyName propertyName = new PropertyNameImpl(Metacard.MODIFIED);
SortBy sortBy = new SortByImpl(propertyName, SortOrder.ASCENDING);
QueryImpl query = new QueryImpl(filterBuilder.allOf(filters));
query.setSortBy(sortBy);
query.setPageSize(PAGE_SIZE);
return query;
}
public void setFilterBuilder(FilterBuilder filterBuilder) {
this.filterBuilder = filterBuilder;
}
public void setRegistryStores(List<RegistryStore> registryStores) {
this.registryStores = registryStores;
}
public void setFederationAdminService(FederationAdminService federationAdminService) {
this.federationAdminService = federationAdminService;
}
public void setEnableDelete(boolean enableDelete) {
this.enableDelete = enableDelete;
}
public void setExecutor(ScheduledExecutorService executor) {
this.executor = executor;
}
public void setRefreshIntervalSeconds(int refreshIntervalSeconds) {
if (this.refreshIntervalSeconds == refreshIntervalSeconds) {
return;
}
this.refreshIntervalSeconds = refreshIntervalSeconds;
if (scheduledTask != null) {
this.scheduledTask.cancel(false);
this.init();
}
}
public void setTaskWaitTimeSeconds(Integer taskWaitTimeSeconds) {
this.taskWaitTimeSeconds = taskWaitTimeSeconds;
}
public void destroy() {
executor.shutdown();
try {
if (!executor.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
LOGGER.error("Thread pool didn't terminate");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
static class RemoteResult {
private String registryId;
private Map<String, Metacard> remoteRegistryMetacards;
public RemoteResult(String registryId, Map<String, Metacard> remoteRegistryMetacards) {
this.registryId = registryId;
this.remoteRegistryMetacards = remoteRegistryMetacards;
}
public String getRegistryId() {
return registryId;
}
public Map<String, Metacard> getRemoteRegistryMetacards() {
return remoteRegistryMetacards;
}
}
private static class RemoteRegistryResults {
private Map<String, Metacard> remoteRegistryMetacards;
private List<String> registryStoresQueried;
private List<String> failureList;
public RemoteRegistryResults(Map<String, Metacard> remoteRegistryMetacards,
List<String> failureList, List<String> storesQueried) {
this.remoteRegistryMetacards = remoteRegistryMetacards;
this.failureList = failureList;
this.registryStoresQueried = storesQueried;
}
public Map<String, Metacard> getRemoteRegistryMetacards() {
return new HashMap<>(remoteRegistryMetacards);
}
public List<String> getRegistryStoresQueried() {
return new ArrayList<>(registryStoresQueried);
}
public List<String> getFailureList() {
return new ArrayList<>(failureList);
}
}
}