package org.gbif.checklistbank.service.mybatis;
import org.gbif.api.model.checklistbank.NameUsage;
import org.gbif.api.model.checklistbank.NameUsageMetrics;
import org.gbif.api.model.checklistbank.ParsedName;
import org.gbif.api.model.checklistbank.VerbatimNameUsage;
import org.gbif.api.model.checklistbank.VernacularName;
import org.gbif.api.model.common.paging.Pageable;
import org.gbif.api.model.common.paging.PagingRequest;
import org.gbif.api.model.common.paging.PagingResponse;
import org.gbif.api.service.checklistbank.NameUsageService;
import org.gbif.checklistbank.model.RawUsage;
import org.gbif.checklistbank.service.mybatis.mapper.NameUsageMapper;
import org.gbif.checklistbank.service.mybatis.mapper.NameUsageMetricsMapper;
import org.gbif.checklistbank.service.mybatis.mapper.ParsedNameMapper;
import org.gbif.checklistbank.service.mybatis.mapper.RawUsageMapper;
import org.gbif.checklistbank.service.mybatis.mapper.VerbatimNameUsageMapperJson;
import org.gbif.checklistbank.service.mybatis.mapper.VernacularNameMapper;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nullable;
import javax.sql.DataSource;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements the NameUsageService using MyBatis.
* All PagingResponses will not have the count set as it can be too costly sometimes.
*/
public class NameUsageServiceMyBatis implements NameUsageService {
private static final Logger LOG = LoggerFactory.getLogger(NameUsageServiceMyBatis.class);
private final NameUsageMapper mapper;
private final NameUsageMetricsMapper metricsMapper;
private final ParsedNameMapper parsedNameMapper;
private final VernacularNameMapper vernacularNameMapper;
private final RawUsageMapper rawUsageMapper;
private final VerbatimNameUsageMapperJson verbatimParser = new VerbatimNameUsageMapperJson();
@Inject
private DataSource ds;
@Inject
NameUsageServiceMyBatis(NameUsageMapper mapper, VernacularNameMapper vernacularNameMapper,
NameUsageMetricsMapper metricsMapper,
RawUsageMapper rawUsageMapper, ParsedNameMapper parsedNameMapper) {
this.mapper = mapper;
this.metricsMapper = metricsMapper;
this.vernacularNameMapper = vernacularNameMapper;
this.rawUsageMapper = rawUsageMapper;
this.parsedNameMapper = parsedNameMapper;
}
@Override
public NameUsage get(int usageKey, @Nullable Locale locale) {
NameUsage usage = mapper.get(usageKey);
addVernacularName(usage, getLanguage(locale));
return usage;
}
@Override
public VerbatimNameUsage getVerbatim(int usageKey) {
VerbatimNameUsage v = null;
RawUsage raw = rawUsageMapper.get(usageKey);
if (raw != null) {
v = verbatimParser.read(raw.getJson());
if (v != null) {
// we might not have crawled that record yet with the new crawling
v.setKey(usageKey);
v.setLastCrawled(raw.getLastCrawled());
}
}
return v;
}
@VisibleForTesting
protected void insertRaw(RawUsage raw) {
rawUsageMapper.insert(raw);
}
@VisibleForTesting
protected RawUsage getRaw(int usageKey) {
return rawUsageMapper.get(usageKey);
}
@Override
public ParsedName getParsedName(int usageKey) {
return parsedNameMapper.getByUsageKey(usageKey);
}
@Nullable
@Override
public NameUsageMetrics getMetrics(int usageKey) {
return metricsMapper.get(usageKey);
}
@Override
public PagingResponse<NameUsage> list(Locale locale, @Nullable UUID datasetKey, @Nullable String sourceId,
@Nullable Pageable page) {
if (page == null) {
page = new PagingRequest();
}
List<NameUsage> usages;
// if a sourceID has been provided as a filter, there wont be too many results
if (!Strings.isNullOrEmpty(sourceId)) {
usages = mapper.listByTaxonId(datasetKey, sourceId, page);
} else {
usages = Lists.newArrayList();
// we need to page over ids only and then lookup each full usage by its key
// otherwise the db queries get too heavy for the database if offsets are high!
List<Integer> usageIds = mapper.list(datasetKey, page);
for (Integer uid : usageIds) {
usages.add(get(uid, locale));
}
}
return localizedPage(locale, usages, page);
}
@Override
public PagingResponse<NameUsage> listByCanonicalName(Locale locale, String canonicalName, @Nullable Pageable page,
@Nullable UUID... datasetKey) {
if (page == null) {
page = new PagingRequest();
}
List<NameUsage> usages = mapper.listByCanonicalName(canonicalName, datasetKey, page);
return localizedPage(locale, usages, page);
}
@Override
public List<NameUsage> listParents(int usageKey, @Nullable Locale locale) {
LinkedList<NameUsage> parents = Lists.newLinkedList();
NameUsage curr = get(usageKey, locale);
if (curr != null) {
// make sure we don't hit some circular structures which should not exist really, but who knows...
Set<Integer> visited = Sets.newHashSet();
visited.add(usageKey);
while (curr.getParentKey() != null) {
int parentKey = curr.getParentKey();
if (visited.contains(parentKey)) {
LOG.warn("Taxonomic hierarchy for usage {} contains circular references!", parentKey);
break;
}
visited.add(parentKey);
curr = get(parentKey, locale);
if (curr == null) {
LOG.warn("Usage not found for parent key {}", parentKey);
break;
}
parents.addFirst(curr);
}
}
return parents;
}
@Override
public PagingResponse<NameUsage> listChildren(int parentKey, @Nullable Locale locale, @Nullable Pageable page) {
if (page == null) {
page = new PagingRequest();
}
return localizedPage(locale, mapper.listChildren(parentKey, page), page);
}
private PagingResponse<NameUsage> localizedPage(Locale locale, List<NameUsage> usages, Pageable requestPage) {
addVernacularNames(usages, getLanguage(locale));
return new PagingResponse<NameUsage>(requestPage, null, usages);
}
@Override
public PagingResponse<NameUsage> listSynonyms(int usageKey, @Nullable Locale locale, @Nullable Pageable page) {
if (page == null) {
page = new PagingRequest();
}
List<NameUsage> usages = mapper.listSynonyms(usageKey, page);
return localizedPage(locale, usages, page);
}
@Override
public PagingResponse<NameUsage> listRoot(UUID datasetKey, @Nullable Locale locale, @Nullable Pageable page) {
if (page == null) {
page = new PagingRequest();
}
List<NameUsage> usages = mapper.listRoot(datasetKey, page);
return localizedPage(locale, usages, page);
}
@Override
public PagingResponse<NameUsage> listRelated(int nubKey, @Nullable Locale locale, @Nullable Pageable page, @Nullable UUID... datasetKey) {
if (page == null) {
page = new PagingRequest();
}
List<NameUsage> usages = mapper.listRelated(nubKey, datasetKey, page);
return localizedPage(locale, usages, page);
}
@Override
public List<NameUsage> listCombinations(int basionymKey, Locale locale) {
List<NameUsage> usages = mapper.listCombinations(basionymKey);
addVernacularNames(usages, getLanguage(locale));
return usages;
}
/**
* @return the lowercase 2 letter iso language code or null
*/
protected static String getLanguage(@Nullable Locale locale) {
if (locale == null || locale.getLanguage() == null || locale.getLanguage().length() != 2) {
return null;
}
return locale.getLanguage().toLowerCase();
}
/**
* Adds a matching vernacular name of a given language to a list of existing name usages.
* See {@link #addVernacularName(org.gbif.api.model.checklistbank.NameUsage, String)} for logic used in picking the common name.
*
* @param usages the checklist usages to add a vernacular name to
* @param language the requested language
*/
private void addVernacularNames(List<NameUsage> usages, @Nullable String language) {
if (language == null) return;
for (NameUsage u : usages) {
addVernacularName(u, language);
}
}
/**
* Adds a vernacular name of a given language to an existing name usage.
* Nothing is added if there is no vernacular name in the given language.
*
* @param u the checklist usage to add the vernacular name to
* @param language the requested language
*/
private void addVernacularName(NameUsage u, @Nullable String language) {
if (u == null || language == null) {
return;
}
// first try with given language
VernacularName v = getVernacularNameInLanguage(u, language);
if (v != null) {
u.setVernacularName(v.getVernacularName());
}
}
/**
* Gets a single vernacular name for a given language and a name usage.
*
* @param u the name usage of the vernacular name
* @param language the requested language, never null
*
* @return vernacular name or null if none can be found
*/
private VernacularName getVernacularNameInLanguage(NameUsage u, String language) {
if (u.isNub()) {
return vernacularNameMapper.getByNubUsage(u.getKey(), language);
} else {
return vernacularNameMapper.getByChecklistUsage(u.getKey(), language);
}
}
}