/** * 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.catalog.plugin.metacard.backup; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.codice.ddf.catalog.async.data.api.internal.ProcessCreateItem; import org.codice.ddf.catalog.async.data.api.internal.ProcessDeleteItem; import org.codice.ddf.catalog.async.data.api.internal.ProcessRequest; import org.codice.ddf.catalog.async.data.api.internal.ProcessResourceItem; import org.codice.ddf.catalog.async.data.api.internal.ProcessUpdateItem; import org.codice.ddf.catalog.async.plugin.api.internal.PostProcessPlugin; import org.codice.ddf.catalog.plugin.metacard.backup.storage.internal.MetacardBackupException; import org.codice.ddf.catalog.plugin.metacard.backup.storage.internal.MetacardBackupStorageProvider; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.data.Attribute; import ddf.catalog.data.BinaryContent; import ddf.catalog.data.Metacard; import ddf.catalog.data.types.Core; import ddf.catalog.plugin.PluginExecutionException; import ddf.catalog.transform.CatalogTransformerException; import ddf.catalog.transform.MetacardTransformer; /** * The MetacardBackupPlugin asynchronously backs up a Metacard using a configured transformer to the file system. * It implements the PostProcessPlugin in order to maintain synchronization with the catalog (CRUD). * <p> * The root backup directory can be configured in the MetacardBackupPlugin section in the admin console. */ public class MetacardBackupPlugin implements PostProcessPlugin { private static final Logger LOGGER = LoggerFactory.getLogger(MetacardBackupPlugin.class); private static final String KEEP_DELETED_METACARDS_PROPERTY = "keepDeletedMetacards"; private static final String METACARD_TRANSFORMER_ID_PROPERTY = "metacardTransformerId"; private static final String OUTPUT_PROVIDER_PROPERTY = "metacardOutputProviderIds"; private static final String BACKUP_INVALID_METACARDS_PROPERTY = "backupInvalidMetacards"; private static final String INVALID_TAG = "INVALID"; private Boolean keepDeletedMetacards = false; private String metacardTransformerId; private MetacardTransformer metacardTransformer; private Boolean backupInvalidMetacards = true; private List<String> metacardOutputProviderIds = new ArrayList<>(); private List<MetacardBackupStorageProvider> storageBackupPlugins = Collections.emptyList(); @Override public ProcessRequest<ProcessCreateItem> processCreate( ProcessRequest<ProcessCreateItem> processRequest) throws PluginExecutionException { processRequest(processRequest); return processRequest; } @Override public ProcessRequest<ProcessUpdateItem> processUpdate( ProcessRequest<ProcessUpdateItem> processRequest) throws PluginExecutionException { processRequest(processRequest); return processRequest; } @Override public ProcessRequest<ProcessDeleteItem> processDelete( ProcessRequest<ProcessDeleteItem> processRequest) throws PluginExecutionException { if (keepDeletedMetacards) { return processRequest; } if (CollectionUtils.isEmpty(storageBackupPlugins)) { throw new PluginExecutionException( "Unable to delete backup ingested metacard; no metacard backup storage provider configured."); } List<ProcessDeleteItem> processUpdateItems = processRequest.getProcessItems(); for (ProcessDeleteItem processUpdateItem : processUpdateItems) { Metacard metacard = processUpdateItem.getMetacard(); for (MetacardBackupStorageProvider storageProvider : storageBackupPlugins) { if (metacardOutputProviderIds.contains(storageProvider.getId())) { try { storageProvider.delete(metacard.getId()); } catch (IOException | MetacardBackupException e) { LOGGER.debug( "Unable to delete backed up metacard data for metacard: {} from: {}", metacard.getId(), storageProvider.getId(), e); } } } } return processRequest; } public void setKeepDeletedMetacards(Boolean keepDeletedMetacards) { this.keepDeletedMetacards = keepDeletedMetacards; } public Boolean getKeepDeletedMetacards() { return keepDeletedMetacards; } public void setMetacardTransformerId(String metacardTransformerId) { this.metacardTransformerId = metacardTransformerId; this.metacardTransformer = lookupTransformerReference(); } public void setMetacardTransformer(MetacardTransformer metacardTransformer) { this.metacardTransformer = metacardTransformer; } public String getMetacardTransformerId() { return metacardTransformerId; } public void setStorageBackupPlugins(List<MetacardBackupStorageProvider> storageBackupPlugins) { if (storageBackupPlugins != null) { this.storageBackupPlugins = storageBackupPlugins; } else { this.storageBackupPlugins = Collections.emptyList(); } } public List<MetacardBackupStorageProvider> getStorageBackupPlugins() { return storageBackupPlugins; } public void setMetacardOutputProviderIds(List<String> metacardOutputProviderIds) { this.metacardOutputProviderIds = metacardOutputProviderIds; } public Boolean getBackupInvalidMetacards() { return backupInvalidMetacards; } public void setBackupInvalidMetacards(Boolean backupInvalidMetacards) { if (backupInvalidMetacards != null) { this.backupInvalidMetacards = backupInvalidMetacards; } else { this.backupInvalidMetacards = false; } } public void refresh(Map<String, Object> properties) { Object metacardTransformerProperty = properties.get(METACARD_TRANSFORMER_ID_PROPERTY); if (metacardTransformerProperty instanceof String && StringUtils.isNotBlank((String) metacardTransformerProperty)) { setMetacardTransformerId((String) metacardTransformerProperty); } Object keepDeletedMetacards = properties.get(KEEP_DELETED_METACARDS_PROPERTY); if (keepDeletedMetacards instanceof Boolean) { this.keepDeletedMetacards = (Boolean) keepDeletedMetacards; } Object metacardOutputProviderIds = properties.get(OUTPUT_PROVIDER_PROPERTY); if (metacardOutputProviderIds instanceof String[]) { this.metacardOutputProviderIds = Arrays.asList((String[]) metacardOutputProviderIds); } Object backupInvalidMetacards = properties.get(BACKUP_INVALID_METACARDS_PROPERTY); if (backupInvalidMetacards instanceof Boolean) { this.backupInvalidMetacards = (Boolean) backupInvalidMetacards; } } private void processRequest(ProcessRequest<? extends ProcessResourceItem> processRequest) throws PluginExecutionException { if (CollectionUtils.isEmpty(storageBackupPlugins)) { throw new PluginExecutionException( "Unable to backup ingested metacard; no metacard backup storage provider configured."); } if (metacardTransformer == null) { throw new PluginExecutionException( "Unable to backup ingested metacard; no Metacard Transformer found."); } List<? extends ProcessResourceItem> processResourceItems = processRequest.getProcessItems(); for (ProcessResourceItem processResourceItem : processResourceItems) { Metacard metacard = processResourceItem.getMetacard(); if (shouldBackupMetacard(metacard)) { try { LOGGER.trace("Backing up metacard : {}", metacard.getId()); BinaryContent binaryContent = metacardTransformer.transform(metacard, Collections.emptyMap()); backupData(binaryContent, metacard.getId()); } catch (CatalogTransformerException e) { LOGGER.debug("Unable to transform metacard with id {}.", metacard.getId(), e); throw new PluginExecutionException(String.format( "Unable to transform metacard with id %s.", metacard.getId())); } } } } private void backupData(BinaryContent content, String metacardId) throws PluginExecutionException { byte[] contentBytes = getContentBytes(content, metacardId); LOGGER.trace("Writing backup from {} to backup provider(s)", metacardId); for (MetacardBackupStorageProvider storageProvider : storageBackupPlugins) { if (metacardOutputProviderIds.contains(storageProvider.getId())) { try { storageProvider.store(metacardId, contentBytes); } catch (IOException | MetacardBackupException e) { LOGGER.debug("Unable to backup {} to backup provider: {}.", metacardId, storageProvider.getId(), e); } } } } byte[] getContentBytes(BinaryContent content, String metacardId) throws PluginExecutionException { return Optional.ofNullable(content) .map(c -> { try { return c.getByteArray(); } catch (IOException e) { return null; } }) .orElseThrow(() -> { LOGGER.debug("No content for transformed metacard {}", metacardId); return new PluginExecutionException(String.format( "No content for transformed metacard %s", metacardId)); }); } private MetacardTransformer lookupTransformerReference() { Bundle bundle = FrameworkUtil.getBundle(this.getClass()); if (bundle != null) { BundleContext bundleContext = bundle.getBundleContext(); try { Collection<ServiceReference<MetacardTransformer>> transformerReference = bundleContext.getServiceReferences(MetacardTransformer.class, "(id=" + metacardTransformerId + ")"); return bundleContext.getService(transformerReference.iterator() .next()); } catch (InvalidSyntaxException | NoSuchElementException e) { LOGGER.warn( "Unable to resolve MetacardTransformer {}. Backup will not be performed.", metacardTransformerId, e); } } return null; } private boolean shouldBackupMetacard(Metacard metacard) { if (backupInvalidMetacards) { return true; } else { Attribute metacardTagsAttr = metacard.getAttribute(Core.METACARD_TAGS); if (metacardTagsAttr != null) { return metacardTagsAttr.getValues() .stream() .filter(String.class::isInstance) .map(String.class::cast) .noneMatch(INVALID_TAG::equalsIgnoreCase); } return true; } } }