UsdaTaxonomyUpdater.java

  1. /*
  2.  * Copyright 2020 Global Crop Diversity Trust
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *   http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  */
  16. package org.gringlobal.worker;

  17. import java.io.File;
  18. import java.io.FileInputStream;
  19. import java.io.IOException;
  20. import java.time.ZoneOffset;
  21. import java.util.ArrayList;
  22. import java.util.HashMap;
  23. import java.util.LinkedList;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Objects;
  27. import java.util.concurrent.atomic.AtomicInteger;
  28. import java.util.stream.Collectors;

  29. import javax.persistence.EntityManager;

  30. import org.apache.commons.io.FileUtils;
  31. import org.apache.commons.lang3.StringUtils;
  32. import org.genesys.taxonomy.download.TaxonomyDownloader;
  33. import org.genesys.taxonomy.gringlobal.component.CabReader;
  34. import org.genesys.taxonomy.gringlobal.model.AuthorRow;
  35. import org.genesys.taxonomy.gringlobal.model.FamilyRow;
  36. import org.genesys.taxonomy.gringlobal.model.GenusRow;
  37. import org.genesys.taxonomy.gringlobal.model.SpeciesRow;
  38. import org.gringlobal.api.exception.InvalidApiUsageException;
  39. import org.gringlobal.model.TaxonomyAuthor;
  40. import org.gringlobal.model.TaxonomyFamily;
  41. import org.gringlobal.model.TaxonomyGenus;
  42. import org.gringlobal.model.TaxonomySpecies;
  43. import org.gringlobal.persistence.TaxonomyAuthorRepository;
  44. import org.gringlobal.persistence.TaxonomyFamilyRepository;
  45. import org.gringlobal.persistence.TaxonomyGenusRepository;
  46. import org.gringlobal.persistence.TaxonomySpeciesRepository;
  47. import org.springframework.beans.factory.annotation.Autowired;
  48. import org.springframework.security.access.prepost.PreAuthorize;
  49. import org.springframework.stereotype.Component;
  50. import org.springframework.transaction.annotation.Transactional;

  51. import com.google.common.collect.Lists;
  52. import com.opencsv.CSVReader;

  53. import lombok.extern.slf4j.Slf4j;

  54. /**
  55.  * The component downloads current GRIN Taxonomy database if no local copy
  56.  * exists and updates Family, Genus and Species tables in the local database.
  57.  *
  58.  * The matching is done on names only, local identifiers will not match GRIN
  59.  * Taxonomy IDs.
  60.  *
  61.  * @author Matija Obreza
  62.  */
  63. @Component
  64. @Slf4j
  65. public class UsdaTaxonomyUpdater {

  66.     private static final String DEBUG_GENUS_NAME = "Neurachne";
  67.     private static final String DEBUG_SPECIES_NAME = "Neurachne alopecuroides";

  68.     @Autowired
  69.     private TaxonomyFamilyRepository taxonomyFamilyRepository;
  70.     @Autowired
  71.     private TaxonomyGenusRepository taxonomyGenusRepository;
  72.     @Autowired
  73.     private TaxonomySpeciesRepository taxonomySpeciesRepository;
  74.     @Autowired
  75.     private TaxonomyAuthorRepository taxonomyAuthorRepository;

  76.     private File downloadFolder = new File(FileUtils.getTempDirectory(), "grin-taxonomy-source"); // + System.currentTimeMillis());

  77.     @Autowired
  78.     private EntityManager entityManager;


  79.     /**
  80.      * Update local taxonomy tables with data from GRIN Taxonomy.
  81.      *
  82.      * @throws Exception
  83.      */
  84.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  85.     @Transactional
  86.     public void update() throws Exception {
  87.         log.info("Updating GRIN taxonomy database from folder {}", downloadFolder.getAbsolutePath());
  88.         downloadDataIfNeeded(downloadFolder);
  89.         updateLocalDatabase();
  90.         log.warn("Taxonomy database updated successfully. Transaction will now be committed. This takes time!");
  91.     }

  92.     /**
  93.      * The update starts with {@link TaxonomyFamily}, {@link TaxonomyGenus} and then
  94.      * {@link TaxonomySpecies}. The entries from source database are mapped to local
  95.      * identifiers. No records are removed from the local database.
  96.      *
  97.      * <p>
  98.      * Note: The update may update capitalization of names.
  99.      * </p>
  100.      *
  101.      * @throws Exception
  102.      */
  103.     private void updateLocalDatabase() throws Exception {
  104.         log.info("Loading taxonomy_family.txt");
  105.         Map<Long, TaxonomyFamily> famTheirsToOurs = new HashMap<>();
  106.         Map<Long, TaxonomyGenus> genTheirsToOurs = new HashMap<>();
  107.         Map<Long, TaxonomySpecies> speTheirsToOurs = new HashMap<>();
  108. //      Map<Long, TaxonomyAuthor> authTheirsToOurs = new HashMap<>();

  109.         Map<Long, Long> currentTypeGenus = new HashMap<>();

  110.         {
  111.             log.warn("Loading {}/taxonomy_family.txt", downloadFolder);

  112.             Map<Long, Long> currentFamily = new HashMap<>();
  113.             List<TaxonomyFamily> allFamilies = taxonomyFamilyRepository.findAll();
  114.             final Map<Long, TaxonomyFamily> allFamiliesByGrinId = new HashMap<>();
  115.             allFamilies.forEach(family -> {
  116.                 if (family.getGrinId() != null) {
  117.                     allFamiliesByGrinId.put(family.getGrinId(), family);
  118.                 }
  119.             });
  120.             List<TaxonomyFamily> toSave = new ArrayList<>();
  121.             // read taxonomy_genus.txt
  122.             try (CSVReader reader = CabReader.openCsvReader(new FileInputStream(new File(downloadFolder, "taxonomy_family.txt")), 0)) {
  123.                 var beanReader = CabReader.beanReader(FamilyRow.class, reader);
  124.                 beanReader.forEach(familyRow -> {
  125.                     TaxonomyFamily family = new TaxonomyFamily();
  126.                     family.setGrinId(familyRow.getTaxonomyFamilyId());
  127.                     // family.setId(familyRow.getTaxonomyFamilyId());
  128.                     // family.setTypeTaxonomyGenus(familyRow.getTypeTaxonomyGenusId());
  129.                     family.setFamilyName(familyRow.getFamilyName());
  130.                     family.setFamilyAuthority(familyRow.getFamilyAuthority());
  131.                     family.setSubfamilyName(familyRow.getSubfamilyName());
  132.                     family.setTribeName(familyRow.getTribeName());
  133.                     family.setSubtribeName(familyRow.getSubtribeName());
  134.        
  135.                     var other = allFamiliesByGrinId.get(familyRow.getTaxonomyFamilyId());
  136.                     if (other != null) {
  137.                         family = other;
  138.                     } else {
  139.                         if (allFamilies.size() > 0) {
  140.                             final TaxonomyFamily compareTo = family;
  141.                             final List<TaxonomyFamily> narrow = allFamilies.stream()
  142.                                 // filter
  143.                                 .filter(m -> (
  144.                                         StringUtils.equalsIgnoreCase(m.getFamilyName(), compareTo.getFamilyName())
  145.                                         && StringUtils.equalsIgnoreCase(m.getFamilyAuthority(), compareTo.getFamilyAuthority())
  146.                                         && StringUtils.equalsIgnoreCase(m.getSubfamilyName(), compareTo.getSubfamilyName())
  147.                                         && StringUtils.equalsIgnoreCase(m.getTribeName(), compareTo.getTribeName())
  148.                                         && StringUtils.equalsIgnoreCase(m.getSubtribeName(), compareTo.getSubtribeName())
  149.                                 ))
  150.                                 // print
  151.                                 .peek(m -> {
  152.                                     log.debug("{} {} {} {} {}", m.getFamilyName(), m.getFamilyAuthority(), m.getSubfamilyName(), m.getTribeName(), m.getSubtribeName());
  153.                                 })
  154.                                 // collect
  155.                                 .collect(Collectors.toList());
  156.            
  157.                             if (narrow.size() == 1) {
  158.                                 family = narrow.get(0);
  159.                             } else if (narrow.size() == 0) {
  160.                                 log.debug("{} matches found! Will create new entry.", narrow.size());
  161.                             } else {
  162.                                 throw new InvalidApiUsageException("This shouldn't happen, your taxonomy_family needs cleaning: " + family.getFamilyName());
  163.                             }
  164.                         }
  165.                     }

  166.                     family.setGrinId(familyRow.getTaxonomyFamilyId());
  167.                     family.setFamilyName(familyRow.getFamilyName());
  168.                     family.setFamilyAuthority(familyRow.getFamilyAuthority());
  169.                     family.setSubfamilyName(familyRow.getSubfamilyName());
  170.                     family.setTribeName(familyRow.getTribeName());
  171.                     family.setSubtribeName(familyRow.getSubtribeName());
  172.        
  173.                     family.setSuprafamilyRankCode(familyRow.getSuprafamilyRankCode());
  174.                     family.setSuprafamilyRankName(familyRow.getSuprafamilyRankName());
  175.                     family.setAlternateName(familyRow.getAlternateName());
  176.                     family.setFamilyTypeCode(familyRow.getFamilyTypeCode());
  177.                     family.setNote(familyRow.getNote());
  178.        
  179.                     toSave.add(family);
  180.                     famTheirsToOurs.put(familyRow.getTaxonomyFamilyId(), family);
  181.                     currentFamily.put(familyRow.getTaxonomyFamilyId(), familyRow.getCurrentTaxonomyFamilyId());
  182.                     currentTypeGenus.put(familyRow.getTaxonomyFamilyId(), familyRow.getTypeTaxonomyGenusId());
  183.                 });
  184.             }

  185.             // Save updates
  186.             Lists.partition(toSave, 100).forEach(batch -> {
  187.                 log.warn("Saving {} taxonomyFamily", batch.size());
  188.                 taxonomyFamilyRepository.saveAll(batch);
  189.                 entityManager.flush();;
  190.             });
  191.             toSave.clear();

  192.             // Update references
  193.             currentFamily.forEach((theirId, theirCurrentId) -> {
  194.                 var family = famTheirsToOurs.get(theirId);
  195.                 var current = famTheirsToOurs.get(theirCurrentId);
  196.                 if (current == null || family.getCurrentTaxonomyFamily() == null || !family.getCurrentTaxonomyFamily().getId().equals(current.getId())) {
  197.                     var reloaded = taxonomyFamilyRepository.findById(family.getId()).orElseThrow();
  198.                     reloaded.setCurrentTaxonomyFamily(taxonomyFamilyRepository.findById(current.getId()).orElseThrow());
  199.                     toSave.add(reloaded);
  200.                 }
  201.             });
  202.             // Save updates
  203.             Lists.partition(toSave, 100).forEach(batch -> {
  204.                 log.warn("Saving {} taxonomyFamily", batch.size());
  205.                 taxonomyFamilyRepository.saveAll(batch);
  206.                 entityManager.flush();
  207.             });

  208.             allFamilies.clear();
  209.             toSave.clear();
  210.             allFamiliesByGrinId.clear();
  211.         }

  212.         {
  213.             // read taxonomy_genus.txt
  214.             log.warn("Loading {}/taxonomy_genus.txt", downloadFolder);
  215.             // Group list of genera by family#id for faster lookups
  216.             final LookupList<String, TaxonomyGenus> allGeneraIndex = new LookupList<>();
  217.             final Map<Long, TaxonomyGenus> allGeneraByGrinId = new HashMap<>();
  218.             taxonomyGenusRepository.findAll().forEach(genus -> {
  219.                 allGeneraIndex.add(indexLookupKey(genus), genus);
  220.                 if (genus.getGrinId() != null) {
  221.                     allGeneraByGrinId.put(genus.getGrinId(), genus);
  222.                 }
  223.             });

  224.             List<TaxonomyGenus> toSave = new ArrayList<>();
  225.             Map<Long, Long> currentGenus = new HashMap<>();

  226.             try (CSVReader reader = CabReader.openCsvReader(new FileInputStream(new File(downloadFolder, "taxonomy_genus.txt")), 0)) {
  227.                 var beanReader = CabReader.beanReader(GenusRow.class, reader);
  228.                 beanReader.forEach(genusRow -> {
  229.                     TaxonomyGenus genus = new TaxonomyGenus();
  230.                     genus.setGrinId(genusRow.getTaxonomyGenusId());
  231.                     genus.setQualifyingCode(genusRow.getQualifyingCode());
  232.                     genus.setHybridCode(genusRow.getHybridCode());
  233.                     genus.setGenusName(genusRow.getGenusName());
  234.                     genus.setGenusAuthority(genusRow.getGenusAuthority());
  235.                     genus.setSubgenusName(genusRow.getSubgenusName());
  236.                     genus.setSectionName(genusRow.getSectionName());
  237.                     genus.setSubsectionName(genusRow.getSubsectionName());
  238.                     genus.setSeriesName(genusRow.getSeriesName());
  239.                     genus.setSubseriesName(genusRow.getSubseriesName());
  240.                     genus.setTaxonomyFamily(famTheirsToOurs.get(genusRow.getTaxonomyFamilyId()));
  241.                     if (genus.getTaxonomyFamily() == null) {
  242.                         log.warn("No family with their id=" + genusRow.getTaxonomyFamilyId());
  243.                         return;
  244.                     }

  245.                     if (StringUtils.equalsIgnoreCase(genus.getGenusName(), DEBUG_GENUS_NAME)) {
  246.                         print(">> Matching", genus);
  247.                     }


  248.                     var other = allGeneraByGrinId.get(genusRow.getTaxonomyGenusId());
  249.                     if (other != null) {
  250.                         genus = other;
  251.                     } else {
  252.                         List<TaxonomyGenus> generaWithName = allGeneraIndex.get(indexLookupKey(genus));
  253.                         if (generaWithName != null) {
  254.                             final TaxonomyGenus compareTo = genus;

  255.                             if (compareTo.getGenusName().equals(DEBUG_GENUS_NAME)) {
  256.                                 print(">> Looking for: ", compareTo);
  257.                             }
  258.                             List<TaxonomyGenus> narrow = generaWithName.stream()
  259.                                 // print
  260.                                 .peek(m -> {
  261.                                     if (compareTo.getGenusName().equals(DEBUG_GENUS_NAME)) {
  262.                                         print("Candidate: ", m);
  263.                                     }
  264.                                 })
  265.                                 // filter
  266.                                 .filter(m -> (
  267.                                     Objects.equals(m.getTaxonomyFamily().getId(), compareTo.getTaxonomyFamily().getId())
  268.                                     && StringUtils.equalsIgnoreCase(m.getGenusName(), compareTo.getGenusName())
  269.                                     && StringUtils.equalsIgnoreCase(m.getGenusAuthority(), compareTo.getGenusAuthority())
  270.                                     && StringUtils.equalsIgnoreCase(m.getSubgenusName(), compareTo.getSubgenusName())
  271.                                     && StringUtils.equalsIgnoreCase(m.getSectionName(), compareTo.getSectionName())
  272.                                     && StringUtils.equalsIgnoreCase(m.getSubsectionName(), compareTo.getSubsectionName())
  273.                                     && StringUtils.equalsIgnoreCase(m.getSeriesName(), compareTo.getSeriesName())
  274.                                     && StringUtils.equalsIgnoreCase(m.getSubseriesName(), compareTo.getSubseriesName())
  275.                                 ))
  276.                                 // print
  277.                                 .peek(m -> {
  278.                                     if (m.getGenusName().equals(DEBUG_GENUS_NAME)) {
  279.                                         print("Match", m);
  280.                                     }
  281.                                     log.debug("{} {} {} {} {} {} {}", m.getGenusName(), m.getGenusAuthority(), m.getSubgenusName(), m.getSectionName(), m.getSubsectionName(), m.getSeriesName(), m.getSubseriesName());
  282.                                 })
  283.                                 // collect
  284.                                 .collect(Collectors.toList());

  285.                             if (narrow.size() == 1) {
  286.                                 genus = narrow.get(0);
  287.                             } else if (narrow.size() == 0) {
  288.                                 log.info("{} matches found for {} {} {} {} {} {} {}! Will create new entry.", narrow.size(), genus.getGenusName(), genus.getGenusAuthority(), genus
  289.                                     .getSubgenusName(), genus.getSectionName(), genus.getSubsectionName(), genus.getSeriesName(), genus.getSubseriesName());
  290.                             } else {
  291.                                 print("Too many matches for:", compareTo);
  292.                                 narrow.forEach(m -> print(">> ", m));
  293.                                 var narrower = narrow.stream().filter(m -> (
  294.                                     StringUtils.equalsIgnoreCase(m.getHybridCode(), compareTo.getHybridCode())
  295.                                     && StringUtils.equalsIgnoreCase(m.getQualifyingCode(), compareTo.getQualifyingCode())
  296.                                 )).collect(Collectors.toList());
  297.                                 if (narrower.size() == 1) {
  298.                                     genus = narrower.get(0);
  299.                                 } else {
  300.                                     throw new InvalidApiUsageException("This shouldn't happen, your taxonomy_genus needs cleaning: " + genus.getGenusName() + " " + genus.getGenusAuthority());
  301.                                 }
  302.                             }
  303.                         } else {
  304.                             log.info("No existing genera for index={}", indexLookupKey(genus));
  305.                             // print("New taxonomy_genus", genus);
  306.                         }
  307.                     }

  308.                     if (StringUtils.equalsIgnoreCase(genus.getGenusName(), DEBUG_GENUS_NAME)) {
  309.                         print(">> Updating", genus);
  310.                     }

  311.                     // genus.setGenusId(genusRow.getGenusId());
  312.                     // genus.setCurrentGenusId(genusRow.getCurrentGenusId());
  313.                     genus.setGrinId(genusRow.getGenusId());
  314.                     genus.setTaxonomyFamily(famTheirsToOurs.get(genusRow.getTaxonomyFamilyId()));
  315.                     if (genus.getTaxonomyFamily() == null) {
  316.                         log.warn("No family with their id=" + genusRow.getTaxonomyFamilyId());
  317.                         return;
  318.                     }

  319.                     genus.setQualifyingCode(genusRow.getQualifyingCode());
  320.                     genus.setHybridCode(genusRow.getHybridCode());
  321.                     genus.setGenusName(genusRow.getGenusName());
  322.                     genus.setGenusAuthority(genusRow.getGenusAuthority());
  323.                     genus.setSubgenusName(genusRow.getSubgenusName());
  324.                     genus.setSectionName(genusRow.getSectionName());
  325.                     genus.setSubsectionName(genusRow.getSubsectionName());
  326.                     genus.setSeriesName(genusRow.getSeriesName());
  327.                     genus.setSubseriesName(genusRow.getSubseriesName());
  328.                     genus.setNote(genusRow.getNote());

  329.                     // genus.setCreatedDate(genusRow.getCreatedDate());
  330.                     // genus.setModifiedDate(genusRow.getModifiedDate()); // Do not update @Versioned modifiedDate

  331.                     if (StringUtils.equalsIgnoreCase(genus.getGenusName(), DEBUG_GENUS_NAME)) {
  332.                         print(">> Updated", genus);
  333.                     }

  334.                     toSave.add(genus);
  335.                     genTheirsToOurs.put(genusRow.getGenusId(), genus);
  336.                     currentGenus.put(genusRow.getTaxonomyGenusId(), genusRow.getCurrentTaxonomyGenusId());
  337.                 });
  338.             }
  339.             Lists.partition(toSave, 1000).forEach(batch -> {
  340.                 log.warn("Saving {} taxonomyGenus", batch.size());
  341.                 taxonomyGenusRepository.saveAll(batch);
  342.                 entityManager.flush();
  343.             });
  344.             toSave.clear();

  345.             // Update references
  346.             currentGenus.forEach((theirId, theirCurrentId) -> {
  347.                 var genus = genTheirsToOurs.get(theirId);
  348.                 var current = genTheirsToOurs.get(theirCurrentId);
  349.                 if (current == null || genus.getCurrentTaxonomyGenus() == null || !genus.getCurrentTaxonomyGenus().getId().equals(current.getId())) {
  350.                     var reloaded = taxonomyGenusRepository.findById(genus.getId()).orElseThrow();
  351.                     reloaded.setCurrentTaxonomyGenus(taxonomyGenusRepository.findById(current.getId()).orElseThrow());
  352.                     toSave.add(reloaded);
  353.                 }
  354.             });
  355.             // Save updates
  356.             log.info("Updating {} genus references", toSave.size());
  357.             Lists.partition(toSave, 1000).forEach(batch -> {
  358.                 log.warn("Saving {} taxonomyGenus", batch.size());
  359.                 taxonomyGenusRepository.saveAll(batch);
  360.                 entityManager.flush();
  361.             });

  362.             toSave.clear();
  363.             allGeneraIndex.clear();
  364.             allGeneraByGrinId.clear();

  365.             {
  366.                 List<TaxonomyFamily> toSaveFam = new ArrayList<>();
  367.                 currentTypeGenus.forEach((theirId, theirGenusId) -> {
  368.                     TaxonomyFamily family = famTheirsToOurs.get(theirId);
  369.                     if (theirGenusId == null) {
  370.                         if (family.getTypeTaxonomyGenus() != null) {
  371.                             family = taxonomyFamilyRepository.findById(family.getId()).orElseThrow();
  372.                             family.setTypeTaxonomyGenus(null);
  373.                             toSaveFam.add(family);
  374.                         }
  375.                     } else {
  376.                         var typeGenus = genTheirsToOurs.get(theirGenusId);
  377.                         if (typeGenus == null || family.getTypeTaxonomyGenus() == null || family.getTypeTaxonomyGenus().getId().equals(typeGenus.getId())) {
  378.                             family = taxonomyFamilyRepository.findById(family.getId()).orElseThrow();
  379.                             family.setTypeTaxonomyGenus(taxonomyGenusRepository.findById(typeGenus.getId()).orElseThrow());
  380.                             toSaveFam.add(family);
  381.                         }
  382.                     }
  383.                     if (family.getTypeTaxonomyGenus() == null && theirGenusId != null) {
  384.                         log.warn("Type genus is null: their genus_id={} our taxonomy_family_id={}", theirGenusId, family.getId());
  385.                     }
  386.                 });
  387.                 Lists.partition(toSaveFam, 100).forEach(batch -> {
  388.                     log.warn("Saving {} taxonomyFamily", batch.size());
  389.                     taxonomyFamilyRepository.saveAll(batch);
  390.                     entityManager.flush();
  391.                 });

  392.                 currentTypeGenus.clear();
  393.             }
  394.         }


  395.         {
  396.             // read taxonomy_species.txt
  397.             log.warn("Loading {}/taxonomy_species.txt", downloadFolder);
  398.             // Group list of species by epithet for faster lookups
  399.             final LookupList<String, TaxonomySpecies> allSpeciesByEpithet = new LookupList<>();
  400.             final Map<Long, TaxonomySpecies> allSpeciesByGrinId = new HashMap<>();
  401.             taxonomySpeciesRepository.findAll().forEach(species -> {
  402.                 allSpeciesByEpithet.add(StringUtils.toRootLowerCase(species.getSpeciesName()), species);
  403.                 if (species.getGrinId() != null) {
  404.                     allSpeciesByGrinId.put(species.getGrinId(), species);
  405.                 }
  406.             });

  407.             List<TaxonomySpecies> toSave = new ArrayList<>();
  408.             Map<Long, Long> currentSpecies = new HashMap<>();

  409.             try (CSVReader reader = CabReader.openCsvReader(new FileInputStream(new File(downloadFolder, "taxonomy_species.txt")), 0)) {
  410.                 final AtomicInteger counter = new AtomicInteger(0);
  411.                 var beanReader = CabReader.beanReader(SpeciesRow.class, reader);
  412.                 beanReader.forEach(speciesRow -> {
  413.                     if (counter.incrementAndGet() % 1000 == 0) {
  414.                         log.warn("Read {} species rows", counter.get());
  415.                     }
  416.                     TaxonomySpecies species = new TaxonomySpecies();
  417.                     species.setGrinId(speciesRow.getTaxonomySpeciesId());
  418.                     species.setTaxonomyGenus(genTheirsToOurs.get(speciesRow.getGenusId()));
  419.                     species.setNomenNumber(speciesRow.getNomenNumber() == null ? null : speciesRow.getNomenNumber().intValue());
  420.                     species.setSpeciesName(speciesRow.getSpeciesName());
  421.                     species.setName(speciesRow.getName());
  422.                     species.setNameAuthority(speciesRow.getNameAuthority());
  423.                     species.setProtologue(speciesRow.getProtologue());

  424.                     var other = allSpeciesByGrinId.get(speciesRow.getTaxonomySpeciesId());
  425.                     if (other != null) {
  426.                         species = other;
  427.                     } else {
  428.                         log.debug("No species with usda_id={}! Searching for {} {}", speciesRow.getTaxonomySpeciesId(), speciesRow.getName(), speciesRow.getNameAuthority());

  429.                         List<TaxonomySpecies> speciesForEpithet = allSpeciesByEpithet.get(StringUtils.toRootLowerCase(species.getSpeciesName()));
  430.                         final TaxonomySpecies compareTo = species;

  431.                         if (speciesForEpithet != null) {
  432.                             if (StringUtils.equalsIgnoreCase(species.getName(), DEBUG_SPECIES_NAME)) {
  433.                                 print(">> Looking for", species);
  434.                             }

  435.                             List<TaxonomySpecies> narrow = speciesForEpithet.stream()
  436.                                 // debug
  437.                                 .peek(m -> {
  438.                                     if (StringUtils.equalsIgnoreCase(compareTo.getName(), DEBUG_SPECIES_NAME)) {
  439.                                         print("Inspecting:", m);
  440.                                     }
  441.                                 })
  442.                                 // filter
  443.                                 .filter(m -> (
  444.                                     Objects.equals(m.getTaxonomyGenus().getId(), compareTo.getTaxonomyGenus().getId())
  445.                                     && StringUtils.equalsIgnoreCase(StringUtils.trimToNull(m.getName()), StringUtils.trimToNull(compareTo.getName()))
  446.                                     && StringUtils.equalsIgnoreCase(StringUtils.trimToNull(m.getNameAuthority()), StringUtils.trimToNull(compareTo.getNameAuthority()))
  447.                                     && StringUtils.equalsIgnoreCase(StringUtils.trimToNull(m.getSynonymCode()), StringUtils.trimToNull(compareTo.getSynonymCode()))
  448.                                     && StringUtils.equalsIgnoreCase(StringUtils.trimToNull(m.getProtologue()), StringUtils.trimToNull(compareTo.getProtologue()))
  449.                                 ))
  450.                                 // print
  451.                                 .peek(m -> {
  452.                                     if (StringUtils.equalsIgnoreCase(compareTo.getName(), DEBUG_SPECIES_NAME)) {
  453.                                         print("Potential match:", m);
  454.                                     }
  455.                                     log.debug("{} {}", m.getName(), m.getNameAuthority());
  456.                                 })
  457.                                 // gather
  458.                                 .collect(Collectors.toList());
  459.            
  460.                             if (narrow.size() == 1) {
  461.                                 species = narrow.get(0);
  462.                             } else if (narrow.size() == 0) {
  463.                                 if (StringUtils.equalsIgnoreCase(species.getName(), DEBUG_SPECIES_NAME)) {
  464.                                     print("No matches found, will add", species);
  465.                                 }
  466.                                 log.debug("{} matches found for {} {}! Will create new entry.", narrow.size(), species.getName(), species.getNameAuthority());
  467.                             } else {
  468.                                 throw new InvalidApiUsageException("This shouldn't happen, your taxonomy_species needs cleaning: " + species.getName() + " " + species.getNameAuthority());
  469.                             }
  470.                         } else {
  471.                             log.debug("No species for epithet={}", species.getSpeciesName());
  472.                             if (StringUtils.equalsIgnoreCase(species.getName(), DEBUG_SPECIES_NAME)) {
  473.                                 print("Will add", species);
  474.                             }
  475.                         }
  476.                     }

  477.                     if (StringUtils.equalsIgnoreCase(species.getName(), DEBUG_SPECIES_NAME)) {
  478.                         print(">> Updating", species);
  479.                     }

  480.                     // species.setSpeciesId(speciesRow.getSpeciesId());
  481.                     // species.setCurrentSpeciesId(speciesRow.getCurrentSpeciesId());
  482.                     species.setGrinId(speciesRow.getTaxonomySpeciesId());
  483.                     species.setTaxonomyGenus(genTheirsToOurs.get(speciesRow.getGenusId()));
  484.                     if (species.getTaxonomyGenus() == null) {
  485.                         log.warn("Missing genus for species id={} genus_id={}", speciesRow.getSpeciesId(), speciesRow.getGenusId());
  486.                         return;
  487.                     }

  488.                     species.setNomenNumber(speciesRow.getNomenNumber() == null ? null : speciesRow.getNomenNumber().intValue());
  489.                     species.setIsSpecificHybrid(speciesRow.getIsSpecificHybrid());
  490.                     species.setSpeciesName(speciesRow.getSpeciesName());
  491.                     species.setSpeciesAuthority(speciesRow.getSpeciesAuthority());
  492.                     species.setIsSubspecificHybrid(speciesRow.getIsSubspecificHybrid());
  493.                     species.setSubspeciesName(speciesRow.getSubspeciesName());
  494.                     species.setSubspeciesAuthority(speciesRow.getSubspeciesAuthority());
  495.                     species.setIsVarietalHybrid(speciesRow.getIsVarietalHybrid());
  496.                     species.setVarietyName(speciesRow.getVarietyName());
  497.                     species.setVarietyAuthority(speciesRow.getVarietyAuthority());
  498.                     species.setIsSubvarietalHybrid(speciesRow.getIsSubvarietalHybrid());
  499.                     species.setSubvarietyName(speciesRow.getSubvarietyName());
  500.                     species.setSubvarietyAuthority(speciesRow.getSubvarietyAuthority());
  501.                     species.setIsFormaHybrid(speciesRow.getIsFormaHybrid());
  502.                     species.setFormaRankType(speciesRow.getFormaRankType());
  503.                     species.setFormaName(speciesRow.getFormaName());
  504.                     species.setFormaAuthority(speciesRow.getFormaAuthority());
  505.                     // species.setPrioritySite1(speciesRow.getPrioritySite1());
  506.                     // species.setPrioritySite2(speciesRow.getPrioritySite2());
  507.                     // species.setCurator1Id(speciesRow.getCurator1Id());
  508.                     // species.setCurator2Id(speciesRow.getCurator2Id());
  509.                     species.setRestrictionCode(speciesRow.getRestrictionCode());
  510.                     species.setLifeFormCode(speciesRow.getLifeFormCode());
  511.                     species.setCommonFertilizationCode(speciesRow.getCommonFertilizationCode());
  512.                     species.setIsNamePending(speciesRow.getIsNamePending());
  513.                     species.setSynonymCode(speciesRow.getSynonymCode());
  514.                     // species.setVerifierCooperator(speciesRow.getVerifierId());
  515.                     if (speciesRow.getNameVerifiedDate() != null) {
  516.                         species.setNameVerifiedDate(speciesRow.getNameVerifiedDate().toInstant(ZoneOffset.UTC));
  517.                     }

  518.                     species.setName(speciesRow.getName());
  519.                     species.setNameAuthority(speciesRow.getNameAuthority());
  520.                     species.setProtologue(speciesRow.getProtologue());
  521.                     species.setProtologueVirtualPath(speciesRow.getProtologueVirtualPath());
  522.                     species.setNote(speciesRow.getNote());
  523.                     species.setSiteNote(speciesRow.getSiteNote());
  524.                     species.setAlternateName(speciesRow.getAlternateName());

  525.                     // species.setCreatedDate(speciesRow.getCreatedDate());
  526.                     // species.setModifiedDate(speciesRow.getModifiedDate()); // Do not update @Versioned modifiedDate

  527.                     if (StringUtils.equalsIgnoreCase(species.getName(), DEBUG_SPECIES_NAME)) {
  528.                         print(">> Updated", species);
  529.                     }

  530.                     toSave.add(species);
  531.                     speTheirsToOurs.put(speciesRow.getSpeciesId(), species);
  532.                     currentSpecies.put(speciesRow.getSpeciesId(), speciesRow.getCurrentTaxonomySpeciesId());
  533.                 });
  534.             }
  535.    
  536.             Lists.partition(toSave, 1000).forEach(batch -> {
  537.                 log.warn("Saving {} taxonomySpecies", batch.size());
  538.                 taxonomySpeciesRepository.saveAll(batch);
  539.                 entityManager.flush();
  540.             });
  541.             toSave.clear();
  542.    
  543.             // Update references
  544.             currentSpecies.forEach((theirId, theirCurrentId) -> {
  545.                 var species = speTheirsToOurs.get(theirId);
  546.                 var current = speTheirsToOurs.get(theirCurrentId);
  547.                 if (current == null || species.getCurrentTaxonomySpecies() == null || !species.getCurrentTaxonomySpecies().getId().equals(current.getId())) {
  548.                     species.setCurrentTaxonomySpecies(current);
  549.                     toSave.add(species);
  550.                 }
  551.             });
  552.             // Save updates
  553.             log.info("Updating {} species references", toSave.size());
  554.             Lists.partition(toSave, 1000).forEach(batch -> {
  555.                 log.warn("Saving {} taxonomySpecies", batch.size());
  556.                 taxonomySpeciesRepository.saveAll(batch);
  557.                 entityManager.flush();
  558.             });
  559.    
  560.             toSave.clear();
  561.         }

  562.         {
  563.             log.warn("Loading {}/taxonomy_author.txt", downloadFolder);

  564.             List<TaxonomyAuthor> allAuthors = taxonomyAuthorRepository.findAll();
  565.             List<TaxonomyAuthor> toSave = new ArrayList<>();
  566.             final LookupList<String, TaxonomyAuthor> authorsLookup = new LookupList<>();
  567.             allAuthors.forEach(author -> {
  568.                 authorsLookup.add(author.getShortName().substring(0, 2), author);
  569.             });

  570.             try (CSVReader reader = CabReader.openCsvReader(new FileInputStream(new File(downloadFolder, "taxonomy_author.txt")), 0)) {
  571.                 var beanReader = CabReader.beanReader(AuthorRow.class, reader);
  572.                 beanReader.forEach(authorRow -> {
  573.                     TaxonomyAuthor author = new TaxonomyAuthor();
  574.                     author.setShortName(authorRow.getShortName());

  575.                     if (author.getShortName() == null) {
  576.                         log.warn("Missing shortName id={}", authorRow.getTaxonomyAuthorId());
  577.                         return;
  578.                     }

  579.                     List<TaxonomyAuthor> authorsByFirst = authorsLookup.get(author.getShortName().substring(0, 2));
  580.                     if (authorsByFirst != null) {
  581.                         final TaxonomyAuthor compareTo = author;
  582.                         List<TaxonomyAuthor> narrow = authorsByFirst.stream()
  583.                             // filter
  584.                             .filter(m -> (
  585.                                 StringUtils.equalsIgnoreCase(StringUtils.trimToNull(m.getShortName()), StringUtils.trim(compareTo.getShortName()))
  586.                             ))
  587.                             // print
  588.                             .peek(m -> {
  589.                                 log.debug("{}", m.getShortName());
  590.                             })
  591.                             // gather
  592.                             .collect(Collectors.toList());

  593.                         if (narrow.size() == 1) {
  594.                             author = narrow.get(0);
  595.                         } else if (narrow.size() == 0) {
  596.                             log.debug("{} matches found for {}! Will create new entry.", narrow.size(), author.getShortName());
  597.                         } else {
  598.                             narrow.forEach(match -> {
  599.                                 log.warn("Found id={} short={} for input {}", match.getId(), match.getShortName(), compareTo.getShortName());
  600.                             });
  601.                             throw new InvalidApiUsageException("This shouldn't happen, your taxonomy_author needs cleaning: " + author.getShortName());
  602.                         }
  603.                     }

  604.                     author.setFullName(authorRow.getFullName());
  605.                     author.setFullNameExpandedDiacritic(authorRow.getFullNameExpandedDiacritic());
  606.                     author.setShortName(authorRow.getShortName());
  607.                     author.setShortNameExpandedDiacritic(authorRow.getShortNameExpandedDiacritic());
  608.                     author.setNote(authorRow.getNote());

  609.                     toSave.add(author);
  610. //                  authTheirsToOurs.put(authorRow.getTaxonomyAuthorId(), author);
  611.                 });
  612.             }
  613.             Lists.partition(toSave, 1000).forEach(batch -> {
  614.                 log.warn("Saving {} taxonomyAuthors", batch.size());
  615.                 taxonomyAuthorRepository.saveAll(batch);
  616.                 entityManager.flush();
  617.             });
  618.             toSave.clear();
  619.         }

  620.         log.warn("Done.");
  621.     }

  622.     private void print(String message, TaxonomySpecies species) {
  623.         TaxonomyGenus tg = species.getTaxonomyGenus();
  624.         log.info("{} {} {} {} proto={} id={}/{} tgid={}/{}",
  625.             message,
  626.             StringUtils.defaultIfBlank(species.getSynonymCode(), ""),
  627.             species.getName(), species.getNameAuthority(),
  628.             species.getProtologue(),
  629.             species.getId(), species.getGrinId(),
  630.             (tg == null ? "null" : tg.getId()), (tg == null ? "null" : tg.getGrinId())
  631.         );
  632.     }

  633.     private String indexLookupKey(TaxonomyGenus genus) {
  634.         return StringUtils.substring(genus.getGenusName(), 0, 3);
  635.     }

  636.     private void print(String message, TaxonomyGenus m) {
  637.         log.info("{} {} {}{} {} {} {} {} {} {} tf={} gid={}/{}",
  638.             message,
  639.             m.getQualifyingCode(),
  640.             StringUtils.defaultIfBlank(m.getHybridCode(), ""), m.getGenusName(),
  641.             m.getGenusAuthority(),
  642.             m.getSubgenusName(),
  643.             m.getSectionName(), m.getSubsectionName(),
  644.             m.getSeriesName(), m.getSubseriesName(),
  645.             (m.getTaxonomyFamily() == null ? null : m.getTaxonomyFamily().getId()), m.getId(), m.getGrinId());
  646.     }

  647.     static void downloadDataIfNeeded(File folder) throws IOException {
  648.         if (!folder.exists()) {
  649.             log.warn("Making directory " + folder.getAbsolutePath());

  650.             if (!folder.mkdirs() || !folder.exists()) {
  651.                 throw new IOException("Failed to create data folder at " + folder.getAbsolutePath());
  652.             }
  653.         }

  654.         // The two required files
  655.         final File genusFile = new File(folder, "taxonomy_genus.txt");
  656.         final File speciesFile = new File(folder, "taxonomy_species.txt");

  657.         if (!genusFile.exists() || !speciesFile.exists()) {
  658.             log.warn("Taxonomy data not provided in {}, starting download", folder.getAbsolutePath());
  659.             final TaxonomyDownloader dl = new TaxonomyDownloader();

  660.             log.warn("Downloading GRIN-Taxonomy database to {}", folder.getAbsolutePath());
  661.             final File downloadedCabFile = File.createTempFile("grin-", ".cab");
  662.             dl.downloadCurrent(downloadedCabFile);

  663.             TaxonomyDownloader.unpackCabinetFile(downloadedCabFile, folder, false);
  664.             if (downloadedCabFile.exists() && downloadedCabFile.canWrite()) {
  665.                 log.warn("Deleting downloaded file {}", downloadedCabFile.getAbsolutePath());
  666.                 FileUtils.forceDelete(downloadedCabFile);
  667.             }
  668.         }
  669.     }

  670.     /**
  671.      * Implementation of a group-by list
  672.      *
  673.      * @param <K> key
  674.      * @param <V> value
  675.      */
  676.     public static class LookupList<K, V> extends HashMap<K, List<V>> {
  677.         private static final long serialVersionUID = 2452703619583443005L;

  678.         public V add(K key, V element) {
  679.             computeIfAbsent(key, k -> new LinkedList<>()).add(element);
  680.             return element;
  681.         }
  682.     }
  683. }