CropServiceImpl.java
/**
* Copyright 2014 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.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.model.ImageGallery;
import org.genesys.filerepository.model.RepositoryFolder;
import org.genesys.filerepository.service.ImageGalleryService;
import org.genesys.filerepository.service.RepositoryService;
import org.genesys.server.api.v1.mapper.APIv1Mapper;
import org.genesys.server.component.security.AsAdminInvoker;
import org.genesys.server.exception.InvalidApiUsageException;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.exception.SearchException;
import org.genesys.server.model.dataset.Dataset;
import org.genesys.server.model.filters.DatasetFilter;
import org.genesys.server.model.filters.DescriptorListFilter;
import org.genesys.server.model.genesys.Accession;
import org.genesys.server.model.genesys.QAccession;
import org.genesys.server.model.impl.Article;
import org.genesys.server.model.impl.Crop;
import org.genesys.server.model.impl.CropLang;
import org.genesys.server.model.impl.DiversityTree;
import org.genesys.server.model.impl.Subset;
import org.genesys.server.model.traits.DescriptorList;
import org.genesys.server.persistence.AccessionRepository;
import org.genesys.server.persistence.CropLangRepository;
import org.genesys.server.persistence.CropRepository;
import org.genesys.server.service.AccessionService;
import org.genesys.server.service.ArticleService;
import org.genesys.server.service.ArticleTranslationService;
import org.genesys.server.service.CRMException;
import org.genesys.server.service.ContentService;
import org.genesys.server.service.CropService;
import org.genesys.server.service.CropTranslationService;
import org.genesys.server.service.CropTranslationService.TranslatedCrop;
import org.genesys.server.service.DatasetService;
import org.genesys.server.service.DescriptorListService;
import org.genesys.server.service.DiversityTreeService;
import org.genesys.server.service.ElasticsearchService;
import org.genesys.server.service.HtmlSanitizer;
import org.genesys.server.service.SubsetService;
import org.genesys.server.service.TranslatorService;
import org.genesys.server.service.TranslatorService.FormattedText;
import org.genesys.server.service.TranslatorService.TextFormat;
import org.genesys.server.service.filter.AccessionFilter;
import org.genesys.server.service.filter.CropFilter;
import org.genesys.server.service.filter.DiversityTreeFilter;
import org.genesys.server.service.filter.SubsetFilter;
import org.genesys.server.service.worker.AccessionProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import com.google.common.collect.Sets;
import com.querydsl.core.BooleanBuilder;
@Service
@Transactional(readOnly = true)
@Validated
public class CropServiceImpl
extends FilteredTranslatedCRUDServiceImpl<Crop, CropLang, CropTranslationService.TranslatedCrop, CropFilter, CropRepository>
implements CropService {
private static final String CACHE_CROPS = "hibernate.org.genesys.server.model.impl.Crop.cache";
public static final Logger LOG = LoggerFactory.getLogger(CropServiceImpl.class);
@Lazy
@Autowired
private SubsetService subsetService;
@Lazy
@Autowired
private DatasetService datasetService;
@Lazy
@Autowired
private DiversityTreeService treeService;
@Autowired
private DescriptorListService descriptorListService;
@Autowired
CropRepository cropRepository;
@Autowired
private HtmlSanitizer htmlSanitizer;
@Autowired
private ArticleService articleService;
@Autowired
private ContentService contentServiceV1;
@Autowired
private AccessionService accessionService;
@Autowired(required = false)
private ElasticsearchService elasticsearchService;
@Autowired
private ImageGalleryService imageGalleryService;
@Autowired
private AccessionProcessor accessionProcessor;
@Autowired
private AccessionRepository accessionRepository;
@Autowired
protected AsAdminInvoker asAdminInvoker;
@Autowired
private RepositoryService repositoryService;
@Autowired(required = false)
private TranslatorService translatorService;
@Autowired
private APIv1Mapper apIv1Mapper;
@Component(value = "CropTranslationSupport")
protected static class CropTranslationSupport
extends BaseTranslationSupport<Crop, CropLang, CropTranslationService.TranslatedCrop, CropFilter, CropLangRepository>
implements CropTranslationService {
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@CacheEvict(allEntries = true, value = CACHE_CROPS)
public CropLang create(CropLang source) {
return super.create(source);
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@CacheEvict(allEntries = true, value = CACHE_CROPS)
public CropLang update(CropLang updated, CropLang target) {
return super.update(updated, target);
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@CacheEvict(allEntries = true, value = CACHE_CROPS)
public CropLang remove(CropLang entity) {
return super.remove(entity);
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@CacheEvict(allEntries = true, value = CACHE_CROPS)
public CropLang upsertLang(Crop entity, CropLang input) {
return super.upsertLang(entity, input);
}
}
/**
* @param shortName the crop name ("groundnuts") as provided by partners, but
* can also be a list of names ("peanut,goober,groundnut")
*/
@Override
// We cache null-results so they're not looked up again
@Cacheable(value = CACHE_CROPS, key = "#shortName", unless = "#shortName == null")
public Crop getCrop(String shortName) {
if (StringUtils.isBlank(shortName)) {
return null;
}
for (String cropName : shortName.split(",;")) {
cropName = StringUtils.trimToNull(cropName);
if (cropName != null) {
Crop crop = cropRepository.findByShortName(cropName);
// Find crop by alias when null
if (crop == null) {
crop = cropRepository.findByOtherNames(cropName);
}
if (crop != null) {
// Lazy load otherNames
if (crop.getOtherNames() != null) {
crop.getOtherNames().size();
}
return crop;
}
}
}
return null;
}
@Override
public CropDetails getDetails(String shortName, Locale locale) throws InvalidRepositoryPathException {
LOG.debug("Getting crop details for {}", shortName);
StopWatch stopWatch = StopWatch.createStarted();
Crop crop = getCrop(shortName);
if (crop == null) {
throw new NotFoundElement("No crop: " + shortName);
}
LOG.trace("got crop after {}ms", stopWatch.getTime());
ArticleTranslationService.TranslatedArticle article = articleService.getArticle(crop, ContentService.ENTITY_BLURB_SLUG, locale);
LOG.trace("got article after {}ms", stopWatch.getTime());
AccessionFilter byCrop = new AccessionFilter()
.crop(Sets.newHashSet(crop.getShortName()));
Long accessionCount = null;
try {
accessionCount = accessionService.countAccessions(byCrop);
} catch (SearchException e) {
LOG.warn("Error occurred during count", e);
}
LOG.trace("got count after {}ms", stopWatch.getTime());
ImageGallery imageGallery = imageGalleryService.loadImageGallery(Paths.get("/crop", crop.getShortName(), "covers"));
if (imageGallery != null) {
if (imageGallery.getImages() != null) {
imageGallery.getImages().size();
}
}
LOG.trace("got covers after {}ms", stopWatch.getTime());
RepositoryFolder folder = repositoryService.getFolder(Paths.get("/crop", crop.getShortName()));
Map<String, ElasticsearchService.TermResult> overview = getOverviewData(byCrop);
LOG.trace("got overview after {}ms", stopWatch.getTime());
PageRequest pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "lastModifiedDate"));
SubsetFilter subsetFilter = new SubsetFilter()
.crops(Collections.singleton(crop.getShortName()));
List<Subset> recentSubsets = null;
try {
recentSubsets = subsetService.list(subsetFilter, pageRequest).getContent();
} catch (SearchException e) {
LOG.error("Error occurred during search", e);
}
DatasetFilter datasetFilter = new DatasetFilter()
.crops(Collections.singleton(crop.getShortName()));
List<Dataset> recentDatasets = null;
try {
recentDatasets = datasetService.list(datasetFilter, pageRequest).getContent();
} catch (SearchException e) {
LOG.error("Error occurred during search", e);
}
DescriptorListFilter descriptorListFilter = new DescriptorListFilter()
.crop(Collections.singleton(crop.getShortName()));
List<DescriptorList> recentDescriptorLists = null;
try {
recentDescriptorLists = descriptorListService.list(descriptorListFilter, pageRequest).getContent();
} catch (SearchException e) {
LOG.error("Error occurred during search", e);
}
DiversityTreeFilter treeFilter = new DiversityTreeFilter()
.crop(crop.getShortName());
List<DiversityTree> diversityTrees = null;
try {
diversityTrees = treeService.list(treeFilter, pageRequest).getContent();
} catch (SearchException e) {
LOG.error("Error occurred during search", e);
}
return new CropDetails.Builder(loadTranslated(crop.getId()))
.blurb(article)
.accessionCount(accessionCount)
.imageGallery(imageGallery)
.folder(folder)
.overview(overview)
.recentDatasets(recentDatasets)
.recentDescriptorLists(recentDescriptorLists)
.recentSubsets(recentSubsets)
.diversityTrees(diversityTrees)
.build();
}
@PreAuthorize("hasRole('ADMINISTRATOR')")
@Override
@Transactional
@CacheEvict(allEntries = true, value = CACHE_CROPS)
public Crop remove(Crop crop) {
super.remove(crop);
crop.setId(null);
return crop;
}
/** APIv1 compatible updateBlurb */
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional(readOnly = false)
public Article updateBlurb(Crop crop, String title, String summary, String textBody, Locale locale) throws CRMException {
//isTemplate = false because it's crop
return apIv1Mapper.map(contentServiceV1.updateArticle(crop, ContentService.ENTITY_BLURB_SLUG, title, summary, textBody, locale));
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER')")
@Transactional(readOnly = false)
@CacheEvict(value = CACHE_CROPS, allEntries = true)
public Crop updateAliases(Crop crop, List<String> otherNames) {
crop.setOtherNames(otherNames.stream().distinct().map(otherName -> StringUtils.trim(otherName)).filter(otherName -> StringUtils.isNotBlank(otherName)).sorted().collect(Collectors.toList()));
return cropRepository.save(crop);
}
@Override
@Cacheable(value = CACHE_CROPS, key = "'listCrops'")
public List<Crop> listCrops() {
List<Crop> crops = cropRepository.findAll();
// Fetch otherNames list
crops.stream().forEach(c -> {
if (c.getOtherNames() != null)
c.getOtherNames().size();
});
return crops;
}
@Override
public List<CropDetails> listDetails(Locale locale) {
StopWatch stopWatch = StopWatch.createStarted();
List<TranslatedCrop> crops = translationSupport.getTranslated(cropRepository.findAll(), locale);
LOG.trace("all crops after {}ms", stopWatch.getTime());
// Fetch details
List<Path> paths = crops.stream().map(c -> Paths.get("/crop", c.entity.getShortName(), "covers")).collect(Collectors.toList());
List<ImageGallery> galleries = new ArrayList<>(crops.size());
try {
galleries.addAll(imageGalleryService.loadImageGalleries(paths));
} catch (InvalidRepositoryPathException ex) {
}
List<CropDetails> details = crops.stream().map(crop -> {
if (crop.entity.getOtherNames() != null)
crop.entity.getOtherNames().size();
ImageGallery imageGallery = galleries.stream()
.filter(ig -> ig.getPath().equalsIgnoreCase("/crop/" + crop.entity.getShortName() + "/covers"))
.findFirst()
.orElse(null);
return new CropDetails.Builder(crop).imageGallery(imageGallery).build();
}).collect(Collectors.toList());
LOG.trace("all image galleries after {}ms", stopWatch.getTime());
return details;
}
@Override
@Cacheable(value = CACHE_CROPS, key = "'findAll-'.concat(#locale)")
public List<Crop> list(final Locale locale) {
final List<Crop> crops = cropRepository.findAll();
// Fetch otherNames list
crops.stream().forEach(c -> {
if (c.getOtherNames() != null)
c.getOtherNames().size();
});
return crops;
}
@Override
public List<Crop> getCrops(List<String> cropNames) {
return cropRepository.findByShortNameIn(cropNames);
}
/**
* Register a crop with Genesys
*/
@Override
@PreAuthorize("hasRole('ADMINISTRATOR')")
@Transactional(readOnly = false)
@CacheEvict(allEntries = true, value = CACHE_CROPS)
public Crop addCrop(String shortName, String name, String description) {
LOG.info("Adding crop {}", shortName);
Crop crop = new Crop();
crop.setShortName(shortName);
crop.setName(name);
crop.setDescription(StringUtils.defaultIfBlank(htmlSanitizer.sanitize(description), null));
crop.setOriginalLanguageTag("en");
crop = cropRepository.save(crop);
LOG.info("Registered crop: {}", crop);
return crop;
}
/**
* Update a crop record in Genesys
*/
@Override
@PreAuthorize("hasRole('ADMINISTRATOR')")
@Transactional(readOnly = false)
@CacheEvict(allEntries = true, value = CACHE_CROPS)
public Crop updateCrop(Crop crop, String name, String description) {
LOG.info("Updating crop {}", crop);
if (crop != null) {
name = StringUtils.defaultIfBlank(name, null);
if (name != null) {
crop.setName(name);
}
description = StringUtils.defaultIfBlank(htmlSanitizer.sanitize(description), null);
if (description != null) {
crop.setDescription(description);
}
cropRepository.save(crop);
LOG.info("Updated crop: {}", crop);
}
return crop;
}
@Override
public void unlinkAccessionsForCrop(Crop crop) {
final BooleanBuilder predicate = new BooleanBuilder();
// accessions with selected crop
predicate.and(QAccession.accession.crop().eq(crop));
final BooleanBuilder cropNames = new BooleanBuilder();
cropNames.or(QAccession.accession.cropName.equalsIgnoreCase(crop.getShortName()));
cropNames.or(QAccession.accession.cropName.likeIgnoreCase(crop.getShortName()));
crop.getOtherNames().forEach(otherName -> {
cropNames.or(QAccession.accession.cropName.equalsIgnoreCase(otherName));
cropNames.or(QAccession.accession.cropName.likeIgnoreCase(otherName));
});
// accession#cropName not one of valid names
predicate.andNot(cropNames);
accessionProcessor.apply(predicate, (accessions) -> {
accessions.forEach(accession -> {
Crop newCrop = getCrop(accession.getCropName());
LOG.trace("{} from {}: {} -> {}", accession.getUuid(), accession.getInstCode(), accession.getCropName(), newCrop == null ? "null" : newCrop.getShortName());
if (!Objects.equals(accession.getCrop(), newCrop)) {
// assign crop, bypass auditing
LOG.debug("Unlinking crop for {} from {}: {} = {}", accession.getUuid(), accession.getInstCode(), accession.getCropName(), newCrop == null ? "null" : newCrop.getShortName());
accessionRepository.updateCrop(accession, newCrop);
}
});
});
}
@Override
public void linkAccessionsWithCrop(Crop crop) {
final BooleanBuilder predicate = new BooleanBuilder();
// accepted crop names
predicate.or(QAccession.accession.cropName.equalsIgnoreCase(crop.getShortName()));
predicate.or(QAccession.accession.cropName.likeIgnoreCase(crop.getShortName()));
crop.getOtherNames().forEach(otherName -> {
predicate.or(QAccession.accession.cropName.equalsIgnoreCase(otherName));
predicate.or(QAccession.accession.cropName.likeIgnoreCase(otherName));
});
// // but not assigned to this crop
// predicate.andAnyOf(QAccession.accession.crop.isNull(), QAccession.accession.crop.ne(crop));
// Only if it does not have a Crop, because maybe it was assigned manually with `cropCode` upsert!
predicate.and(QAccession.accession.crop().isNull());
accessionProcessor.apply(predicate, (accessions) -> {
accessions.forEach(accession -> {
Crop newCrop = getCrop(accession.getCropName());
LOG.trace("{} from {}: {} -> {}", accession.getUuid(), accession.getInstCode(), accession.getCropName(), newCrop == null ? "null" : newCrop.getShortName());
if (!Objects.equals(newCrop, accession.getCrop())) {
// assign crop, bypass auditing
LOG.debug("Updating crop for {} from {}: {} = {}", accession.getUuid(), accession.getInstCode(), accession.getCropName(), newCrop == null ? "null" : newCrop.getShortName());
accessionRepository.updateCrop(accession, newCrop);
}
});
});
}
private Map<String, ElasticsearchService.TermResult> getOverviewData(AccessionFilter byCropFilter) {
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, byCropFilter, 10, terms);
} catch (SearchException e) {
LOG.error("Error occurred during search", e);
return null;
}
}
private void copyValues(Crop target, Crop source) {
target.setDescription(source.getDescription());
target.setName(source.getName());
if (source.getOtherNames() != null) {
target.setOtherNames(source.getOtherNames().stream().distinct().map(otherName -> StringUtils.trim(otherName)).filter(otherName -> StringUtils.isNotBlank(otherName)).sorted().collect(Collectors.toList()));
}
if (source.getOriginalLanguageTag() != null) {
target.setOriginalLanguageTag(source.getOriginalLanguageTag());
}
}
@Transactional
public Crop update(final Crop crop) {
final Crop toUpdate = getCrop(crop.getShortName());
return update(crop, toUpdate);
}
@Override
@Transactional
public Crop update(Crop updated, Crop target) {
return updateFast(updated, target);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR')")
@Transactional
@CacheEvict(allEntries = true, value = CACHE_CROPS)
public Crop updateFast(Crop updated, Crop target) {
copyValues(target, updated);
return cropRepository.save(target);
}
@Transactional
public Crop create(final Crop crop) {
return createFast(crop);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR')")
@Transactional
@CacheEvict(allEntries = true, value = CACHE_CROPS)
public Crop createFast(Crop source) {
return super.createFast(source);
}
@Override
public CropLang machineTranslate(Crop original, String targetLanguage) throws TranslatorService.TranslatorException {
var mt = new CropLang();
mt.setMachineTranslated(true);
mt.setLanguageTag(targetLanguage);
mt.setEntity(original);
if (translatorService == null) return mt;
var builder = TranslatorService.TranslationStructuredRequest.builder()
.targetLang(targetLanguage);
if (! Objects.equals(Locale.ENGLISH.getLanguage(), targetLanguage) && ! Objects.equals(Locale.ENGLISH.getLanguage(), original.getOriginalLanguageTag())) {
// Translations to other languages use the English version (either original or translated)
var enTranslation = translationSupport.getLang(original, Locale.ENGLISH.getLanguage());
if (enTranslation == null) {
throw new InvalidApiUsageException("English text is not available.");
}
builder
.sourceLang(enTranslation.getLanguageTag())
// .context(enTranslation.getDescription())
.texts(Map.of(
"name", new FormattedText(TextFormat.markdown, enTranslation.getTitle()),
"description", new FormattedText(TextFormat.markdown, enTranslation.getDescription())
));
} else {
// Translations to English use the original texts
builder
.sourceLang(original.getOriginalLanguageTag())
// .context(original.getDescription())
.texts(Map.of(
"name", new FormattedText(TextFormat.text, original.getName()),
"description", new FormattedText(TextFormat.html, original.getDescription())
));
}
builder.texts(Map.of(
"name", new FormattedText(TextFormat.text, original.getName()),
"description", new FormattedText(TextFormat.html, original.getDescription())
));
var translations = translatorService.translate(builder.build());
if (StringUtils.isNotBlank(original.getName())) {
mt.setTitle(translations.getTexts().get("name"));
}
if (StringUtils.isNotBlank(original.getDescription())) {
mt.setDescription(translations.getTexts().get("description"));
}
return mt;
}
}