/** * 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.validator.metacard.duplication; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.ArrayUtils; import org.opengis.filter.Filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; import ddf.catalog.CatalogFramework; import ddf.catalog.data.Attribute; import ddf.catalog.data.Metacard; import ddf.catalog.federation.FederationException; import ddf.catalog.filter.FilterBuilder; import ddf.catalog.operation.QueryRequest; import ddf.catalog.operation.SourceResponse; import ddf.catalog.operation.impl.QueryImpl; import ddf.catalog.operation.impl.QueryRequestImpl; import ddf.catalog.source.SourceUnavailableException; import ddf.catalog.source.UnsupportedQueryException; import ddf.catalog.validation.MetacardValidator; import ddf.catalog.validation.ReportingMetacardValidator; import ddf.catalog.validation.ValidationException; import ddf.catalog.validation.impl.ValidationExceptionImpl; import ddf.catalog.validation.impl.report.MetacardValidationReportImpl; import ddf.catalog.validation.impl.violation.ValidationViolationImpl; import ddf.catalog.validation.report.MetacardValidationReport; import ddf.catalog.validation.violation.ValidationViolation; public class DuplicationValidator implements MetacardValidator, ReportingMetacardValidator, ddf.catalog.util.Describable, org.codice.ddf.platform.services.common.Describable { private static final Logger LOGGER = LoggerFactory.getLogger(DuplicationValidator.class); private static final String DESCRIBABLE_PROPERTIES_FILE = "/describable.properties"; private static final String ORGANIZATION = "organization"; private static final String VERSION = "version"; private static Properties describableProperties = new Properties(); static { try (InputStream properties = DuplicationValidator.class.getResourceAsStream( DESCRIBABLE_PROPERTIES_FILE)) { describableProperties.load(properties); } catch (IOException e) { LOGGER.info("Failed to load properties", e); } } private final CatalogFramework catalogFramework; private final FilterBuilder filterBuilder; private String[] errorOnDuplicateAttributes; private String[] warnOnDuplicateAttributes; public DuplicationValidator(CatalogFramework catalogFramework, FilterBuilder filterBuilder) { this.catalogFramework = catalogFramework; this.filterBuilder = filterBuilder; } /** * Setter for the list of attributes to test for duplication in the local catalog. Resulting * attributes will cause the {@link ddf.catalog.data.types.Validation#VALIDATION_ERRORS} attribute * to be set on the metacard. * * @param attributeStrings */ public void setErrorOnDuplicateAttributes(String[] attributeStrings) { if (attributeStrings != null) { this.errorOnDuplicateAttributes = Arrays.copyOf(attributeStrings, attributeStrings.length); } } /** * Setter for the list of attributes to test for duplication in the local catalog. Resulting * attributes will cause the {@link ddf.catalog.data.types.Validation#VALIDATION_WARNINGS} attribute * to be set on the metacard. * * @param attributeStrings */ public void setWarnOnDuplicateAttributes(String[] attributeStrings) { if (attributeStrings != null) { this.warnOnDuplicateAttributes = Arrays.copyOf(attributeStrings, attributeStrings.length); } } @Override public Optional<MetacardValidationReport> validateMetacard(Metacard metacard) { Preconditions.checkArgument(metacard != null, "The metacard cannot be null."); return getReport(reportDuplicates(metacard)); } @Override public void validate(Metacard metacard) throws ValidationException { final Optional<MetacardValidationReport> report = validateMetacard(metacard); if (report.isPresent()) { final List<String> errors = report.get() .getMetacardValidationViolations() .stream() .filter(validationViolation -> validationViolation.getSeverity() .equals(ValidationViolation.Severity.ERROR)) .map(ValidationViolation::getMessage) .collect(Collectors.toList()); final List<String> warnings = report.get() .getMetacardValidationViolations() .stream() .filter(validationViolation -> validationViolation.getSeverity() .equals(ValidationViolation.Severity.WARNING)) .map(ValidationViolation::getMessage) .collect(Collectors.toList()); String message = String.format("Duplicate data found in catalog for ID {%s}.", metacard.getId()); final ValidationExceptionImpl exception = new ValidationExceptionImpl(message); exception.setErrors(errors); exception.setWarnings(warnings); throw exception; } } private Set<ValidationViolation> reportDuplicates(final Metacard metacard) { Set<ValidationViolation> violations = new HashSet<>(); if (ArrayUtils.isNotEmpty(warnOnDuplicateAttributes)) { ValidationViolation warnValidation = reportDuplicates(metacard, warnOnDuplicateAttributes, ValidationViolation.Severity.WARNING); if (warnValidation != null) { violations.add(warnValidation); } } if (ArrayUtils.isNotEmpty(errorOnDuplicateAttributes)) { ValidationViolation errorViolation = reportDuplicates(metacard, errorOnDuplicateAttributes, ValidationViolation.Severity.ERROR); if (errorViolation != null) { violations.add(errorViolation); } } return violations; } private ValidationViolation reportDuplicates(final Metacard metacard, String[] attributeNames, ValidationViolation.Severity severity) { Set<String> duplicates = new HashSet<>(); ValidationViolation violation = null; final Set<String> uniqueAttributeNames = Stream.of(attributeNames) .filter(attribute -> metacard.getAttribute(attribute) != null) .collect(Collectors.toSet()); final Set<Attribute> uniqueAttributes = uniqueAttributeNames.stream() .map(attribute -> metacard.getAttribute(attribute)) .collect(Collectors.toSet()); if (!uniqueAttributes.isEmpty()) { LOGGER.debug("Checking for duplicates for id {} against attributes [{}]", metacard.getId(), collectionToString(uniqueAttributeNames)); SourceResponse response = query(uniqueAttributes, metacard.getId()); if (response != null) { response.getResults() .forEach(result -> duplicates.add(result.getMetacard() .getId())); } if (!duplicates.isEmpty()) { violation = createViolation(uniqueAttributeNames, duplicates, severity); LOGGER.debug(violation.getMessage()); } } return violation; } private Filter[] buildFilters(Set<Attribute> attributes) { Filter[] filters = attributes.stream() .flatMap(attribute -> { return attribute.getValues() .stream() .map(value -> filterBuilder.attribute(attribute.getName()) .equalTo() .text(value.toString() .trim())); }) .toArray(Filter[]::new); return filters; } private SourceResponse query(Set<Attribute> attributes, String originalId) { final Filter filter = filterBuilder.allOf(filterBuilder.anyOf(buildFilters(attributes)), filterBuilder.not(filterBuilder.attribute(Metacard.ID) .is() .equalTo() .text(originalId))); LOGGER.debug("filter {}", filter); QueryImpl query = new QueryImpl(filter); query.setRequestsTotalResultsCount(false); QueryRequest request = new QueryRequestImpl(query); SourceResponse response = null; try { response = catalogFramework.query(request); } catch (FederationException | SourceUnavailableException | UnsupportedQueryException e) { LOGGER.debug("Query failed ", e); } return response; } private ValidationViolation createViolation(final Set<String> attributes, Set<String> duplicates, ValidationViolation.Severity severity) { return new ValidationViolationImpl(attributes, String.format("Duplicate data found in catalog: {%s}, based on attributes: {%s}.", collectionToString(duplicates), collectionToString(attributes)), severity); } private String collectionToString(final Collection collection) { return (String) collection.stream() .map(Object::toString) .sorted() .collect(Collectors.joining(", ")); } private Optional<MetacardValidationReport> getReport( final Set<ValidationViolation> violations) { if (CollectionUtils.isNotEmpty(violations)) { final MetacardValidationReportImpl report = new MetacardValidationReportImpl(); violations.forEach(report::addMetacardViolation); return Optional.of(report); } return Optional.empty(); } @Override public String getVersion() { return describableProperties.getProperty(VERSION); } @Override public String getId() { return this.getClass() .getSimpleName(); } @Override public String getTitle() { return this.getClass() .getSimpleName(); } @Override public String getDescription() { return "Checks metacard against the local catalog for duplicates based on configurable attributes."; } @Override public String getOrganization() { return describableProperties.getProperty(ORGANIZATION); } }