GeoServiceImpl.java
/*
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.server.service.impl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.exception.SearchException;
import org.genesys.server.model.genesys.Accession;
import org.genesys.server.model.impl.Country;
import org.genesys.server.model.impl.FaoInstitute;
import org.genesys.server.model.impl.ITPGRFAStatus;
import org.genesys.server.model.impl.QCountry;
import org.genesys.server.model.vocab.VocabularyTerm;
import org.genesys.server.persistence.CountryRepository;
import org.genesys.server.persistence.ITPGRFAStatusRepository;
import org.genesys.server.service.AccessionService;
import org.genesys.server.service.CRMException;
import org.genesys.server.service.ContentService;
import org.genesys.server.service.ElasticsearchService;
import org.genesys.server.service.GenesysService;
import org.genesys.server.service.GeoService;
import org.genesys.server.service.InstituteService;
import org.genesys.server.service.filter.AccessionFilter;
import org.genesys.server.service.filter.CountryFilter;
import org.genesys.server.service.worker.CountryInfo;
import org.genesys.server.service.worker.CustomISO3166Source;
import org.genesys.server.service.worker.CustomISO3166Source.Custom3166Entry;
import org.genesys.server.service.worker.DavrosCountrySource;
import org.genesys.server.service.worker.GeoNamesCountrySource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Sets;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.Predicate;
import com.querydsl.jpa.impl.JPAQueryFactory;
@Service
@Transactional(readOnly = true)
public class GeoServiceImpl implements GeoService {
public static final Logger LOG = LoggerFactory.getLogger(GeoServiceImpl.class);
@Autowired
private JPAQueryFactory jpaQueryFactory;
@Autowired
CountryRepository countryRepository;
@Autowired
ContentService contentService;
@Autowired
ITPGRFAStatusRepository itpgrfaRepository;
@Autowired(required = false)
private GeoNamesCountrySource geoNamesCountrySource;
@Autowired(required = false)
private DavrosCountrySource davrosCountrySource;
@Autowired(required = false)
private CustomISO3166Source customISO3166Source;
@Autowired(required = false)
private ElasticsearchService elasticsearchService;
@Autowired
private InstituteService instituteService;
@Autowired
private AccessionService accessionService;
@Autowired
private GenesysService genesysService;
private final ObjectMapper mapper = new ObjectMapper();
@Override
public List<Country> listAll() {
return countryRepository.findAll(Sort.by("name", "current"));
}
@Override
public List<Country> listAll(final Locale locale) {
final List<Country> all = listAll();
Collections.sort(all, new Comparator<Country>() {
@Override
public int compare(Country o1, Country o2) {
return o1.getName(locale).compareTo(o2.getName(locale));
}
});
return all;
}
@Override
public List<Country> listActive(final Locale locale) {
final List<Country> countries = countryRepository.findByCurrent(true);
Country.sort(countries, locale);
return countries;
}
@Override
public long countActive() {
return countryRepository.countByCurrent(true);
}
@Override
public List<Country> listITPGRFA(Locale locale) {
final List<Country> countries = countryRepository.findITPGRFA();
Country.sort(countries, locale);
return countries;
}
@Override
public Country getCountry(String isoCode) {
Country country = null;
if (isoCode.length() == 3) {
country = countryRepository.findByCode3(isoCode);
} else if (isoCode.length() == 2) {
// RO = ROU and ROM
country = countryRepository.findByCode2AndCurrent(isoCode, true);
if (country == null) {
country = countryRepository.findByCode2AndCurrent(isoCode, false);
}
}
return country;
}
@Override
public Country findCountry(String countryString) {
Country country = getCountry(countryString);
// Let's try the name
if (country == null) {
country = countryRepository.findByName(countryString);
}
// Let's try translations
if (country == null) {
country = findCountryByName(countryString);
}
// Let's sanitize input
if (country == null) {
country = findCountryByName(countryString.replaceAll("\\s*\\(.[^\\)]+\\)", ""));
if (country != null) {
LOG.warn("Found country {} for {}", country.getName(), countryString);
} else {
LOG.warn("No country for {} - queried by {}", countryString, countryString.replaceAll("\\s*\\(.[^\\)]+\\)", ""));
}
}
return country;
}
/**
* Check if we have a country that has
*
* @param name in i18n
* @return
*/
private Country findCountryByName(String name) {
final List<Country> countries = countryRepository.findWithI18N("%" + name.trim() + "%");
LOG.debug("Found {} that have {}", countries.size(), name);
for (final Country c : countries) {
try {
final JsonNode nameJ = mapper.readTree(c.getNameL());
final Iterator<JsonNode> it = nameJ.elements();
while (it.hasNext()) {
final JsonNode el = it.next();
if (name.equalsIgnoreCase(el.textValue())) {
LOG.debug("Found match for {} in: {}", name, c.getName());
return c;
}
}
} catch (final IOException e) {
LOG.error(e.getMessage(), e);
}
}
return null;
}
/**
* Get current country based on ISO3 code. Follow replacedBy where possible.
*
* @param code3
* @return
*/
@Override
public Country getCurrentCountry(String code3) {
if (code3 == null) {
return null;
}
Country country = getCountry(code3);
if (country != null && country.getReplacedBy() != null) {
// Loop detection
final Set<Long> seenCountryId = new HashSet<Long>();
while (!seenCountryId.contains(country.getId()) && country.getReplacedBy() != null) {
LOG.info("Country {} replaced by {}", country.getCode3(), country.getReplacedBy());
// Put countryId to seen list
seenCountryId.add(country.getId());
// Update reference
country = country.getReplacedBy();
}
}
return country;
}
@Override
public Country getCountryByRefnameId(long refnameId) {
return countryRepository.findByRefnameId(refnameId);
}
@Override
@Transactional(readOnly = false)
@CacheEvict(value = "statistics", allEntries = true)
public void updateCountryData() throws IOException {
// update current countries
updateGeoNamesCountries();
// update from Davros, it has info on inactive country codes
updateDavrosCountries();
// update custom ISO3316-3 codes
updateCustomCountryCodes();
LOG.info("Country data up to date");
}
private void updateCustomCountryCodes() throws IOException {
if (customISO3166Source == null) {
LOG.warn("Custom ISO3166 reader not available");
return;
}
final List<Custom3166Entry> countries = customISO3166Source.fetchCountryData();
if (LOG.isDebugEnabled()) {
LOG.debug("Got {} custom ISO3166 codes from source.", countries.size());
}
// check against repository
for (final Custom3166Entry countryInfo : countries) {
final Country country = countryRepository.findByCode3(countryInfo.getCode3());
if (country == null) {
LOG.info("ISO3166 code {} is not registered: {}", countryInfo.getCode3(), countryInfo);
final Country newCountry = new Country();
newCountry.setCode3(countryInfo.getCode3());
newCountry.setName(countryInfo.getName());
newCountry.setWikiLink(countryInfo.getUrl());
newCountry.setCurrent(true);
countryRepository.save(newCountry);
LOG.info("Added ISO3166 code {}", newCountry);
} else {
LOG.debug("Updating existing custom ISO3166 code {}", country.getCode3());
country.setCurrent(true);
country.setName(countryInfo.getName());
country.setWikiLink(countryInfo.getUrl());
countryRepository.save(country);
}
}
}
private void updateDavrosCountries() throws IOException {
if (davrosCountrySource == null) {
LOG.warn("davros.org country source not available");
return;
}
final List<CountryInfo> countries = davrosCountrySource.fetchCountryData();
if (LOG.isDebugEnabled()) {
LOG.debug("Got {} countries from remote source.", countries.size());
}
// check against repository
for (final CountryInfo countryInfo : countries) {
// Country country =
// countryRepository.findByName(countryInfo.getName());
final Country country = countryRepository.findByCode3(countryInfo.getIso3());
if (country == null) {
LOG.info("Country {} is not registered: {}", countryInfo.getIso3(), countryInfo);
if (countryInfo.isActive()) {
LOG.warn("Country is marked as active. Should not be.");
}
final Country newCountry = new Country();
newCountry.setCode2(countryInfo.getIso());
newCountry.setCode3(countryInfo.getIso3());
newCountry.setCodeNum(countryInfo.getIsoNum());
newCountry.setCurrent(countryInfo.isActive());
newCountry.setName(countryInfo.getName());
countryRepository.save(newCountry);
LOG.info("Added country {}", newCountry);
} else {
LOG.debug("Exists {}", country);
// if iso2 is blank
if (StringUtils.isBlank(country.getCode2()) && StringUtils.isNotBlank(countryInfo.getIso())) {
LOG.info("Updating country iso2 code");
country.setCode2(countryInfo.getIso());
countryRepository.save(country);
}
// if iso-numeric is blank
if (StringUtils.isBlank(country.getCodeNum()) && StringUtils.isNotBlank(countryInfo.getIsoNum())) {
LOG.info("Updating country iso-numeric code");
country.setCodeNum(countryInfo.getIsoNum());
countryRepository.save(country);
}
/*
* // if all fields match if
* (country.getCode2().equals(countryInfo.getIso()) &&
* country.getCodeNum() != null &&
* country.getCodeNum().equals(countryInfo.getIsoNum())) { if
* (country.isCurrent() != countryInfo.isActive()) { LOG.warn(
* "Country " + country + " is listed as active=" +
* countryInfo.isActive() + " on remote site");
* country.setCurrent(countryInfo.isActive());
* countryRepository.save(country); } }
*/
}
}
}
private void updateGeoNamesCountries() throws IOException {
if (geoNamesCountrySource == null) {
LOG.warn("geonames.org country source not available");
return;
}
final List<CountryInfo> countries = geoNamesCountrySource.fetchCountryData();
if (LOG.isDebugEnabled()) {
LOG.debug("Got {} countries from remote source.", countries.size());
}
// deactivate all
countryRepository.deactivateAll();
// check against repository
for (final CountryInfo countryInfo : countries) {
final Country country = countryRepository.findByCode3(countryInfo.getIso3());
if (country == null) {
LOG.info("Country {} is not registered", countryInfo);
final Country newCountry = new Country();
newCountry.setCode2(countryInfo.getIso());
newCountry.setCode3(countryInfo.getIso3());
newCountry.setCodeNum(countryInfo.getIsoNum());
newCountry.setCurrent(countryInfo.isActive());
newCountry.setName(countryInfo.getName());
newCountry.setRefnameId(countryInfo.getRefnameId());
countryRepository.save(newCountry);
LOG.info("Added country {}", newCountry);
} else {
LOG.debug("Exists {}", country);
country.setCurrent(true);
country.setCode2(countryInfo.getIso());
country.setCodeNum(countryInfo.getIsoNum());
country.setName(countryInfo.getName());
country.setRefnameId(countryInfo.getRefnameId());
countryRepository.save(country);
/*
* // update refname id if (country.getRefnameId() == null &&
* countryInfo.getRefnameId() != null) {
* country.setRefnameId(countryInfo.getRefnameId());
* countryRepository.save(country); }
*
* // if country name is not the same if
* (StringUtils.isNotBlank(countryInfo.getName()) &&
* !countryInfo.getName().equals(country.getName())) {
*
* LOG.info("Updating country name from: " + country.getName() +
* " to: " + countryInfo.getName());
* country.setName(countryInfo.getName());
* countryRepository.save(country); }
*/
}
}
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR')")
@Transactional(readOnly = false)
public void updateBlurb(Country country, String blurb, Locale locale) throws CRMException {
// TODO Should we provide summary?
contentService.updateArticle(country, ContentService.ENTITY_BLURB_SLUG, null, null, blurb, locale);
}
@Override
public List<Long> listCountryRefnameIds() {
return countryRepository.listRefnameIds();
}
@Override
// TODO Where do we autorize access?
// @PreAuthorize("hasRole('ADMINISTRATOR')")
@Transactional(readOnly = false)
public void updateCountryNames(String isoCode3, String jsonTranslations) {
Country country = countryRepository.findByCode3(isoCode3);
country.setNameL(jsonTranslations);
country = countryRepository.save(country);
LOG.info("Updated translations of {} i18n={}", country, country.getNameL());
}
@Override
@Transactional(readOnly = false)
public Country updateCountryWiki(String isoCode3, String wiki) {
Country country = countryRepository.findByCode3(isoCode3);
LOG.info("Loaded {} i18n={}", country, country.getNameL());
country.setWikiLink(wiki);
country = countryRepository.save(country);
LOG.info("Updated wiki link of {} i18n={}", country, country.getNameL());
return country;
}
@Override
public ArrayNode toJson(List<FaoInstitute> members) {
// Generate JSON
final ArrayNode jsonArray = mapper.createArrayNode();
for (final FaoInstitute inst : members) {
if (inst.getLatitude() != null) {
final ObjectNode instNode = mapper.createObjectNode();
instNode.put("lat", inst.getLatitude());
instNode.put("lng", inst.getLongitude());
instNode.put("elevation", inst.getElevation());
instNode.put("title", inst.getFullName());
instNode.put("code", inst.getCode());
jsonArray.add(instNode);
}
}
return jsonArray;
}
@Override
public ITPGRFAStatus getITPGRFAStatus(Country country) {
return itpgrfaRepository.findByCountry(country);
}
@Override
@Transactional(readOnly = false)
public ITPGRFAStatus updateITPGRFA(Country country, String contractingParty, String membership, String membershipBy) {
if (country == null) {
LOG.warn("Country is null, not updating ITPGRFA");
return null;
}
ITPGRFAStatus itpgrfaStatus = itpgrfaRepository.findByCountry(country);
if (itpgrfaStatus == null) {
LOG.info("New ITPGRFA entry for {}", country.getName());
itpgrfaStatus = new ITPGRFAStatus();
itpgrfaStatus.setCountry(country);
} else {
LOG.info("Updating ITPGRFA entry for {}", country.getName());
}
itpgrfaStatus.setContractingParty(contractingParty);
itpgrfaStatus.setMembership(membership);
itpgrfaStatus.setMembershipBy(membershipBy);
return itpgrfaRepository.save(itpgrfaStatus);
}
@Override
public String filteredKml(String jsonFilter) {
return null;
}
@Override
public List<Country> autocomplete(String term) {
CountryFilter filter = new CountryFilter();
term = StringUtils.strip(term);
if (term.length() == 0) {
return List.of();
}
filter._text(term + " | " + term + "*");
List<Country> autocompleteCountries = new ArrayList<>();
try {
autocompleteCountries = elasticsearchService.find(Country.class, filter);
} catch (SearchException e) {
LOG.error("Error in searching countries with term: {}", term);
}
return autocompleteCountries;
}
@Override
public List<VocabularyTerm> autoCompleteTerm(String ac) {
return autocomplete(ac).stream().map((country) -> toVocabularyTerm(country, country.getCode3())).collect(Collectors.toList());
}
@Override
public CountryDetails getDetails(String iso3code) {
Country country = getCountry(iso3code);
if (country == null) {
throw new NotFoundElement("Cannot find country by ISO code " + iso3code);
}
AccessionFilter byCountry = new AccessionFilter();
byCountry.countryOfOrigin(new CountryFilter())
.countryOfOrigin.code3 = Sets.newHashSet(country.getCode3());
Long accessionCount = null;
try {
accessionCount = accessionService.countAccessions(byCountry);
} catch (SearchException e) {
LOG.warn("Error occurred during search", e);
}
long countByLocation = genesysService.countByLocation(country);
Map<String, ElasticsearchService.TermResult> overview = getOverviewData(byCountry);
List<FaoInstitute> genesysInstitutes = instituteService.listByCountryActive(country);
List<FaoInstitute> faoInstitutes = instituteService.listByCountry(country);
return CountryDetails.from(country, getITPGRFAStatus(country), accessionCount, countByLocation, faoInstitutes, genesysInstitutes, overview);
}
private Map<String, ElasticsearchService.TermResult> getOverviewData(AccessionFilter byCountryFilter) {
String[] terms = new String[] {"taxonomy.genus", "taxonomy.genusSpecies", "institute.code",
"institute.country.code3", "mlsStatus", "available"};
if (elasticsearchService == null)
return Map.of();
try {
return elasticsearchService.termStatisticsAuto(Accession.class, byCountryFilter, 10, terms);
} catch (SearchException e) {
LOG.error("Error occurred during search", e);
return null;
}
}
@Override
public String getBoundingBox(final Set<String> iso3Codes) {
List<String> countryIso3List = new ArrayList<>(iso3Codes);
Set<Double> minLatitudeSet = new TreeSet<>(Double::compareTo);
Set<Double> minLongitudeSet = new TreeSet<>(Double::compareTo);
Set<Double> maxLongitudeSet = new TreeSet<>(Comparator.reverseOrder());
Set<Double> maxLatitudeSet = new TreeSet<>(Comparator.reverseOrder());
for (String iso3: countryIso3List) {
Country country = getCountry(iso3);
if (country != null) {
if (country.getMinLatitude() != null) minLatitudeSet.add(country.getMinLatitude());
if (country.getMinLongitude() != null) minLongitudeSet.add(country.getMinLongitude());
if (country.getMaxLatitude() != null) maxLatitudeSet.add(country.getMaxLatitude());
if (country.getMaxLongitude() != null) maxLongitudeSet.add(country.getMaxLongitude());
}
}
final ObjectNode geoJson = mapper.createObjectNode();
geoJson.put("north", maxLatitudeSet.stream().findFirst().orElse(0.0));
geoJson.put("south", minLatitudeSet.stream().findFirst().orElse(0.0));
geoJson.put("east", maxLongitudeSet.stream().findFirst().orElse(0.0));
geoJson.put("west", minLongitudeSet.stream().findFirst().orElse(0.0));
return geoJson.toString();
}
private VocabularyTerm toVocabularyTerm(Country country, String code) {
if (country == null) {
throw new NotFoundElement("No such country term");
}
VocabularyTerm countryTerm = new VocabularyTerm();
countryTerm.setCode(code);
countryTerm.setTitle(country.getName());
return countryTerm;
}
@Override
public Page<VocabularyTerm> list3166Alpha2Terms(Pageable page) {
Page<Country> res = countryRepository.findAll(QCountry.country.code2.isNotNull(), page);
return res.map(c -> toVocabularyTerm(c, c.getCode2()));
}
@Override
public Page<VocabularyTerm> list3166Alpha3Terms(Pageable page) {
Page<Country> res = countryRepository.findAll(QCountry.country.code3.isNotNull(), page);
return res.map(c -> toVocabularyTerm(c, c.getCode3()));
}
@Override
public Map<String, String> decode3166Alpha3Terms(Set<String> codes, Locale locale) {
Predicate whereClause = codes != null ? QCountry.country.code3.in(codes) : null;
List<Tuple> query = jpaQueryFactory.select(QCountry.country.code3, QCountry.country.nameL, QCountry.country.name)
.from(QCountry.country)
.where(whereClause)
.fetch();
return query.stream().collect(
Collectors.toMap(
tuple -> tuple.get(QCountry.country.code3),
tuple -> decodeNameLocal(locale, tuple.get(QCountry.country.nameL), tuple.get(QCountry.country.name))
)
);
}
@Override
public Page<VocabularyTerm> list3166NumericTerms(Pageable page) {
Page<Country> res = countryRepository.findAll(QCountry.country.codeNum.isNotNull(), page);
return res.map(c -> toVocabularyTerm(c, c.getCodeNum() != null ? c.getCodeNum().toString() : null));
}
@Override
public VocabularyTerm get3166Alpha2Term(String code) {
Country c = countryRepository.findByCode2(code);
return c == null ? null : toVocabularyTerm(c, c.getCode2());
}
@Override
public VocabularyTerm get3166Alpha3Term(String code) {
Country c = countryRepository.findByCode3(code);
return c == null ? null : toVocabularyTerm(c, c.getCode3());
}
@Override
public VocabularyTerm get3166NumericTerm(String code) {
Country c = countryRepository.findByCodeNum(code);
return c == null ? null : toVocabularyTerm(c, c.getCodeNum().toString());
}
private String decodeNameLocal(Locale locale, String nameL, String name){
if (nameL == null)
return name;
try {
JsonNode nameJ = mapper.readTree(nameL);
if (nameJ.has(locale.toLanguageTag())) {
return nameJ.get(locale.toLanguageTag()).textValue();
} else if (nameJ.has(locale.getLanguage())) {
return nameJ.get(locale.getLanguage()).textValue();
}
return name;
} catch (IOException e) {
LOG.warn("Error while decoding country code: {}", e.getMessage());
return name;
}
}
}