GeoServiceImpl.java

  1. /*
  2.  * Copyright 2018 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.genesys.server.service.impl;

  17. import java.io.IOException;
  18. import java.util.ArrayList;
  19. import java.util.Collections;
  20. import java.util.Comparator;
  21. import java.util.HashSet;
  22. import java.util.Iterator;
  23. import java.util.List;
  24. import java.util.Locale;
  25. import java.util.Map;
  26. import java.util.Set;
  27. import java.util.TreeSet;
  28. import java.util.stream.Collectors;

  29. import org.apache.commons.lang3.StringUtils;
  30. import org.genesys.server.exception.NotFoundElement;
  31. import org.genesys.server.exception.SearchException;
  32. import org.genesys.server.model.genesys.Accession;
  33. import org.genesys.server.model.impl.Country;
  34. import org.genesys.server.model.impl.FaoInstitute;
  35. import org.genesys.server.model.impl.ITPGRFAStatus;
  36. import org.genesys.server.model.impl.QCountry;
  37. import org.genesys.server.model.vocab.VocabularyTerm;
  38. import org.genesys.server.persistence.CountryRepository;
  39. import org.genesys.server.persistence.ITPGRFAStatusRepository;
  40. import org.genesys.server.service.AccessionService;
  41. import org.genesys.server.service.CRMException;
  42. import org.genesys.server.service.ContentService;
  43. import org.genesys.server.service.ElasticsearchService;
  44. import org.genesys.server.service.GenesysService;
  45. import org.genesys.server.service.GeoService;
  46. import org.genesys.server.service.InstituteService;
  47. import org.genesys.server.service.filter.AccessionFilter;
  48. import org.genesys.server.service.filter.CountryFilter;
  49. import org.genesys.server.service.worker.CountryInfo;
  50. import org.genesys.server.service.worker.CustomISO3166Source;
  51. import org.genesys.server.service.worker.CustomISO3166Source.Custom3166Entry;
  52. import org.genesys.server.service.worker.DavrosCountrySource;
  53. import org.genesys.server.service.worker.GeoNamesCountrySource;
  54. import org.slf4j.Logger;
  55. import org.slf4j.LoggerFactory;
  56. import org.springframework.beans.factory.annotation.Autowired;
  57. import org.springframework.cache.annotation.CacheEvict;
  58. import org.springframework.data.domain.Page;
  59. import org.springframework.data.domain.Pageable;
  60. import org.springframework.data.domain.Sort;
  61. import org.springframework.security.access.prepost.PreAuthorize;
  62. import org.springframework.stereotype.Service;
  63. import org.springframework.transaction.annotation.Transactional;

  64. import com.fasterxml.jackson.databind.JsonNode;
  65. import com.fasterxml.jackson.databind.ObjectMapper;
  66. import com.fasterxml.jackson.databind.node.ArrayNode;
  67. import com.fasterxml.jackson.databind.node.ObjectNode;
  68. import com.google.common.collect.Sets;
  69. import com.querydsl.core.Tuple;
  70. import com.querydsl.core.types.Predicate;
  71. import com.querydsl.jpa.impl.JPAQueryFactory;

  72. @Service
  73. @Transactional(readOnly = true)
  74. public class GeoServiceImpl implements GeoService {
  75.     public static final Logger LOG = LoggerFactory.getLogger(GeoServiceImpl.class);

  76.     @Autowired
  77.     private JPAQueryFactory jpaQueryFactory;

  78.     @Autowired
  79.     CountryRepository countryRepository;

  80.     @Autowired
  81.     ContentService contentService;

  82.     @Autowired
  83.     ITPGRFAStatusRepository itpgrfaRepository;

  84.     @Autowired(required = false)
  85.     private GeoNamesCountrySource geoNamesCountrySource;

  86.     @Autowired(required = false)
  87.     private DavrosCountrySource davrosCountrySource;

  88.     @Autowired(required = false)
  89.     private CustomISO3166Source customISO3166Source;

  90.     @Autowired(required = false)
  91.     private ElasticsearchService elasticsearchService;

  92.     @Autowired
  93.     private InstituteService instituteService;

  94.     @Autowired
  95.     private AccessionService accessionService;

  96.     @Autowired
  97.     private GenesysService genesysService;

  98.     private final ObjectMapper mapper = new ObjectMapper();

  99.     @Override
  100.     public List<Country> listAll() {
  101.         return countryRepository.findAll(Sort.by("name", "current"));
  102.     }

  103.     @Override
  104.     public List<Country> listAll(final Locale locale) {
  105.         final List<Country> all = listAll();
  106.         Collections.sort(all, new Comparator<Country>() {
  107.             @Override
  108.             public int compare(Country o1, Country o2) {
  109.                 return o1.getName(locale).compareTo(o2.getName(locale));
  110.             }
  111.         });
  112.         return all;
  113.     }

  114.     @Override
  115.     public List<Country> listActive(final Locale locale) {
  116.         final List<Country> countries = countryRepository.findByCurrent(true);
  117.         Country.sort(countries, locale);
  118.         return countries;
  119.     }

  120.     @Override
  121.     public long countActive() {
  122.         return countryRepository.countByCurrent(true);
  123.     }

  124.     @Override
  125.     public List<Country> listITPGRFA(Locale locale) {
  126.         final List<Country> countries = countryRepository.findITPGRFA();
  127.         Country.sort(countries, locale);
  128.         return countries;
  129.     }

  130.     @Override
  131.     public Country getCountry(String isoCode) {
  132.         Country country = null;
  133.         if (isoCode.length() == 3) {
  134.             country = countryRepository.findByCode3(isoCode);
  135.         } else if (isoCode.length() == 2) {
  136.             // RO = ROU and ROM
  137.             country = countryRepository.findByCode2AndCurrent(isoCode, true);
  138.             if (country == null) {
  139.                 country = countryRepository.findByCode2AndCurrent(isoCode, false);
  140.             }
  141.         }

  142.         return country;
  143.     }

  144.     @Override
  145.     public Country findCountry(String countryString) {
  146.         Country country = getCountry(countryString);

  147.         // Let's try the name
  148.         if (country == null) {
  149.             country = countryRepository.findByName(countryString);
  150.         }

  151.         // Let's try translations
  152.         if (country == null) {
  153.             country = findCountryByName(countryString);
  154.         }
  155.        
  156.         // Let's sanitize input
  157.         if (country == null) {
  158.             country = findCountryByName(countryString.replaceAll("\\s*\\(.[^\\)]+\\)", ""));
  159.             if (country != null) {
  160.                 LOG.warn("Found country {} for {}", country.getName(), countryString);
  161.             } else {
  162.                 LOG.warn("No country for {} - queried by {}", countryString, countryString.replaceAll("\\s*\\(.[^\\)]+\\)", ""));
  163.             }
  164.         }

  165.         return country;
  166.     }

  167.     /**
  168.      * Check if we have a country that has
  169.      *
  170.      * @param name in i18n
  171.      * @return
  172.      */
  173.     private Country findCountryByName(String name) {
  174.         final List<Country> countries = countryRepository.findWithI18N("%" + name.trim() + "%");
  175.         LOG.debug("Found {} that have {}", countries.size(), name);
  176.         for (final Country c : countries) {
  177.             try {
  178.                 final JsonNode nameJ = mapper.readTree(c.getNameL());
  179.                 final Iterator<JsonNode> it = nameJ.elements();
  180.                 while (it.hasNext()) {
  181.                     final JsonNode el = it.next();
  182.                     if (name.equalsIgnoreCase(el.textValue())) {
  183.                         LOG.debug("Found match for {} in: {}", name, c.getName());
  184.                         return c;
  185.                     }
  186.                 }
  187.             } catch (final IOException e) {
  188.                 LOG.error(e.getMessage(), e);
  189.             }
  190.         }
  191.         return null;
  192.     }

  193.     /**
  194.      * Get current country based on ISO3 code. Follow replacedBy where possible.
  195.      *
  196.      * @param code3
  197.      * @return
  198.      */
  199.     @Override
  200.     public Country getCurrentCountry(String code3) {
  201.         if (code3 == null) {
  202.             return null;
  203.         }

  204.         Country country = getCountry(code3);

  205.         if (country != null && country.getReplacedBy() != null) {
  206.             // Loop detection
  207.             final Set<Long> seenCountryId = new HashSet<Long>();
  208.             while (!seenCountryId.contains(country.getId()) && country.getReplacedBy() != null) {
  209.                 LOG.info("Country {} replaced by {}", country.getCode3(), country.getReplacedBy());

  210.                 // Put countryId to seen list
  211.                 seenCountryId.add(country.getId());

  212.                 // Update reference
  213.                 country = country.getReplacedBy();
  214.             }
  215.         }

  216.         return country;
  217.     }

  218.     @Override
  219.     public Country getCountryByRefnameId(long refnameId) {
  220.         return countryRepository.findByRefnameId(refnameId);
  221.     }

  222.     @Override
  223.     @Transactional(readOnly = false)
  224.     @CacheEvict(value = "statistics", allEntries = true)
  225.     public void updateCountryData() throws IOException {
  226.         // update current countries
  227.         updateGeoNamesCountries();

  228.         // update from Davros, it has info on inactive country codes
  229.         updateDavrosCountries();

  230.         // update custom ISO3316-3 codes
  231.         updateCustomCountryCodes();

  232.         LOG.info("Country data up to date");
  233.     }

  234.     private void updateCustomCountryCodes() throws IOException {
  235.         if (customISO3166Source == null) {
  236.             LOG.warn("Custom ISO3166 reader not available");
  237.             return;
  238.         }

  239.         final List<Custom3166Entry> countries = customISO3166Source.fetchCountryData();
  240.         if (LOG.isDebugEnabled()) {
  241.             LOG.debug("Got {} custom ISO3166 codes from source.", countries.size());
  242.         }

  243.         // check against repository
  244.         for (final Custom3166Entry countryInfo : countries) {
  245.             final Country country = countryRepository.findByCode3(countryInfo.getCode3());

  246.             if (country == null) {
  247.                 LOG.info("ISO3166 code {} is not registered: {}", countryInfo.getCode3(), countryInfo);

  248.                 final Country newCountry = new Country();
  249.                 newCountry.setCode3(countryInfo.getCode3());
  250.                 newCountry.setName(countryInfo.getName());
  251.                 newCountry.setWikiLink(countryInfo.getUrl());
  252.                 newCountry.setCurrent(true);
  253.                 countryRepository.save(newCountry);

  254.                 LOG.info("Added ISO3166 code {}", newCountry);
  255.             } else {
  256.                 LOG.debug("Updating existing custom ISO3166 code {}", country.getCode3());
  257.                
  258.                 country.setCurrent(true);
  259.                 country.setName(countryInfo.getName());
  260.                 country.setWikiLink(countryInfo.getUrl());
  261.                 countryRepository.save(country);
  262.             }
  263.         }
  264.     }

  265.     private void updateDavrosCountries() throws IOException {
  266.         if (davrosCountrySource == null) {
  267.             LOG.warn("davros.org country source not available");
  268.             return;
  269.         }

  270.         final List<CountryInfo> countries = davrosCountrySource.fetchCountryData();

  271.         if (LOG.isDebugEnabled()) {
  272.             LOG.debug("Got {} countries from remote source.", countries.size());
  273.         }

  274.         // check against repository
  275.         for (final CountryInfo countryInfo : countries) {

  276.             // Country country =
  277.             // countryRepository.findByName(countryInfo.getName());
  278.             final Country country = countryRepository.findByCode3(countryInfo.getIso3());

  279.             if (country == null) {
  280.                 LOG.info("Country {} is not registered: {}", countryInfo.getIso3(), countryInfo);

  281.                 if (countryInfo.isActive()) {
  282.                     LOG.warn("Country is marked as active. Should not be.");
  283.                 }

  284.                 final Country newCountry = new Country();
  285.                 newCountry.setCode2(countryInfo.getIso());
  286.                 newCountry.setCode3(countryInfo.getIso3());
  287.                 newCountry.setCodeNum(countryInfo.getIsoNum());
  288.                 newCountry.setCurrent(countryInfo.isActive());
  289.                 newCountry.setName(countryInfo.getName());
  290.                 countryRepository.save(newCountry);

  291.                 LOG.info("Added country {}", newCountry);

  292.             } else {
  293.                 LOG.debug("Exists {}", country);

  294.                 // if iso2 is blank
  295.                 if (StringUtils.isBlank(country.getCode2()) && StringUtils.isNotBlank(countryInfo.getIso())) {
  296.                     LOG.info("Updating country iso2 code");
  297.                     country.setCode2(countryInfo.getIso());
  298.                     countryRepository.save(country);
  299.                 }

  300.                 // if iso-numeric is blank
  301.                 if (StringUtils.isBlank(country.getCodeNum()) && StringUtils.isNotBlank(countryInfo.getIsoNum())) {
  302.                     LOG.info("Updating country iso-numeric code");
  303.                     country.setCodeNum(countryInfo.getIsoNum());
  304.                     countryRepository.save(country);
  305.                 }
  306.                 /*
  307.                  * // if all fields match if
  308.                  * (country.getCode2().equals(countryInfo.getIso()) &&
  309.                  * country.getCodeNum() != null &&
  310.                  * country.getCodeNum().equals(countryInfo.getIsoNum())) { if
  311.                  * (country.isCurrent() != countryInfo.isActive()) { LOG.warn(
  312.                  * "Country " + country + " is listed as active=" +
  313.                  * countryInfo.isActive() + " on remote site");
  314.                  * country.setCurrent(countryInfo.isActive());
  315.                  * countryRepository.save(country); } }
  316.                  */
  317.             }
  318.         }
  319.     }

  320.     private void updateGeoNamesCountries() throws IOException {
  321.         if (geoNamesCountrySource == null) {
  322.             LOG.warn("geonames.org country source not available");
  323.             return;
  324.         }

  325.         final List<CountryInfo> countries = geoNamesCountrySource.fetchCountryData();

  326.         if (LOG.isDebugEnabled()) {
  327.             LOG.debug("Got {} countries from remote source.", countries.size());
  328.         }

  329.         // deactivate all
  330.         countryRepository.deactivateAll();

  331.         // check against repository
  332.         for (final CountryInfo countryInfo : countries) {
  333.             final Country country = countryRepository.findByCode3(countryInfo.getIso3());

  334.             if (country == null) {
  335.                 LOG.info("Country {} is not registered", countryInfo);

  336.                 final Country newCountry = new Country();
  337.                 newCountry.setCode2(countryInfo.getIso());
  338.                 newCountry.setCode3(countryInfo.getIso3());
  339.                 newCountry.setCodeNum(countryInfo.getIsoNum());
  340.                 newCountry.setCurrent(countryInfo.isActive());
  341.                 newCountry.setName(countryInfo.getName());
  342.                 newCountry.setRefnameId(countryInfo.getRefnameId());
  343.                 countryRepository.save(newCountry);

  344.                 LOG.info("Added country {}", newCountry);

  345.             } else {
  346.                 LOG.debug("Exists {}", country);
  347.                 country.setCurrent(true);
  348.                 country.setCode2(countryInfo.getIso());
  349.                 country.setCodeNum(countryInfo.getIsoNum());
  350.                 country.setName(countryInfo.getName());
  351.                 country.setRefnameId(countryInfo.getRefnameId());
  352.                 countryRepository.save(country);

  353.                 /*
  354.                  * // update refname id if (country.getRefnameId() == null &&
  355.                  * countryInfo.getRefnameId() != null) {
  356.                  * country.setRefnameId(countryInfo.getRefnameId());
  357.                  * countryRepository.save(country); }
  358.                  *
  359.                  * // if country name is not the same if
  360.                  * (StringUtils.isNotBlank(countryInfo.getName()) &&
  361.                  * !countryInfo.getName().equals(country.getName())) {
  362.                  *
  363.                  * LOG.info("Updating country name from: " + country.getName() +
  364.                  * " to: " + countryInfo.getName());
  365.                  * country.setName(countryInfo.getName());
  366.                  * countryRepository.save(country); }
  367.                  */
  368.             }
  369.         }
  370.     }

  371.     @Override
  372.     @PreAuthorize("hasRole('ADMINISTRATOR')")
  373.     @Transactional(readOnly = false)
  374.     public void updateBlurb(Country country, String blurb, Locale locale) throws CRMException {
  375.         // TODO Should we provide summary?
  376.         contentService.updateArticle(country, ContentService.ENTITY_BLURB_SLUG, null, null, blurb, locale);
  377.     }

  378.     @Override
  379.     public List<Long> listCountryRefnameIds() {
  380.         return countryRepository.listRefnameIds();
  381.     }

  382.     @Override
  383.     // TODO Where do we autorize access?
  384.     // @PreAuthorize("hasRole('ADMINISTRATOR')")
  385.     @Transactional(readOnly = false)
  386.     public void updateCountryNames(String isoCode3, String jsonTranslations) {
  387.         Country country = countryRepository.findByCode3(isoCode3);
  388.         country.setNameL(jsonTranslations);
  389.         country = countryRepository.save(country);
  390.         LOG.info("Updated translations of {} i18n={}", country, country.getNameL());
  391.     }

  392.     @Override
  393.     @Transactional(readOnly = false)
  394.     public Country updateCountryWiki(String isoCode3, String wiki) {
  395.         Country country = countryRepository.findByCode3(isoCode3);
  396.         LOG.info("Loaded {} i18n={}", country, country.getNameL());
  397.         country.setWikiLink(wiki);
  398.         country = countryRepository.save(country);
  399.         LOG.info("Updated wiki link of {} i18n={}", country, country.getNameL());
  400.         return country;
  401.     }

  402.     @Override
  403.     public ArrayNode toJson(List<FaoInstitute> members) {
  404.         // Generate JSON
  405.         final ArrayNode jsonArray = mapper.createArrayNode();
  406.         for (final FaoInstitute inst : members) {
  407.             if (inst.getLatitude() != null) {
  408.                 final ObjectNode instNode = mapper.createObjectNode();
  409.                 instNode.put("lat", inst.getLatitude());
  410.                 instNode.put("lng", inst.getLongitude());
  411.                 instNode.put("elevation", inst.getElevation());
  412.                 instNode.put("title", inst.getFullName());
  413.                 instNode.put("code", inst.getCode());
  414.                 jsonArray.add(instNode);
  415.             }
  416.         }
  417.         return jsonArray;
  418.     }

  419.     @Override
  420.     public ITPGRFAStatus getITPGRFAStatus(Country country) {
  421.         return itpgrfaRepository.findByCountry(country);
  422.     }

  423.     @Override
  424.     @Transactional(readOnly = false)
  425.     public ITPGRFAStatus updateITPGRFA(Country country, String contractingParty, String membership, String membershipBy) {
  426.         if (country == null) {
  427.             LOG.warn("Country is null, not updating ITPGRFA");
  428.             return null;
  429.         }

  430.         ITPGRFAStatus itpgrfaStatus = itpgrfaRepository.findByCountry(country);
  431.         if (itpgrfaStatus == null) {
  432.             LOG.info("New ITPGRFA entry for {}", country.getName());
  433.             itpgrfaStatus = new ITPGRFAStatus();
  434.             itpgrfaStatus.setCountry(country);
  435.         } else {
  436.             LOG.info("Updating ITPGRFA entry for {}", country.getName());
  437.         }
  438.         itpgrfaStatus.setContractingParty(contractingParty);
  439.         itpgrfaStatus.setMembership(membership);
  440.         itpgrfaStatus.setMembershipBy(membershipBy);

  441.         return itpgrfaRepository.save(itpgrfaStatus);
  442.     }

  443.     @Override
  444.     public String filteredKml(String jsonFilter) {
  445.         return null;
  446.     }

  447.     @Override
  448.     public List<Country> autocomplete(String term) {
  449.         CountryFilter filter = new CountryFilter();
  450.         term = StringUtils.strip(term);
  451.         if (term.length() == 0) {
  452.             return List.of();
  453.         }
  454.         filter._text(term + " | " + term + "*");
  455.         List<Country> autocompleteCountries = new ArrayList<>();
  456.         try {
  457.             autocompleteCountries = elasticsearchService.find(Country.class, filter);
  458.         } catch (SearchException e) {
  459.             LOG.error("Error in searching countries with term: {}", term);
  460.         }
  461.         return autocompleteCountries;
  462.     }

  463.     @Override
  464.     public List<VocabularyTerm> autoCompleteTerm(String ac) {
  465.         return autocomplete(ac).stream().map((country) -> toVocabularyTerm(country, country.getCode3())).collect(Collectors.toList());
  466.     }

  467.     @Override
  468.     public CountryDetails getDetails(String iso3code) {
  469.         Country country = getCountry(iso3code);
  470.         if (country == null) {
  471.             throw new NotFoundElement("Cannot find country by ISO code " + iso3code);
  472.         }

  473.         AccessionFilter byCountry = new AccessionFilter();
  474.         byCountry.countryOfOrigin(new CountryFilter())
  475.             .countryOfOrigin.code3 = Sets.newHashSet(country.getCode3());
  476.         Long accessionCount = null;
  477.         try {
  478.             accessionCount = accessionService.countAccessions(byCountry);
  479.         } catch (SearchException e) {
  480.             LOG.warn("Error occurred during search", e);
  481.         }
  482.         long countByLocation = genesysService.countByLocation(country);

  483.         Map<String, ElasticsearchService.TermResult> overview = getOverviewData(byCountry);
  484.         List<FaoInstitute> genesysInstitutes = instituteService.listByCountryActive(country);
  485.         List<FaoInstitute> faoInstitutes = instituteService.listByCountry(country);

  486.         return CountryDetails.from(country, getITPGRFAStatus(country), accessionCount, countByLocation, faoInstitutes, genesysInstitutes, overview);
  487.     }

  488.     private Map<String, ElasticsearchService.TermResult> getOverviewData(AccessionFilter byCountryFilter) {
  489.         String[] terms = new String[] {"taxonomy.genus", "taxonomy.genusSpecies", "institute.code",
  490.             "institute.country.code3", "mlsStatus", "available"};

  491.         if (elasticsearchService == null)
  492.             return Map.of();

  493.         try {
  494.             return elasticsearchService.termStatisticsAuto(Accession.class, byCountryFilter, 10, terms);
  495.         } catch (SearchException e) {
  496.             LOG.error("Error occurred during search", e);
  497.             return null;
  498.         }
  499.     }

  500.     @Override
  501.     public String getBoundingBox(final Set<String> iso3Codes) {
  502.         List<String> countryIso3List = new ArrayList<>(iso3Codes);

  503.         Set<Double> minLatitudeSet = new TreeSet<>(Double::compareTo);
  504.         Set<Double> minLongitudeSet = new TreeSet<>(Double::compareTo);
  505.         Set<Double> maxLongitudeSet = new TreeSet<>(Comparator.reverseOrder());
  506.         Set<Double> maxLatitudeSet = new TreeSet<>(Comparator.reverseOrder());

  507.         for (String iso3: countryIso3List) {
  508.             Country country = getCountry(iso3);
  509.             if (country != null) {
  510.                 if (country.getMinLatitude() != null) minLatitudeSet.add(country.getMinLatitude());
  511.                 if (country.getMinLongitude() != null) minLongitudeSet.add(country.getMinLongitude());
  512.                 if (country.getMaxLatitude() != null) maxLatitudeSet.add(country.getMaxLatitude());
  513.                 if (country.getMaxLongitude() != null) maxLongitudeSet.add(country.getMaxLongitude());
  514.             }
  515.         }

  516.         final ObjectNode geoJson = mapper.createObjectNode();
  517.         geoJson.put("north", maxLatitudeSet.stream().findFirst().orElse(0.0));
  518.         geoJson.put("south", minLatitudeSet.stream().findFirst().orElse(0.0));
  519.         geoJson.put("east", maxLongitudeSet.stream().findFirst().orElse(0.0));
  520.         geoJson.put("west", minLongitudeSet.stream().findFirst().orElse(0.0));
  521.         return geoJson.toString();
  522.     }

  523.     private VocabularyTerm toVocabularyTerm(Country country, String code) {

  524.         if (country == null) {
  525.             throw new NotFoundElement("No such country term");
  526.         }

  527.         VocabularyTerm countryTerm = new VocabularyTerm();

  528.         countryTerm.setCode(code);
  529.         countryTerm.setTitle(country.getName());

  530.         return countryTerm;
  531.     }

  532.     @Override
  533.     public Page<VocabularyTerm> list3166Alpha2Terms(Pageable page) {
  534.         Page<Country> res = countryRepository.findAll(QCountry.country.code2.isNotNull(), page);
  535.         return res.map(c -> toVocabularyTerm(c, c.getCode2()));
  536.     }

  537.     @Override
  538.     public Page<VocabularyTerm> list3166Alpha3Terms(Pageable page) {
  539.         Page<Country> res = countryRepository.findAll(QCountry.country.code3.isNotNull(), page);
  540.         return res.map(c -> toVocabularyTerm(c, c.getCode3()));
  541.     }

  542.     @Override
  543.     public Map<String, String> decode3166Alpha3Terms(Set<String> codes, Locale locale) {
  544.         Predicate whereClause = codes != null ? QCountry.country.code3.in(codes) : null;

  545.         List<Tuple> query = jpaQueryFactory.select(QCountry.country.code3, QCountry.country.nameL, QCountry.country.name)
  546.                 .from(QCountry.country)
  547.                 .where(whereClause)
  548.                 .fetch();
  549.         return query.stream().collect(
  550.                 Collectors.toMap(
  551.                         tuple -> tuple.get(QCountry.country.code3),
  552.                         tuple -> decodeNameLocal(locale, tuple.get(QCountry.country.nameL), tuple.get(QCountry.country.name))
  553.                 )
  554.         );
  555.     }

  556.     @Override
  557.     public Page<VocabularyTerm> list3166NumericTerms(Pageable page) {
  558.         Page<Country> res = countryRepository.findAll(QCountry.country.codeNum.isNotNull(), page);
  559.         return res.map(c -> toVocabularyTerm(c, c.getCodeNum() != null ? c.getCodeNum().toString() : null));

  560.     }

  561.     @Override
  562.     public VocabularyTerm get3166Alpha2Term(String code) {
  563.         Country c = countryRepository.findByCode2(code);
  564.         return c == null ? null : toVocabularyTerm(c, c.getCode2());
  565.     }

  566.     @Override
  567.     public VocabularyTerm get3166Alpha3Term(String code) {
  568.         Country c = countryRepository.findByCode3(code);
  569.         return c == null ? null : toVocabularyTerm(c, c.getCode3());
  570.     }

  571.     @Override
  572.     public VocabularyTerm get3166NumericTerm(String code) {
  573.         Country c = countryRepository.findByCodeNum(code);
  574.         return c == null ? null : toVocabularyTerm(c, c.getCodeNum().toString());
  575.     }

  576.     private String decodeNameLocal(Locale locale, String nameL, String name){
  577.         if (nameL == null)
  578.             return name;

  579.         try {
  580.             JsonNode nameJ = mapper.readTree(nameL);
  581.             if (nameJ.has(locale.toLanguageTag())) {
  582.                 return nameJ.get(locale.toLanguageTag()).textValue();
  583.             } else if (nameJ.has(locale.getLanguage())) {
  584.                 return nameJ.get(locale.getLanguage()).textValue();
  585.             }
  586.             return name;
  587.         } catch (IOException e) {
  588.             LOG.warn("Error while decoding country code: {}", e.getMessage());
  589.             return  name;
  590.         }
  591.     }

  592. }