TaxonomyServiceImpl.java

/*
 * Copyright 2020 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.math.BigInteger;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.apache.commons.lang3.StringUtils;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.exception.SearchException;
import org.genesys.server.model.genesys.QTaxonomy2;
import org.genesys.server.model.genesys.Taxonomy2;
import org.genesys.server.model.grin.TaxonomySpecies;
import org.genesys.server.persistence.AccessionRepository;
import org.genesys.server.persistence.Taxonomy2Repository;
import org.genesys.server.persistence.grin.TaxonomyGenusRepository;
import org.genesys.server.persistence.grin.TaxonomySpeciesRepository;
import org.genesys.server.service.ElasticsearchService;
import org.genesys.server.service.TaxonomyService;
import org.genesys.server.service.filter.TaxonomyExtraFilter;
import org.genesys.server.service.filter.TaxonomyFilter;
import org.genesys.server.service.filter.TaxonomySpeciesFilter;
import org.genesys.spring.TransactionHelper;
import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.google.common.collect.Lists;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;

import lombok.extern.slf4j.Slf4j;

@Service
@Transactional(readOnly = true)
@Slf4j
public class TaxonomyServiceImpl implements TaxonomyService {

	@Autowired
	private Taxonomy2Repository taxonomy2Repository;

	@Autowired
	private TaxonomySpeciesRepository grinSpeciesRepository;

	@Autowired
	private TaxonomyGenusRepository grinGenusRepository;

	@Autowired
	private AccessionRepository accessionRepository;
	
	@Autowired
	private JPAQueryFactory jpaQueryFactory;

	@Autowired(required = false)
	private ElasticsearchService elasticsearchService;

	@PersistenceContext
	private EntityManager entityManager;

	@Override
	public Taxonomy2 get(Long id) {
		return taxonomy2Repository.findById(id).orElse(null);
	}

	@Override
	public List<String> autocompleteGenus(String term) {
		List<String> strings;

		strings = taxonomy2Repository.autocompleteGenus(term + "%", PageRequest.of(0, 200));

		return strings;
	}

	@Override
	public List<String> autocompleteSpecies(String term, List<String> genus) {
		List<String> strings;

		if (!genus.isEmpty()) {
			strings = taxonomy2Repository.autocompleteSpeciesByGenus("%" + term + "%", genus, PageRequest.of(0, 10));
		} else {
			strings = taxonomy2Repository.autocompleteSpecies("%" + term + "%", PageRequest.of(0, 10));
		}

		return strings;
	}

	@Override
	public List<String> autocompleteSubtaxa(String term, List<String> genus, List<String> species) {
		List<String> strings;

		if (!genus.isEmpty() && !species.isEmpty()) {
			log.debug("Genus={} sp={}", genus, species);
			strings = taxonomy2Repository.autocompleteSubtaxaByGenusAndSpecies("%" + term + "%", genus, species, PageRequest.of(0, 10));
		} else if (!species.isEmpty()) {
			log.debug("sp={}", species);
			strings = taxonomy2Repository.autocompleteSubtaxaBySpecies("%" + term + "%", species, PageRequest.of(0, 10));
		} else if (!genus.isEmpty()) {
			log.debug("Genus={}", genus);
			strings = taxonomy2Repository.autocompleteSubtaxaByGenus("%" + term + "%", genus, PageRequest.of(0, 10));
		} else {
			strings = taxonomy2Repository.autocompleteSubtaxa("%" + term + "%", PageRequest.of(0, 10));
		}

		return strings;
	}

	@Override
	public List<String> autocompleteTaxonomy(String term) {
		return taxonomy2Repository.autocompleteTaxonomy("%" + term + "%", PageRequest.of(0, 10));
	}

	@Override
	@Cacheable(value = "hibernate.org.genesys.server.model.impl.Taxonomy2.fullname", key = "#genus + '-' + #species + '-' + #spAuthor + '-' + #subtaxa + '-' + #subtAuthor", unless = "#result == null")
	public Taxonomy2 find(String genus, String species, String spAuthor, String subtaxa, String subtAuthor) {
		return taxonomy2Repository.findByGenusAndSpeciesAndSpAuthorAndSubtaxaAndSubtAuthor(genus, species, spAuthor, subtaxa, subtAuthor);
	}

	@Override
	@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
	public Taxonomy2 internalEnsure(String genus, String species, String spAuthor, String subtaxa, String subtAuthor) throws InterruptedException {
		Long taxSpeciesId = null, taxGenusId = null;

		// Direct species
		if (subtaxa.equals("") && spAuthor.equals("") && subtAuthor.equals("")) {
			if (StringUtils.equals(species, "sp.")) {
				// Self
			} else {
				final Taxonomy2 genusTaxa = internalEnsure(genus, "sp.", "", "", "");
				taxGenusId = genusTaxa.getId();
			}
		} else {
			final Taxonomy2 speciesTaxa = internalEnsure(genus, species, "", "", "");
			taxSpeciesId = speciesTaxa.getId();
			taxGenusId = speciesTaxa.getTaxGenus();
		}

		Taxonomy2 taxonomy = null;
		try {
			taxonomy = find(genus, species, spAuthor, subtaxa, subtAuthor);
		} catch (final Throwable e) {
			log.info("Taxonomy not found: {}", e.getMessage());
		}

		if (taxonomy != null) {
			return taxonomy;
		} else {
			log.info("Adding new taxonomic name: {} {} {} {} {}", genus, species, spAuthor, subtaxa, subtAuthor);
			taxonomy = new Taxonomy2();
			taxonomy.setGenus(genus);
			taxonomy.setSpecies(species);
			taxonomy.setSpAuthor(spAuthor);
			taxonomy.setSubtaxa(subtaxa);
			taxonomy.setSubtAuthor(subtAuthor);
			taxonomy.setTaxGenus(taxGenusId);
			taxonomy.setTaxSpecies(taxSpeciesId);

			try {
				taxonomy = taxonomy2Repository.save(taxonomy);

				if (taxGenusId == null) {
					taxonomy.setTaxGenus(taxonomy.getId());
					taxonomy = taxonomy2Repository.save(taxonomy);
				}
				if (taxSpeciesId == null) {
					taxonomy.setTaxSpecies(taxonomy.getId());
					taxonomy = taxonomy2Repository.save(taxonomy);
				}

				return taxonomy;

			} catch (final Throwable e) {
				log.warn("Error {} :{}", e.getMessage(), taxonomy);
				throw new RuntimeException(e.getMessage());
			}
		}

	}

	@Override
	public long getTaxonomy2Id(String genus) {
		return find(genus, "sp.", "", "", "").getId();
	}

	@Override
	public long getTaxonomy2Id(String genus, String species) {
		final Taxonomy2 tax = find(genus, species, "", "", "");
		return tax.getId();
	}

	@Override
	public Taxonomy2 get(String genus, String species) {
		return find(genus, species, "", "", "");
	}

	@Override
	public long countTaxonomy2() {
		return taxonomy2Repository.count();
	}

	@Override
	public Taxonomy2 get(String genus) {
		return find(genus, "sp.", "", "", "");
	}

	@Override
	public List<Taxonomy2> list(TaxonomyFilter filter) {
		final BooleanBuilder predicate = new BooleanBuilder();
		if (filter != null) {
			predicate.and(filter.buildPredicate());
		}
		return Lists.newArrayList(taxonomy2Repository.findAll(predicate, QTaxonomy2.taxonomy2.genus.asc(), QTaxonomy2.taxonomy2.species.asc(), QTaxonomy2.taxonomy2.id.asc()));
	}

	@Override
	public Page<Taxonomy2Info> list(TaxonomyExtraFilter filter, Pageable page) {
		final BooleanBuilder predicate = new BooleanBuilder();
		if (filter != null) {
			predicate.and(filter.buildPredicate());
		}

		JPAQuery<Taxonomy2> query = jpaQueryFactory.selectFrom(QTaxonomy2.taxonomy2);
		query.leftJoin(QTaxonomy2.taxonomy2.grinTaxonomySpecies());
		query.leftJoin(QTaxonomy2.taxonomy2.currentTaxonomySpecies());

		List<Taxonomy2> matches = query.where(predicate).offset(page.getOffset()).limit(page.getPageSize()).orderBy(
			QTaxonomy2.taxonomy2.accessions.size().desc(), QTaxonomy2.taxonomy2.taxonName.asc(), QTaxonomy2.taxonomy2.id.asc()).fetch();

		final Page<Taxonomy2> res = taxonomy2Repository.findAll(predicate, page);
		final List<Taxonomy2Info> content = matches.stream().map(taxonomy2 -> {
			Long accessionCount = accessionRepository.countByTaxonomy(taxonomy2);
			return Taxonomy2Info.from(taxonomy2, accessionCount);
		}).collect(Collectors.toList());

		return new PageImpl<>(content, page, res.getTotalElements());
	}

	@Override
	public Page<TaxonomySpecies> listSpecies(TaxonomySpeciesFilter filter, Pageable page) throws SearchException {
		final BooleanBuilder predicate = new BooleanBuilder();
		if (filter != null) {
			predicate.and(filter.buildPredicate());
			if (filter.isFulltextQuery()) {
				return elasticsearchService.findAll(TaxonomySpecies.class, filter, page);
			}
		}
		Page<TaxonomySpecies> res = grinSpeciesRepository.findAll(predicate, page);
		// Ensure current species is loaded
		res.getContent().forEach(grinSpecies -> Hibernate.initialize(grinSpecies.getCurrentTaxonomySpecies()));
		return new PageImpl<>(res.getContent(), page, res.getTotalElements());
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public Taxonomy2 setGrinSpecies(long taxonomy2Id, Long customGrinSpeciesId) {
		Taxonomy2 taxonomy2 = taxonomy2Repository.findById(taxonomy2Id).orElseThrow(() -> new NotFoundElement("No such Taxonomy2"));

		if (customGrinSpeciesId == null) {
			taxonomy2.setGrinTaxonomySpecies(null);
			taxonomy2.setCurrentTaxonomySpecies(null);
			taxonomy2.setOverrideTaxonomySpecies(null);
			taxonomy2.setFamily(null);
		} else {
			TaxonomySpecies grinSpecies = grinSpeciesRepository.findById(customGrinSpeciesId).orElseThrow(() -> new NotFoundElement("No such TaxonomySpecies"));
			taxonomy2.setOverrideTaxonomySpecies(grinSpecies);
			taxonomy2.setGrinTaxonomySpecies(grinSpecies);
			taxonomy2.setCurrentTaxonomySpecies(grinSpecies.getCurrentTaxonomySpecies());
			taxonomy2.setFamily(grinSpecies.getTaxonomyGenus().getTaxonomyFamily().getFamilyName());
		}
		return taxonomy2Repository.save(taxonomy2);
	}

	@Override
	public List<String> getAllGenera() {
		return taxonomy2Repository.getAllGenera();
	}

	@Override
	public List<Long> findByGenus(List<String> genus) {
		return taxonomy2Repository.findByGenus(genus);
	}

	@Override
	@Transactional
	public void cleanupTaxonomies() {
		Set<BigInteger> referencedIds = taxonomy2Repository.findTaxonomyReferencedIds();
		Set<Long> allIds = taxonomy2Repository.findTaxonomyIds();
		for (BigInteger integer : referencedIds) {
			allIds.remove(integer.longValue());
		}

		if (allIds.size() > 0) {
			taxonomy2Repository.removeUnusedIds(allIds);
		}
	}

	@Override
	@Transactional(timeout = 50, isolation = Isolation.READ_UNCOMMITTED, propagation = Propagation.REQUIRED)
	public Taxonomy2 ensureTaxonomy(Taxonomy2 example) {
		example.sanitize();
		Taxonomy2 existing = taxonomy2Repository.findByGenusAndSpeciesAndSpAuthorAndSubtaxaAndSubtAuthor(example.getGenus(), example.getSpecies(), example.getSpAuthor(), example
			.getSubtaxa(), example.getSubtAuthor());
		if (existing == null) {
			log.debug("Adding taxonomy {} {} {} {} {}", example.getGenus(), example.getSpecies(), example.getSpAuthor(), example.getSubtaxa(), example.getSubtAuthor());
			Taxonomy2 newTaxa = new Taxonomy2();
			newTaxa.setGenus(example.getGenus());
			newTaxa.setSpecies(example.getSpecies());
			newTaxa.setSpAuthor(example.getSpAuthor());
			newTaxa.setSubtaxa(example.getSubtaxa());
			newTaxa.setSubtAuthor(example.getSubtAuthor());
			newTaxa.sanitize();

			Taxonomy2 tsP = new Taxonomy2();
			tsP.setGenus(newTaxa.getGenus());
			tsP.setSpecies(newTaxa.getSpecies());
			tsP.sanitize();
			if (tsP.equalTo(newTaxa)) {
				// equal to species level
				tsP.setSpecies(null);
				tsP.sanitize();
				// get genus id
				if (!tsP.equalTo(newTaxa)) {
					tsP = ensureTaxonomy(tsP);
					newTaxa.setTaxGenus(tsP.getTaxGenus());
				} else {
					// incoming taxonomy is genus level only.
					// System.err.println("What now " + newTaxa + " == " + tsP);
				}
			} else {
				// not equal to species level, ensure taxSpecies
				tsP = ensureTaxonomy(tsP);
				newTaxa.setTaxGenus(tsP.getTaxGenus());
				newTaxa.setTaxSpecies(tsP.getId());
			}

			Taxonomy2 t = taxonomy2Repository.save(newTaxa.sanitize());
			// self-references
			t.setTaxGenus(t.getTaxGenus());
			t.setTaxSpecies(t.getTaxSpecies());
			return taxonomy2Repository.save(t);
		} else {
			return existing;
		}
	}

	@Override
	public List<Long> findTaxonomy2GenusId(List<String> genera) {
		return taxonomy2Repository.findTaxGenusId(genera);
	}

	@Override
	public List<Long> findTaxonomy2SpeciesId(List<String> speciesNames) {
		return taxonomy2Repository.findTaxSpeciesId(speciesNames);
	}
	
	@Override
	public List<Long> findGrinGenusId(List<String> genera) {
		return grinGenusRepository.findTaxonomyGenusId(genera);
	}

	@Override
	public List<Long> findGrinSpeciesId(List<String> speciesNames) {
		return grinSpeciesRepository.findTaxonomySpeciesId(speciesNames);
	}

	@Override
	public List<Long> findTaxonomy2ByGrinGenus(List<String> genus) {
		return taxonomy2Repository.findByGrinGenus(genus);
	}

	@Override
	public List<Long> findTaxonomy2ByCurrentGrinGenus(List<String> genus) {
		return taxonomy2Repository.findByCurrentGrinGenus(genus);
	}

	@Override
	public List<Long> findTaxonomy2ByGrinSpecies(List<String> genusSpecies) {
		return taxonomy2Repository.findByGrinGenusAndSpecies(genusSpecies);
	}

	@Override
	public List<Long> findTaxonomy2ByCurrentGrinSpecies(List<String> genusSpecies) {
		return taxonomy2Repository.findByCurrentGrinGenusAndSpecies(genusSpecies);
	}

	@Override
	public List<Long> findTaxonomy2ByGrinNames(List<String> names) {
		return taxonomy2Repository.findByGrinNames(names);
	}

	@Override
	public List<Long> findTaxonomy2ByCurrentGrinNames(List<String> names) {
		return taxonomy2Repository.findByCurrentGrinNames(names);
	}

	@Override
	public List<Long> listTaxonomy2ByGrinId(Collection<Long> grinId) {
		return taxonomy2Repository.findByGrinId(grinId);
	}

	@Override
	public List<Long> listTaxonomy2ByCurrentGrinId(Collection<Long> grinId) {
		return taxonomy2Repository.findByCurrentGrinId(grinId);
	}

	@Override
	@Transactional
	public void updateFamilyNames() {

	var sqlUpdateQuery = 
		"update taxonomy2 t2 set t2.lastModifiedDate = now(), t2.family = " +
			"(select distinct gf.family_name from grin_species gs " +
			"left join grin_genus gg on gg.taxonomy_genus_id = gs.taxonomy_genus_id " +
			"left join grin_family gf on gg.taxonomy_family_id = gf.taxonomy_family_id " +
			"where t2.grinTaxonomySpecies = gs.taxonomy_species_id) " +
		"where t2.grinTaxonomySpecies is not null;";
		var updated = entityManager.createNativeQuery(sqlUpdateQuery).executeUpdate();

		log.warn("Updated {} taxonomy records with GRIN family names", updated);
	}

}