package org.gbif.checklistbank.nub.validation;
import org.apache.commons.lang3.StringUtils;
import org.gbif.api.model.common.LinneanClassification;
import org.gbif.api.vocabulary.Kingdom;
import org.gbif.api.vocabulary.Rank;
import org.gbif.checklistbank.neo.traverse.Traversals;
import org.gbif.checklistbank.nub.NubDb;
import org.gbif.checklistbank.nub.model.NubUsage;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import org.junit.Assert;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.NotFoundException;
import org.neo4j.graphdb.Transaction;
import org.neo4j.helpers.Strings;
import org.neo4j.helpers.collection.Iterables;
import org.neo4j.helpers.collection.Iterators;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class NeoAssertionEngine implements AssertionEngine {
private static final Logger LOG = LoggerFactory.getLogger(NeoAssertionEngine.class);
private final NubDb db;
private final Int2IntMap usage2NubKey = new Int2IntOpenHashMap();
private boolean valid = true;
public NeoAssertionEngine(NubDb db) {
this.db = db;
LOG.info("Populate reverse key map for neo ids");
for (Map.Entry<Long, NubUsage> nub : db.dao().nubUsages()) {
int nubKey = (int) (long) nub.getKey();
int usageKey = nub.getValue().usageKey;
Preconditions.checkArgument(!usage2NubKey.containsKey(usageKey), "usageKey " + usageKey + " not unique");
usage2NubKey.put(usageKey, nubKey);
}
}
@Override
public boolean isValid() {
return valid;
}
@Override
public void assertParentsContain(String searchName, Rank searchRank, String parent) {
try (Transaction tx = db.beginTx()) {
Node start = findUsageByCanonical(searchName, searchRank).node;
assertParentsContain(start, null, parent);
} catch (AssertionError e) {
valid = false;
LOG.error("Classification for {} {} lacks parent {}", searchRank, searchName, parent, e);
}
}
@Override
public void assertParentsContain(int usageKey, Rank parentRank, String parent) {
try (Transaction tx = db.beginTx()) {
Node start = nodeById(usageKey);
assertParentsContain(start, parentRank, parent);
} catch (AssertionError e) {
valid = false;
LOG.error("Classification for usage {} missing {}", usageKey, parent, e);
}
}
private void assertParentsContain(Node start, @Nullable Rank parentRank, String parent) throws AssertionError {
try (Transaction tx = db.beginTx()) {
boolean found = false;
for (Node p : Traversals.PARENTS.traverse(start).nodes()) {
NubUsage u = db.dao().readNub(p);
if (parent.equalsIgnoreCase(u.parsedName.canonicalName()) && (parentRank == null || u.rank.equals(parentRank))) {
found = true;
}
}
Assert.assertTrue(found);
}
}
@Override
public void assertClassification(int usageKey, LinneanClassification classification) {
try (Transaction tx = db.beginTx()) {
NubUsage usage = getUsage(usageKey);
Map<Rank, String> parents = db.parentsMap(usage.node);
for (Rank r : Rank.DWC_RANKS) {
if (!StringUtils.isBlank(classification.getHigherRank(r))) {
if (!parents.get(r).equalsIgnoreCase(classification.getHigherRank(r))) {
valid = false;
LOG.error("Unexpected {} {} for {} {}", r, classification.getHigherRank(r), usage.toStringComplete());
}
}
}
} catch (AssertionError e) {
valid = false;
LOG.error("Classification assertion failed for {}", usageKey, e);
}
}
@Override
public void assertClassification(int usageKey, String... classification) {
Iterator<String> expected = Lists.newArrayList(classification).iterator();
try (Transaction tx = db.beginTx()) {
Node start = nodeById(usageKey);
for (Node p : Traversals.PARENTS.traverse(start).nodes()) {
NubUsage u = db.dao().readNub(p);
Assert.assertEquals(expected.next(), u.parsedName.canonicalName());
}
Assert.assertFalse(expected.hasNext());
} catch (AssertionError e) {
valid = false;
LOG.error("Classification for usage {} wrong", usageKey, e);
}
}
@Override
public void assertSearchMatch(int expectedSearchMatches, String name) {
assertSearchMatch(expectedSearchMatches, name, null);
}
@Override
public void assertSearchMatch(int expectedSearchMatches, String name, Rank rank) {
List<NubUsage> matches = Lists.newArrayList();
try {
matches = findUsagesByCanonical(name, rank);
Assert.assertEquals(expectedSearchMatches, matches.size());
} catch (AssertionError e) {
valid = false;
LOG.error("Expected {} matches, but found {} for name {} with rank {}", expectedSearchMatches, matches.size(), name, rank);
}
}
@Override
public void assertNotExisting(String name, Rank rank) {
List<NubUsage> matches = Lists.newArrayList();
try {
matches = findUsagesByCanonical(name, rank);
Assert.assertTrue(matches.isEmpty());
} catch (AssertionError e) {
valid = false;
LOG.error("Found name expected to be missing: {} {} with rank {}", matches.get(0).node, name, rank);
}
}
private NubUsage getUsage(int usageKey) {
long nodeId = usage2NubKey.get(usageKey);
return db.dao().readNub(nodeId);
}
@Override
public void assertUsage(int usageKey, Rank rank, String name, String accepted, Kingdom kingdom) {
NubUsage u = null;
try (Transaction tx = db.beginTx()) {
u = getUsage(usageKey);
Assert.assertNotNull(u);
Assert.assertEquals(rank, u.rank);
Assert.assertTrue(u.parsedName.canonicalNameComplete().startsWith(name));
if (StringUtils.isBlank(accepted)) {
Assert.assertTrue(u.status.isAccepted());
} else {
Assert.assertTrue(u.status.isSynonym());
NubUsage p = db.parent(u);
Assert.assertTrue(p.parsedName.canonicalNameComplete().startsWith(accepted));
}
NubUsage ku = findRootUsage(u);
if (kingdom != null) {
Assert.assertEquals(kingdom, ku.kingdom);
Assert.assertEquals(Rank.KINGDOM, ku.rank);
Assert.assertEquals(kingdom.scientificName(), ku.parsedName.getScientificName());
}
} catch (AssertionError e) {
LOG.error("Usage {}, {} wrong: {}", usageKey, name, e);
valid = false;
}
}
private Node nodeById(int usageKey) throws AssertionError {
try {
return db.getNode(usage2NubKey.get(usageKey));
} catch (NotFoundException e) {
throw new AssertionError("Usage " + usageKey + " not found");
}
}
private NubUsage findRootUsage(NubUsage u) {
try (Transaction tx = db.beginTx()) {
Node root = Iterables.last(Traversals.PARENTS.traverse(u.node).nodes());
return db.dao().readNub(root);
}
}
private NubUsage findUsageByCanonical(String name, Rank rank) {
List<NubUsage> matches = findUsagesByCanonical(name, rank);
if (matches.size() > 1 || matches.isEmpty()) {
valid = false;
LOG.error("{} matches when expecting single match for {} {}", matches.size(), rank, name);
throw new AssertionError("No single match for " + name);
}
return matches.get(0);
}
private List<NubUsage> findUsagesByCanonical(String name, @Nullable Rank rank) {
List<NubUsage> matches = Lists.newArrayList();
try (Transaction tx = db.beginTx()) {
for (Node n : db.dao().findByName(name)) {
NubUsage u = db.dao().readNub(n);
if (rank == null || rank.equals(u.rank)) {
matches.add(u);
}
}
}
return matches;
}
}