AccessionFilter.java

/*
 * Copyright 2019 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.filter;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.validation.constraints.Pattern;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.model.filters.NumberFilter;
import org.genesys.blocks.model.filters.StringFilter;
import org.genesys.blocks.model.filters.UuidModelFilter;
import org.genesys.blocks.util.CurrentApplicationContext;
import org.genesys.blocks.util.FilterUtils;
import org.genesys.server.model.dataset.QDataset;
import org.genesys.server.model.genesys.Accession;
import org.genesys.server.model.genesys.AccessionData;
import org.genesys.server.model.genesys.QAccession;
import org.genesys.server.model.genesys.QAccessionId;
import org.genesys.server.model.genesys.QTaxonomy2;
import org.genesys.server.model.impl.DiversityTreeAccessionRef;
import org.genesys.server.model.impl.FaoInstitute;
import org.genesys.server.model.impl.QDiversityTree;
import org.genesys.server.model.impl.QSubset;
import org.genesys.server.service.DiversityTreeService;
import org.genesys.server.service.PGRFANetworkService;
import org.genesys.server.service.PartnerService;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.UUIDDeserializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.JPQLQuery;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;

/**
 * Filters for {@link Accession}.
 */
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@Accessors(fluent = true)
public class AccessionFilter extends UuidModelFilter<AccessionFilter, Accession> implements IFullTextFilter {

	private static final long serialVersionUID = -1441103961567816877L;

	public static final String[] ES_BOOSTED_FIELDS = { "accessionNumber", "accessionName", "aliases.name", "coll.collNumb" };

	private static final Map<String, String> REMAPPED_PROPERTIES = Map.of("taxonomy.grinTaxonName", "taxonomy.grinTaxonomySpecies", "taxonomy.grinCurrentName", "taxonomy.currentTaxonomySpecies");

	/** Any text. */
	public String _text;

	/** The historic. */
	public Boolean historic = false;

	/** The aegis. */
	public Boolean aegis;

	/** The mls status. */
	public Boolean mlsStatus;

	/** The available. */
	public Boolean available;

	/** The crop. */
	public Set<String> crop;

	/** The crop name. */
	public StringFilter cropName;

	/** The institute. */
	public InstituteFilter institute;

	/** The acce numb. */
	public StringFilter accessionNumber;

	/** The accession numbers. */
	public Set<String> accessionNumbers;

	/** The doi. */
	public Set<String> doi;

	/** The seq no. */
	public NumberFilter<Double> seqNo;

	/** The taxonomy. */
	public TaxonomyFilter taxonomy;

	/** The samp stat. */
	public Set<Integer> sampStat;

	/** The origin. */
	public CountryFilter countryOfOrigin;

	/** The geo. */
	public AccessionGeoFilter geo;

	/** The coll. */
	public AccessionCollectFilter coll;

	/** The storage. */
	public Set<Integer> storage;

	/** The sgsv. */
	public Boolean sgsv;

	/** The art 15. */
	public Boolean inTrust;

	/** The pdci. */
	public NumberFilter<Double> pdci;

	/** acce.lists uuid */
	public Set<UUID> lists;

	/** accession aliases */
	public StringFilter aliases;

	/** accessions with images */
	public Boolean images;

	/** has DOI assigned */
	public Boolean hasDoi;

	/** is in a Dataset */
	public Boolean hasDataset;

	/** is in a Subset */
	public Boolean hasSubset;

	/** The duplSite. */
	public Set<@Pattern(regexp = "[A-Z]{3}\\d{3,4}") String> duplSite;

	/** The donorCode. */
	public Set<@Pattern(regexp = "[A-Z]{3}\\d{3,4}") String> donorCode;

	/** The breederCode. */
	public Set<@Pattern(regexp = "[A-Z]{3}\\d{3,4}") String> breederCode;

	/** Subset. */
	@JsonDeserialize(contentUsing = UUIDDeserializer.class)
	public Set<UUID> subsets;

	/** Dataset. */
	@JsonDeserialize(contentUsing = UUIDDeserializer.class)
	public Set<UUID> datasets;

	/** DiversityTree. */
	@JsonDeserialize(contentUsing = UUIDDeserializer.class)
	public Set<UUID> diversityTrees;

	/** The nodeKey. See {@link DiversityTreeAccessionRef#nodeKey} */
	public String nodeKey;

	/** The networks. */
	public Set<String> networks;

	/** The partner. */
	public Set<UUID> partner;

	/** The curation type. */
	public Set<AccessionData.CurationType> curationType;

	public StringFilter acquisitionDate;

	public AccessionFilter() {
		// Use defaults
	}

	public AccessionFilter(Boolean historical) {
		this.historic = historical;
	}

	/*
	 * (non-Javadoc)
	 * @see org.genesys.blocks.model.filters.BasicModelFilter#buildQuery()
	 */
	public List<Predicate> collectPredicates() {
		return collectPredicates(QAccession.accession);
	}

	public <T> JPQLQuery<T> buildJpaQuery(JPQLQuery<T> query, QAccession accession) {
		QAccessionId qAccessionId = accession.accessionId();
		query.innerJoin(qAccessionId);

		final List<Predicate> predicates = super.collectPredicates(accession, accession._super._super);

		if (!FilterUtils.isEmpty(taxonomy)) {
			QTaxonomy2 taxonomyJoin = new QTaxonomy2("T");
			query.innerJoin(accession.taxonomy(), taxonomyJoin);

			// Prevent cross-joins
			query.leftJoin(taxonomyJoin.grinTaxonomySpecies());
			query.leftJoin(taxonomyJoin.currentTaxonomySpecies());

			predicates.addAll(taxonomy.collectPredicates(taxonomyJoin));
		}
		if (!FilterUtils.isEmpty(crop)) {
			query.leftJoin(accession.crop());
			predicates.add(accession.crop().shortName.in(crop));
		}
		if (!FilterUtils.isEmpty(institute)) {
			query.innerJoin(accession.institute());
			predicates.addAll(institute.collectPredicates(accession.institute()));
//			predicates.add(accession.institute().id.in(fetchInstituteIds(institute)));
		}
		if (!FilterUtils.isEmpty(countryOfOrigin)) {
			query.leftJoin(accession.countryOfOrigin());
			predicates.addAll(countryOfOrigin.collectPredicates(accession.countryOfOrigin()));
		}
		if (!FilterUtils.isEmpty(geo)) {
//			query.leftJoin(qAccessionId.geo);
			if (!FilterUtils.isEmpty(geo.climate())) {
				query.leftJoin(qAccessionId.climate());
			}
			predicates.addAll(geo.collectPredicates(qAccessionId));
		}
		if (!FilterUtils.isEmpty(coll)) {
			query.leftJoin(qAccessionId.coll());
			predicates.addAll(coll.collectPredicates(qAccessionId.coll()));
		}
		if (!FilterUtils.isEmpty(pdci)) {
			query.leftJoin(qAccessionId.pdci());
			predicates.add(pdci.buildQuery(qAccessionId.pdci().score));
		}
		if (CollectionUtils.isNotEmpty(subsets)) {
			QSubset qSubset = new QSubset("subsets");
			query.leftJoin(accession.subsets, qSubset);
			predicates.add(qSubset.uuid.in(subsets));
		}
		if (CollectionUtils.isNotEmpty(datasets)) {
			QDataset qDataset = new QDataset("datasets");
			query.leftJoin(accession.datasets, qDataset);
			predicates.add(qDataset.uuid.in(datasets));
		}
		if (CollectionUtils.isNotEmpty(diversityTrees)) {
			QDiversityTree qDiversityTree = new QDiversityTree("diversityTrees");
			query.innerJoin(accession.diversityTrees, qDiversityTree);
			BooleanExpression predicate = qDiversityTree.uuid.in(diversityTrees);

			if (StringUtils.isNotBlank(nodeKey)) {
				Set<Long> treeAccessions = getDivTreeAccessionIds(diversityTrees, nodeKey);
				predicate = qAccessionId.id.in(treeAccessions);
			}
			predicates.add(predicate);
		}

		// Fields
		if (historic != null) {
			predicates.add(accession.historic.eq(historic));
		}
		if (aegis != null) {
			predicates.add(accession.aegis.eq(aegis));
		}
		if (cropName != null) {
			predicates.add(cropName.buildQuery(accession.cropName));
		}
		if (CollectionUtils.isNotEmpty(doi)) {
			predicates.add(accession.doi.in(doi));
		}
		if (CollectionUtils.isNotEmpty(accessionNumbers)) {
			predicates.add(accession.accessionNumber.in(accessionNumbers));
		}
		if (accessionNumber != null) {
			predicates.add(accessionNumber.buildQuery(accession.accessionNumber));
		}
		if (seqNo != null) {
			predicates.add(seqNo.buildQuery(accession.seqNo));
		}
		if (CollectionUtils.isNotEmpty(sampStat)) {
			predicates.add(accession.sampStat.in(sampStat));
		}
		if (CollectionUtils.isNotEmpty(storage)) {
			predicates.add(qAccessionId.storage.any().in(storage));
		}
		if (mlsStatus != null) {
			predicates.add(accession.mlsStatus.eq(mlsStatus));
		}
		if (available != null) {
			predicates.add(accession.available.eq(available));
		}
		if (inTrust != null) {
			predicates.add(accession.inTrust.eq(inTrust));
		}
		if (CollectionUtils.isNotEmpty(lists)) {
			predicates.add(qAccessionId.lists.any().uuid.in(lists));
		}
		if (CollectionUtils.isNotEmpty(uuid)) {
			predicates.add(qAccessionId.uuid.in(uuid));
		}
		if (aliases != null) {
			predicates.add(aliases.buildQuery(qAccessionId.aliases.any().name));
		}
		if (CollectionUtils.isNotEmpty(duplSite)) {
			predicates.add(qAccessionId.duplSite.any().in(duplSite));
		}
		if (sgsv != null) {
			predicates.add(accession.inSvalbard.eq(sgsv));
		}
		if (CollectionUtils.isNotEmpty(curationType)) {
			predicates.add(accession.curationType.in(curationType));
		}
		if (CollectionUtils.isNotEmpty(donorCode)) {
			predicates.add(accession.donorCode.in(donorCode));
		}
		if (CollectionUtils.isNotEmpty(breederCode)) {
			predicates.add(qAccessionId.breederCode.any().in(breederCode));
		}
		if (acquisitionDate != null) {
			predicates.add(acquisitionDate.buildQuery(accession.acquisitionDate));
		}
		if (images != null) {
			if (images) {
				predicates.add(qAccessionId.imageCount.gt(0));
			} else {
				predicates.add(qAccessionId.imageCount.eq(0));
			}
		}
		if (CollectionUtils.isNotEmpty(networks)) {
			Set<Long> networkMembers = getNetworkMemberIds(networks);
			predicates.add(accession.institute().id.in(networkMembers));
		}
		if (CollectionUtils.isNotEmpty(partner)) {
			Set<Long> partnerInstitutes = getPartnerInstituteIds(partner);
			predicates.add(accession.institute().id.in(partnerInstitutes));
		}

		if (hasDoi != null) {
			if (hasDoi) {
				predicates.add(accession.doi.isNotNull());
			} else {
				predicates.add(accession.doi.isNull());
			}
		}
		if (hasSubset != null) {
			if (hasSubset) {
				predicates.add(qAccessionId.subsetCount.gt(0));
			} else {
				predicates.add(qAccessionId.subsetCount.eq(0l));
			}
		}
		if (hasDataset != null) {
			if (hasDataset) {
				predicates.add(qAccessionId.datasetCount.gt(0));
			} else {
				predicates.add(qAccessionId.datasetCount.eq(0l));
			}
		}

		var builder = new BooleanBuilder(ExpressionUtils.allOf(predicates));

		if (NOT != null) {
			// This is not a regular NOT operation where not(A & B) = not(A) or not(B)
			// This is not(A, B) = not(A) and not(B)
			builder.and(ExpressionUtils.anyOf(NOT.collectPredicates()).not());
		}

		if (AND != null) {
			builder.and(AND.buildPredicate());
		}
		if (OR != null) {
			builder.or(OR.buildPredicate());
		}


		query.where(builder);
		return query;
	}

	/**
	 * Builds the query.
	 *
	 * @param accession the accession
	 * @return the predicate
	 */
	public List<Predicate> collectPredicates(QAccession accession) {
		final List<Predicate> predicates = super.collectPredicates(accession, accession._super._super);
		QAccessionId qAccessionId = accession.accessionId();

		if (taxonomy != null) {
			predicates.addAll(taxonomy.collectPredicates(accession.taxonomy()));
		}
		if (crop != null && !crop.isEmpty()) {
			predicates.add(accession.crop().isNotNull().and(accession.crop().shortName.in(crop)));
		}
		if (institute != null) {
			predicates.addAll(institute.collectPredicates(accession.institute()));
//			predicates.add(accession.institute().id.in(fetchInstituteIds(institute)));
		}
		if (countryOfOrigin != null) {
			predicates.addAll(countryOfOrigin.collectPredicates(accession.countryOfOrigin()));
		}
		if (geo != null) {
			predicates.addAll(geo.collectPredicates(qAccessionId));
		}
		if (coll != null) {
			predicates.addAll(coll.collectPredicates(qAccessionId.coll()));
		}
		if (pdci != null) {
			predicates.add(pdci.buildQuery(qAccessionId.pdci().score));
		}
		if (subsets != null) {
			predicates.add(accession.subsets.isNotEmpty().and(accession.subsets.any().uuid.in(subsets)));
		}
		if (datasets != null) {
			predicates.add(accession.datasets.isNotEmpty().and(accession.datasets.any().uuid.in(datasets)));
		}
		if (diversityTrees != null) {
			BooleanExpression predicate = accession.diversityTrees.isNotEmpty().and(accession.diversityTrees.any().uuid.in(diversityTrees));

			if (StringUtils.isNotBlank(nodeKey)) {
				Set<Long> treeAccessions = getDivTreeAccessionIds(diversityTrees, nodeKey);
				predicate = qAccessionId.id.in(treeAccessions);
			}
			predicates.add(predicate);
		}

		// Fields
		if (historic != null) {
			predicates.add(accession.historic.eq(historic));
		}
		if (aegis != null) {
			predicates.add(accession.aegis.eq(aegis));
		}
		if (cropName != null) {
			predicates.add(cropName.buildQuery(accession.cropName));
		}
		if (CollectionUtils.isNotEmpty(doi)) {
			predicates.add(accession.doi.isNotNull().and(accession.doi.in(doi)));
		}
		if (CollectionUtils.isNotEmpty(accessionNumbers)) {
			predicates.add(accession.accessionNumber.in(accessionNumbers));
		}
		if (accessionNumber != null) {
			predicates.add(accessionNumber.buildQuery(accession.accessionNumber));
		}
		if (seqNo != null) {
			predicates.add(seqNo.buildQuery(accession.seqNo));
		}
		if (CollectionUtils.isNotEmpty(sampStat)) {
			predicates.add(accession.sampStat.isNotNull().and(accession.sampStat.in(sampStat)));
		}
		if (CollectionUtils.isNotEmpty(storage)) {
			predicates.add(qAccessionId.storage.size().gt(0).and(qAccessionId.storage.any().in(storage)));
		}
		if (mlsStatus != null) {
			predicates.add(accession.mlsStatus.eq(mlsStatus));
		}
		if (available != null) {
			predicates.add(accession.available.eq(available));
		}
		if (inTrust != null) {
			predicates.add(accession.inTrust.eq(inTrust));
		}
		if (CollectionUtils.isNotEmpty(lists)) {
			predicates.add(qAccessionId.lists.isNotEmpty().and(qAccessionId.lists.any().uuid.in(lists)));
		}
		if (CollectionUtils.isNotEmpty(uuid)) {
			// predicates.add(accession.accessionId.uuid.isNotNull()); // Not null
			predicates.add(qAccessionId.uuid.in(uuid));
		}
		if (aliases != null) {
			predicates.add(qAccessionId.aliases.isNotEmpty().and(aliases.buildQuery(qAccessionId.aliases.any().name)));
		}
		if (CollectionUtils.isNotEmpty(duplSite)) {
			predicates.add(qAccessionId.duplSite.any().in(duplSite));
		}
		if (sgsv != null) {
			predicates.add(accession.inSvalbard.eq(sgsv));
		}
		if (CollectionUtils.isNotEmpty(curationType)) {
			predicates.add(accession.curationType.in(curationType));
		}
		if (CollectionUtils.isNotEmpty(donorCode)) {
			predicates.add(accession.donorCode.in(donorCode));
		}
		if (CollectionUtils.isNotEmpty(breederCode)) {
			predicates.add(qAccessionId.breederCode.any().in(breederCode));
		}
		if (acquisitionDate != null) {
			predicates.add(acquisitionDate.buildQuery(accession.acquisitionDate));
		}
		if (images != null) {
			if (images) {
				predicates.add(qAccessionId.imageCount.gt(0));
			} else {
				predicates.add(qAccessionId.imageCount.eq(0));
			}
		}
		if (CollectionUtils.isNotEmpty(networks)) {
			Set<Long> networkMembers = getNetworkMemberIds(networks);
			predicates.add(accession.institute().id.in(networkMembers));
		}
		if (CollectionUtils.isNotEmpty(partner)) {
			Set<Long> partnerInstitutes = getPartnerInstituteIds(partner);
			predicates.add(accession.institute().id.in(partnerInstitutes));
		}
		if (hasDoi != null) {
			if (hasDoi) {
				predicates.add(accession.doi.isNotNull());
			} else {
				predicates.add(accession.doi.isNull());
			}
		}
		if (hasSubset != null) {
			if (hasSubset) {
				predicates.add(qAccessionId.subsetCount.gt(0));
			} else {
				predicates.add(qAccessionId.subsetCount.eq(0l));
			}
		}
		if (hasDataset != null) {
			if (hasDataset) {
				predicates.add(qAccessionId.datasetCount.gt(0));
			} else {
				predicates.add(qAccessionId.datasetCount.eq(0l));
			}
		}

		if (lastModifiedDate != null) {
			predicates.add(lastModifiedDate.buildQuery(accession.lastModifiedDate));
		}

		return predicates;
	}

//	private Collection<Long> fetchInstituteIds(InstituteFilter filter) {
//		try {
//			InstituteService service = CurrentApplicationContext.getContext().getBean(InstituteService.class);
//			return service.list(filter, Pageable.unpaged()).map(FaoInstitute::getId).getContent();
//		} catch (Throwable e) { 
//			throw new RuntimeException("Could not fetch institute ids", e);
//		}
//	}

	private Set<Long> getNetworkMemberIds(Set<String> slugs) {
		PGRFANetworkService networkService = CurrentApplicationContext.getContext().getBean(PGRFANetworkService.class);
		return networkService.getMembers(slugs).stream().map(FaoInstitute::getId).collect(Collectors.toSet());
	}

	private Set<Long> getPartnerInstituteIds(Set<UUID> uuids) {
		PartnerService partnerService = CurrentApplicationContext.getContext().getBean(PartnerService.class);
		return partnerService.loadInstitutes(uuids).stream().map(FaoInstitute::getId).collect(Collectors.toSet());
	}

	private Set<Long> getDivTreeAccessionIds(Set<UUID> treeUuids, String nodeKey) {
		DiversityTreeService treeService = CurrentApplicationContext.getContext().getBean(DiversityTreeService.class);
		return treeService.loadAccessionRefs(treeUuids, nodeKey).stream().filter(ref -> ref.getAccession() != null).map(ref -> ref.getAccession().getAccessionId().getId()).collect(Collectors.toSet());
	}

	/**
	 * Holder.
	 *
	 * @return the institute filter
	 */
	public synchronized InstituteFilter holder() {
		if (this.institute == null) {
			return this.institute = new InstituteFilter();
		} else {
			return this.institute;
		}
	}

	/**
	 * Origin.
	 *
	 * @return the country filter
	 */
	public synchronized CountryFilter origin() {
		if (this.countryOfOrigin == null) {
			return this.countryOfOrigin = new CountryFilter();
		}
		return this.countryOfOrigin;
	}

	/**
	 * Geo.
	 *
	 * @return the accession geo filter
	 */
	public synchronized AccessionGeoFilter geo() {
		return this.geo == null ? this.geo = new AccessionGeoFilter() : this.geo;
	}

	/**
	 * Coll.
	 *
	 * @return the accession collect filter
	 */
	public synchronized AccessionCollectFilter coll() {
		return this.coll == null ? this.coll = new AccessionCollectFilter() : this.coll;
	}

	/**
	 * Taxa.
	 *
	 * @return the taxonomy filter
	 */
	public synchronized TaxonomyFilter taxa() {
		return this.taxonomy == null ? this.taxonomy = new TaxonomyFilter() : this.taxonomy;
	}

	/**
	 * Accession Number filter.
	 *
	 * @return the string filter
	 */
	public synchronized StringFilter accessionNumber() {
		return this.accessionNumber == null ? this.accessionNumber = new StringFilter() : this.accessionNumber;
	}

	/**
	 * Accession Number filter.
	 *
	 * @return the string filter
	 */
	public synchronized Set<String> accessionNumbers() {
		return this.accessionNumbers == null ? this.accessionNumbers = new HashSet<>() : this.accessionNumbers;
	}

	public synchronized Set<String> doi() {
		if (doi == null) {
			doi = new HashSet<>();
		}
		return doi;
	}

	public String get_text() {
		return _text;
	}

	/**
	 * Institute filter.
	 *
	 * @return the institute filter
	 */
	public synchronized InstituteFilter institute() {
		return this.institute == null ? this.institute = new InstituteFilter() : this.institute;
	}

	@Override
	public String[] boostedFields() {
		return ES_BOOSTED_FIELDS;
	}

	@Override
	public Map<String, String> remappedProperties() {
		return REMAPPED_PROPERTIES;
	}

	private static final ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());

	public static AccessionFilter fromJson(String json) throws JsonProcessingException {
		if (StringUtils.isBlank(json)) {
			return new AccessionFilter();
		}
		return mapper.readValue(json, AccessionFilter.class);
	}
}