/** * 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.ui.metacard.associations; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.codice.ddf.catalog.ui.util.EndpointUtil; import org.opengis.filter.Filter; import org.opengis.filter.sort.SortBy; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import ddf.catalog.CatalogFramework; import ddf.catalog.core.versioning.DeletedMetacard; import ddf.catalog.core.versioning.MetacardVersion; import ddf.catalog.data.Attribute; import ddf.catalog.data.Metacard; import ddf.catalog.data.Result; import ddf.catalog.data.impl.AttributeImpl; import ddf.catalog.federation.FederationException; import ddf.catalog.operation.QueryResponse; import ddf.catalog.operation.impl.QueryImpl; import ddf.catalog.operation.impl.QueryRequestImpl; import ddf.catalog.operation.impl.UpdateRequestImpl; import ddf.catalog.source.IngestException; import ddf.catalog.source.SourceUnavailableException; import ddf.catalog.source.UnsupportedQueryException; public class Associated { private static final String ASSOCIATION_PREFIX = "metacard.associations."; private static final Set<String> ASSOCIATION_TYPES = ImmutableSet.of(Metacard.DERIVED, Metacard.RELATED); private final EndpointUtil util; private final CatalogFramework catalogFramework; public Associated(EndpointUtil util, CatalogFramework catalogFramework) { this.util = util; this.catalogFramework = catalogFramework; } public Collection<Edge> getAssociations(String metacardId) throws UnsupportedQueryException, SourceUnavailableException, FederationException { Map<String, Metacard> metacardMap = query(withNonrestrictedTags(forRootAndParents(metacardId))); if (metacardMap.isEmpty()) { return Collections.emptyList(); } Metacard root = metacardMap.get(metacardId); Collection<Metacard> parents = metacardMap.values() .stream() .filter(m -> !m.getId() .equals(metacardId)) .collect(Collectors.toList()); Map<String, Metacard> childMetacardMap = query(withNonrestrictedTags(forChildAssociations( root))); Collection<Edge> parentEdges = createParentEdges(parents, root); Collection<Edge> childrenEdges = createChildEdges(childMetacardMap.values(), root); Collection<Edge> edges = Stream.of(parentEdges, childrenEdges) .flatMap(Collection::stream) .collect(Collectors.toList()); return edges; } public void putAssociations(String id, Collection<Edge> edges) throws UnsupportedQueryException, SourceUnavailableException, FederationException, IngestException { Collection<Edge> oldEdges = getAssociations(id); List<String> ids = Stream.concat(oldEdges.stream(), edges.stream()) .flatMap(e -> Stream.of(e.child, e.parent)) .filter(Objects::nonNull) .map(m -> m.get(Metacard.ID)) .filter(Objects::nonNull) .map(Object::toString) .distinct() .collect(Collectors.toList()); Map<String, Metacard> metacards = util.getMetacards(ids, getNonrestrictedTagsFilter()) .entrySet() .stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue() .getMetacard())); Map<String, Metacard> changedMetacards = new HashMap<>(); Set<Edge> oldEdgeSet = new HashSet<>(oldEdges); Set<Edge> newEdgeSet = new HashSet<>(edges); Set<Edge> oldDiff = Sets.difference(oldEdgeSet, newEdgeSet); Set<Edge> newDiff = Sets.difference(newEdgeSet, oldEdgeSet); for (Edge edge : oldDiff) { removeEdge(edge, metacards, changedMetacards); } for (Edge edge : newDiff) { addEdge(edge, metacards, changedMetacards); } if (changedMetacards.isEmpty()) { return; } catalogFramework.update(new UpdateRequestImpl(changedMetacards.keySet() .toArray(new String[0]), new ArrayList<>(changedMetacards.values()))); } private void removeEdge(Edge edge, Map<String, Metacard> metacards, /*Mutable*/ Map<String, Metacard> changedMetacards) { String id = edge.parent.get(Metacard.ID) .toString(); Metacard target = changedMetacards.getOrDefault(id, metacards.get(id)); ArrayList<String> values = Optional.of(target) .map(m -> m.getAttribute(edge.relation)) .map(Attribute::getValues) .map(util::getStringList) .orElseGet(ArrayList::new); values.remove(edge.child.get(Metacard.ID) .toString()); target.setAttribute(new AttributeImpl(edge.relation, values)); changedMetacards.put(id, target); } private void addEdge(Edge edge, Map<String, Metacard> metacards, Map<String, Metacard> changedMetacards) { String id = edge.parent.get(Metacard.ID) .toString(); Metacard target = changedMetacards.getOrDefault(id, metacards.get(id)); ArrayList<String> values = Optional.of(target) .map(m -> m.getAttribute(edge.relation)) .map(Attribute::getValues) .map(util::getStringList) .orElseGet(ArrayList::new); values.add(edge.child.get(Metacard.ID) .toString()); target.setAttribute(new AttributeImpl(edge.relation, values)); changedMetacards.put(id, target); } private Collection<Edge> createChildEdges(Collection<Metacard> children, Metacard root) { List<Edge> edges = new ArrayList<>(); for (Metacard child : children) { List<String> relations = getRelationsToChild(root, child); edges.addAll(relations.stream() .map(relation -> new Edge(root, child, relation)) .collect(Collectors.toList())); } return edges; } private Collection<Edge> createParentEdges(Collection<Metacard> parents, Metacard root) { List<Edge> edges = new ArrayList<>(); for (Metacard parent : parents) { List<String> relations = getRelationsToChild(parent, root); edges.addAll(relations.stream() .map(relation -> new Edge(parent, root, relation)) .collect(Collectors.toList())); } return edges; } private Filter forRootAndParents(String rootId) { Filter root = util.getFilterBuilder() .attribute(Metacard.ID) .is() .equalTo() .text(rootId); Filter related = util.getFilterBuilder() .attribute(Metacard.RELATED) .is() .like() .text(rootId); Filter derived = util.getFilterBuilder() .attribute(Metacard.DERIVED) .is() .like() .text(rootId); Filter parents = util.getFilterBuilder() .anyOf(related, derived); return util.getFilterBuilder() .anyOf(root, parents); } private Filter withNonrestrictedTags(Filter filter) { if (filter == null) { return null; } return util.getFilterBuilder() .allOf(filter, getNonrestrictedTagsFilter()); } private Filter getNonrestrictedTagsFilter() { return util.getFilterBuilder() .not(util.getFilterBuilder() .anyOf(util.getFilterBuilder() .attribute(Metacard.TAGS) .is() .like() .text(DeletedMetacard.DELETED_TAG), util.getFilterBuilder() .attribute(Metacard.TAGS) .is() .like() .text(MetacardVersion.VERSION_TAG))); } private Filter forChildAssociations(Metacard metacard) { Set<String> childIds = ASSOCIATION_TYPES.stream() .map(metacard::getAttribute) .filter(Objects::nonNull) .map(Attribute::getValues) .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(String.class::isInstance) .map(String.class::cast) .collect(Collectors.toSet()); if (childIds.isEmpty()) { return null; } return util.getFilterBuilder() .anyOf(childIds.stream() .map(id -> util.getFilterBuilder() .attribute(Metacard.ID) .is() .equalTo() .text(id)) .collect(Collectors.toList())); } private Map<String, Metacard> query(Filter filter) throws UnsupportedQueryException, SourceUnavailableException, FederationException { if (filter == null) { return Collections.emptyMap(); } QueryResponse query = catalogFramework.query(new QueryRequestImpl(new QueryImpl(filter, 1, 0, SortBy.NATURAL_ORDER, false, TimeUnit.SECONDS.toMillis(30)), false)); return query.getResults() .stream() .map(Result::getMetacard) .collect(Collectors.toMap(Metacard::getId, Function.identity())); } private List<String> getRelationsToChild(Metacard parent, Metacard child) { List<String> relations = new ArrayList<>(); for (String associationType : ASSOCIATION_TYPES) { if (Optional.of(parent) .map(m -> m.getAttribute(associationType)) .map(Attribute::getValues) .map(util::getStringList) .map(l -> l.contains(child.getId())) .orElse(false)) { relations.add(associationType); } } return relations; } public class Edge { private Map<String, Object> parent; private Map<String, Object> child; private String relation; public Edge(Metacard parent, Metacard child, String relation) { this.parent = util.getMetacardMap(parent); this.child = util.getMetacardMap(child); this.relation = relation; } public int hashCode() { return new HashCodeBuilder().append(parent.get(Metacard.ID) .toString()) .append(child.get(Metacard.ID) .toString()) .append(relation) .build(); } public boolean equals(Object o) { if (o == null) { return false; } if (o == this) { return true; } if (!(o instanceof Edge)) { return false; } Edge rhs = (Edge) o; return new EqualsBuilder().append(parent.get(Metacard.ID) .toString(), rhs.parent.get(Metacard.ID) .toString()) .append(child.get(Metacard.ID) .toString(), rhs.child.get(Metacard.ID) .toString()) .append(relation, rhs.relation) .build(); } @Override public String toString() { return String.format("%s [%s]-> %s", parent.get("id") .toString(), relation, child.get("id") .toString()); } } }