AccessionServiceImpl.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.gringlobal.service.impl;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.api.exception.NotFoundElement;
import org.gringlobal.api.v1.MultiOp;
import org.gringlobal.application.config.GGCESecurityConfig;
import org.gringlobal.component.GGCE;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.Accession;
import org.gringlobal.model.AccessionAction;
import org.gringlobal.model.AccessionInvGroup;
import org.gringlobal.model.AccessionInvName;
import org.gringlobal.model.AccessionSource;
import org.gringlobal.model.AccessionSourceMap;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.InventoryMaintenancePolicy;
import org.gringlobal.model.Method;
import org.gringlobal.model.QAccession;
import org.gringlobal.model.QAccessionAction;
import org.gringlobal.model.QAccessionInvGroup;
import org.gringlobal.model.QAccessionInvName;
import org.gringlobal.model.QAccessionPedigree;
import org.gringlobal.model.QAccessionSource;
import org.gringlobal.model.QInventory;
import org.gringlobal.model.Site;
import org.gringlobal.model.community.AccessionMCPD;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.model.workflow.WorkflowActionStep;
import org.gringlobal.persistence.AccessionActionRepository;
import org.gringlobal.persistence.AccessionInvAnnotationRepository;
import org.gringlobal.persistence.AccessionInvAttachRepository;
import org.gringlobal.persistence.AccessionInvGroupRepository;
import org.gringlobal.persistence.AccessionInvNameRepository;
import org.gringlobal.persistence.AccessionRepository;
import org.gringlobal.persistence.AccessionSourceRepository;
import org.gringlobal.persistence.InventoryMaintenancePolicyRepository;
import org.gringlobal.persistence.InventoryRepository;
import org.gringlobal.persistence.MethodRepository;
import org.gringlobal.persistence.SiteRepository;
import org.gringlobal.service.AccessionActionService;
import org.gringlobal.service.AccessionActionService.AccessionActionRequest;
import org.gringlobal.service.AccessionActionService.AccessionActionScheduleFilter;
import org.gringlobal.service.ShortFilterService.FilterInfo;
import org.gringlobal.service.AccessionInvGroupService;
import org.gringlobal.service.AccessionService;
import org.gringlobal.service.AccessionSourceMapService;
import org.gringlobal.service.InventoryAttachmentService;
import org.gringlobal.service.InventoryService;
import org.gringlobal.service.MethodService;
import org.gringlobal.service.filter.AccessionActionFilter;
import org.gringlobal.service.filter.AccessionFilter;
import org.gringlobal.spring.TransactionHelper;
import org.gringlobal.util.CoordUtil;
import org.gringlobal.worker.AccessionMCPDConverter;
import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
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.AccessDeniedException;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.querydsl.core.types.ExpressionUtils;

@Service
@Transactional(readOnly = true)
@Slf4j
public class AccessionServiceImpl extends FilteredCRUDService2Impl<Accession, AccessionFilter, AccessionRepository> implements AccessionService {

	private static final String[] BOOST_FIELDS = { "accessionNumber", "names.plantName", "taxonomySpecies.name" };

	@Autowired
	protected JPAQueryFactory jpaQueryFactory;

	@Autowired
	private InventoryService inventoryService;

	@Autowired
	private AccessionSourceRepository accessionSourceRepository;

	@Autowired
	private AccessionInvNameRepository accessionInvNameRepository;

	@Autowired
	private AccessionInvAttachRepository accessionInvAttachRepository;

	@Autowired
	private AccessionInvAnnotationRepository annotationRepository;

	@Autowired
	private AccessionInvGroupService accessionInvGroupService;

	@Autowired
	private AccessionInvGroupRepository accessionInvGroupRepository;

	@Autowired
	private AccessionSourceMapService accessionSourceMapService;

	@Autowired
	private SiteRepository siteRepository;

	@Autowired
	private InventoryMaintenancePolicyRepository inventoryMaintenancePolicyRepository;

	@Autowired
	private InventoryRepository inventoryRepository;

	@Autowired
	private MethodRepository methodRepository;

	@Autowired
	private OverviewHelper overviewHelper;

	@Autowired
	private AccessionMCPDConverter accessionMCPDConverter;

	@Autowired
	private GGCESecurityConfig.GgceSec ggceSec;

	@Autowired
	private InventoryAttachmentService attachmentService;

	@Component
	protected static class ActionSupport extends BaseActionSupport<Accession, AccessionAction, AccessionActionFilter, AccessionActionRepository, AccessionActionRequest, AccessionActionScheduleFilter>
		implements AccessionActionService {

		@Autowired
		private AccessionRepository accessionRepository;

		@Autowired
		private MethodService methodService;

		@Override
		protected EntityPath<Accession> getOwningEntityPath() {
			return QAccessionAction.accessionAction.accession();
		}

		@Override
		protected void initializeActionDetails(List<AccessionAction> actions) {
			actions.forEach(action -> Hibernate.initialize(action.getAccession()));
		}

		@Override
		protected void applyOwningEntityFilter(AccessionActionScheduleFilter filter, String owningEntityAlias, List<Predicate> predicates) {
			QAccession qAccession = new QAccession(owningEntityAlias);
			if (predicates != null && filter.accession != null) {
				predicates.addAll(filter.accession.collectPredicates(qAccession));
			}
		}

		@Override
		@PostAuthorize("@ggceSec.actionAllowed('PassportData', 'ADMINISTRATION', returnObject.accession.site)")
		protected AccessionAction createAction(Accession owningEntity) {
			AccessionAction action = new AccessionAction();
			action.setAccession(owningEntity);
			action.setIsWebVisible("N");
			return action;
		}

		@Override
		protected void updateAction(AccessionAction action, AccessionActionRequest request) {
			action.setIsWebVisible(request.webVisible);
			if (request.method != null && !request.method.isNew()) {
				action.setMethod(methodService.get(request.method.getId()));
			}
		}

		@Override
		@Transactional
		@PostAuthorize("@ggceSec.actionAllowed('PassportData', 'ADMINISTRATION', returnObject.accession.site)")
		public AccessionAction create(AccessionAction source) {
			assert (source.getId() == null);
			AccessionAction saved = repository.save(source);
			return _lazyLoad(saved);
		}

		@Override
		@Transactional
		@PostAuthorize("@ggceSec.actionAllowed('PassportData', 'ADMINISTRATION', returnObject.accession.site)")
		public AccessionAction remove(AccessionAction entity) {
			return super.remove(entity);
		}

		@Override
		@Transactional
		@PostAuthorize("@ggceSec.actionAllowed('PassportData', 'ADMINISTRATION', returnObject.accession.site)")
		public AccessionAction update(AccessionAction updated) {
			return super.update(updated);
		}

		@Override
		protected Iterable<Accession> findOwningEntities(Set<Long> id) {
			return accessionRepository.findAll(QAccession.accession.id.in(id));
		}

		@Override
		public AccessionAction prepareNextWorkflowStepAction(WorkflowActionStep nextStep, AccessionAction completedAction) {
			AccessionAction nextAction = new AccessionAction();
			nextAction.setAccession(new Accession(completedAction.getAccession().getId()));
			nextAction.setIsWebVisible("N");
			return nextAction;
		}
	}

	@Override
	protected NumberPath<Long> entityIdPredicate() {
		return QAccession.accession.id;
	}

	@Override
	protected JPAQuery<Accession> entityListQuery() {
		return jpaQueryFactory.selectFrom(QAccession.accession)
			// site (@OneToOne)
			.join(QAccession.accession.site()).fetchJoin()
			// species (@OneToOne)
			.join(QAccession.accession.taxonomySpecies()).fetchJoin()
			// pedigree (@OneToOne optional)
			.leftJoin(QAccession.accession.accessionPedigree()).fetchJoin();
	}

	@Override
	@Transactional
	@PostAuthorize("@ggceSec.actionAllowed('PassportData', 'CREATE', returnObject.site)")
	public Accession create(Accession source) {
		assert (source.getId() == null);
		Accession accession = repository.save(source);

		if (CollectionUtils.isNotEmpty(source.getAccessionSources())) {
			// Clone and fix reference
			List<AccessionSource> sources = source.getAccessionSources().stream().map((accessionSource) -> {
				AccessionSource copy = new AccessionSource();
				copy.apply(accessionSource);
				copy.setAccession(accession);
				return copy;
			}).collect(Collectors.toList());
			accession.setAccessionSources(accessionSourceRepository.saveAll(sources));
		}

		// we assure system inventory inside the aspect, so mustn't be null
		Inventory systemInventory = inventoryRepository.getSystemInventory(accession);

		if (CollectionUtils.isNotEmpty(source.getNames())) {
			// Clone and fix reference
			List<AccessionInvName> names = source.getNames().stream().map((accessionInvName) -> {
				AccessionInvName copy = new AccessionInvName();
				copy.apply(accessionInvName);
				copy.setInventory(systemInventory);
				return copy;
			}).collect(Collectors.toList());
			accessionInvNameRepository.saveAll(names);
		}
		return accession;
	}

	@Override
	@Transactional
	@PreAuthorize("@ggceSec.actionAllowed('PassportData', 'WRITE', #updated.site)")
	@PostAuthorize("@ggceSec.actionAllowed('PassportData', 'WRITE', returnObject.site)")
	public Accession update(Accession updated) {
		return super.update(updated);
	}

	@Override
	@Transactional
	@PreAuthorize("@ggceSec.actionAllowed('PassportData', 'WRITE', #target.site)")
	public Accession update(Accession updated, Accession target) {
		assert (target.getId() != null);
		assert (target.getId().equals(updated.getId()));

		log.debug("Update Accession. Input data {}", updated);
		target.apply(updated);

		final Accession saved = repository.save(target);

		// FIXME This must be revised
		if (Hibernate.isInitialized(updated.getAccessionSources()) && CollectionUtils.isNotEmpty(updated.getAccessionSources())) {
			// Update references
			updated.getAccessionSources().forEach((accessionSource) -> {
				accessionSource.setAccession(saved);
			});
			accessionSourceRepository.saveAll(updated.getAccessionSources());
		}
		return saved;
	}

	@Override
	@Transactional
	@PreAuthorize("@ggceSec.actionAllowed('PassportData', 'WRITE', #target.site)")
	public Accession updateFast(Accession updated, Accession target) {
		target.apply(updated);
		return repository.save(target);
	}

	@Override
	@Transactional
	@PreAuthorize("@ggceSec.actionAllowed('PassportData', 'DELETE', #entity.site)")
	public Accession remove(Accession entity) {
		return super.remove(entity);
	}

	@Override
	@Transactional
	@PreAuthorize("@ggceSec.actionAllowed('PassportData', 'DELETE', #accession.site)")
	public void deleteDefaultInventory(Accession accession) {
		final Inventory systemInventory = inventoryRepository.getSystemInventory(accession);
		if (systemInventory != null) {
			log.info("Removing the SYSTEM inventory for removed accession {}", accession.getId());
			inventoryRepository.delete(systemInventory);
		}
	}

	@Override
	public Page<Accession> list(AccessionFilter filter, Pageable page) throws SearchException {
		return super.list(Accession.class, filter, page, BOOST_FIELDS);
	}

	@Override
	public Page<AccessionMCPD> listMCPD(AccessionFilter filter, Pageable page) throws SearchException {
		var stopWatch = Stopwatch.createStarted();
		var r = super.list(Accession.class, filter, page, BOOST_FIELDS);
		log.warn("Loaded page {} in {}ms", page.getPageNumber(), stopWatch.elapsed(TimeUnit.MILLISECONDS));


		// Populate JPA caches
		var accessionIds = new ArrayList<Long>(); // r.getContent().stream().map(Accession::getId).collect(Collectors.toList())
		Map<Long, Accession> accessionsById = new HashMap<>();
		r.getContent().forEach(a -> {
			accessionIds.add(a.getId());
			a.setInventories(new ArrayList<>());
			a.setAccessionSources(new ArrayList<>());
			a.setNames(new ArrayList<>());
			accessionsById.put(a.getId(), a);
		});

		var accessionInventories = jpaQueryFactory.selectFrom(QInventory.inventory)
			.where(QInventory.inventory.accession().id.in(accessionIds)).fetch();
		log.warn("Loaded {} inventories for page {} in {}ms", accessionInventories.size(), page.getPageNumber(), stopWatch.elapsed(TimeUnit.MILLISECONDS));
		accessionInventories.forEach(inv -> {
			var a = accessionsById.get(inv.getAccession().getId());
			if (a != null) {
				a.getInventories().add(inv);
			}
		});
		log.warn("Generated inventories of accessions in page {} in {}ms", page.getPageNumber(), stopWatch.elapsed(TimeUnit.MILLISECONDS));

		var accessionSources = jpaQueryFactory.selectFrom(QAccessionSource.accessionSource)
			.leftJoin(QAccessionSource.accessionSource.cooperators).fetchJoin()
			.where(QAccessionSource.accessionSource.accession().id.in(accessionIds)).fetch();
		log.warn("Loaded {} accessionSources for page {} in {}ms", accessionSources.size(), page.getPageNumber(), stopWatch.elapsed(TimeUnit.MILLISECONDS));
		accessionSources.forEach(source -> {
			var a = accessionsById.get(source.getAccession().getId());
			if (a != null) {
				a.getAccessionSources().add(source);
			}
		});
		log.warn("Generated accessionSources of accessions in page {} in {}ms", page.getPageNumber(), stopWatch.elapsed(TimeUnit.MILLISECONDS));

		var accessionInvNames = jpaQueryFactory.selectFrom(QAccessionInvName.accessionInvName)
			.join(QAccessionInvName.accessionInvName.inventory()).fetchJoin()
			.where(QAccessionInvName.accessionInvName.inventory().accession().id.in(accessionIds).and(QAccessionInvName.accessionInvName.inventory().formTypeCode.eq(Inventory.SYSTEM_INVENTORY_FTC)))
			.orderBy(QAccessionInvName.accessionInvName.plantNameRank.asc())
			.fetch();
		log.warn("Loaded {} accessionInvName for page {} in {}ms", accessionInvNames.size(), page.getPageNumber(), stopWatch.elapsed(TimeUnit.MILLISECONDS));
		accessionInvNames.forEach(accessionInvName -> {
			var a = accessionsById.get(accessionInvName.getInventory().getAccession().getId());
			if (a != null) {
				a.getNames().add(accessionInvName);
			}
		});
		log.warn("Generated accessionInvName of accessions in page {} in {}ms", page.getPageNumber(), stopWatch.elapsed(TimeUnit.MILLISECONDS));

		var accessionPedigrees = jpaQueryFactory.selectFrom(QAccessionPedigree.accessionPedigree)
			.where(QAccessionPedigree.accessionPedigree.accession().id.in(accessionIds)).fetch();
		log.warn("Loaded {} accessionPedigree for page {} in {}ms", accessionPedigrees.size(), page.getPageNumber(), stopWatch.elapsed(TimeUnit.MILLISECONDS));
		accessionPedigrees.forEach(accessionPedigree -> {
			var a = accessionsById.get(accessionPedigree.getAccession().getId());
			if (a != null) {
				accessionPedigree.lazyLoad();
				a.setAccessionPedigree(accessionPedigree);
			}
		});
		log.warn("Generated accessionPedigree of accessions in page {} in {}ms", page.getPageNumber(), stopWatch.elapsed(TimeUnit.MILLISECONDS));

		var result = new PageImpl<>(r.getContent().stream().map(accessionMCPDConverter::convert).collect(Collectors.toList()), page, r.getTotalElements());
		log.warn("Converted page {} to MCPD in {}ms", page.getPageNumber(), stopWatch.elapsed(TimeUnit.MILLISECONDS));

		return result;
	}

	@Override
	@Transactional
	@PreAuthorize("@ggceSec.actionAllowed('PassportData', 'ADMINISTRATION')")
	public MultiOp<Accession> update(List<Accession> accessions) {
		List<AccessionSource> accessionSources = accessions.stream().map(Accession::getAccessionSources)
			// filter
			.filter((s) -> s != null && s.size() > 0)
			// merge to single list
			.reduce(new ArrayList<>(), (aggregated, sources) -> {
				aggregated.addAll(sources);
				return aggregated;
			});

		accessions = accessions.stream().map(a -> repository.save(a)).collect(Collectors.toList());
		accessionSourceRepository.saveAll(accessionSources);

		return new MultiOp<>(accessions);
	}

	@Override
	public AccessionDetails getAccessionDetails(Accession accession) {
		if (accession == null) {
			throw new NotFoundElement();
		}

		accession = this.reload(accession);

		// initialize lazy data
		Hibernate.initialize(accession.getAccessionSources());
		Hibernate.initialize(accession.getAccessionActions());
		Hibernate.initialize(accession.getAccessionIprs());
		Hibernate.initialize(accession.getAccessionPedigree());
		Hibernate.initialize(accession.getAccessionQuarantines());
		Hibernate.initialize(accession.getCitations());
		Hibernate.initialize(accession.getBackupLocation1Site());
		Hibernate.initialize(accession.getBackupLocation2Site());
		Hibernate.initialize(accession.getExploration());

		AccessionDetails accessionDetails = new AccessionDetails();
		accessionDetails.accession = accession;
		accessionDetails.sources = accession.getAccessionSources();
		if (accessionDetails.sources != null) {
			accessionDetails.sources.forEach((source) -> source.lazyLoad());
		}
		accessionDetails.actions = accession.getAccessionActions();
		accessionDetails.ipr = accession.getAccessionIprs();
		accessionDetails.pedigree = accession.getAccessionPedigree();
		if (accessionDetails.pedigree != null) {
			accessionDetails.pedigree.lazyLoad();
		}
		accessionDetails.quarantine = accession.getAccessionQuarantines();
		if (accessionDetails.quarantine != null) {
			accessionDetails.quarantine.forEach((quarantine) -> quarantine.lazyLoad());
		}
		accessionDetails.citations = accession.getCitations();
		accessionDetails.names = accessionInvNameRepository.findAccessionNames(accession, Inventory.SYSTEM_INVENTORY_FTC);
		accessionDetails.groups = accessionInvGroupService.listAccessionGroups(accession);
		accessionDetails.attachments = accessionInvAttachRepository.findAccessionAttachments(accession, Inventory.SYSTEM_INVENTORY_FTC);
		accessionDetails.attachments.forEach(accessionInvAttach -> {
			if (accessionInvAttach.getAttachCooperator() != null) {
				accessionInvAttach.getAttachCooperator().getId();
			}
		});
		accessionDetails.annotations = annotationRepository.findAccessionAnnotations(accession, Inventory.SYSTEM_INVENTORY_FTC);
		accessionDetails.sourceCooperators = accessionSourceMapService.listAccessionSourceMaps(accession);

		return accessionDetails;
	}

	@Override
	public AccessionMCPD getMCPD(Long id) {
		return accessionMCPDConverter.convert(get(id));
	}

	@Override
	@Transactional
	@PreAuthorize("@ggceSec.actionAllowed('Acquisition', 'CREATE')")
	public AccessionInvGroup acquire(AcquisitionData acquisitionBatch) {
		if (acquisitionBatch.accessions == null || acquisitionBatch.accessions.size() == 0) {
			throw new InvalidApiUsageException("No accessions in acquisition batch");
		}

		Method method = acquisitionBatch.methodId == null ? null : methodRepository.getReferenceById(acquisitionBatch.methodId);
		if (method != null) {
			log.info("Registering new material with method {}", method.getName());
		}

		Site site = siteRepository.getReferenceById(acquisitionBatch.siteId);
		log.info("Registering new material with site {}", site.getSiteShortName());

		InventoryMaintenancePolicy inventoryMaintPolicy = inventoryMaintenancePolicyRepository.getReferenceById(acquisitionBatch.inventoryMaintenancePolicyId);
		log.info("Using policy {}", inventoryMaintPolicy.getMaintenanceName());

		// Make accessions
		List<Accession> accessions = acquisitionBatch.accessions.stream().map((newAcce) -> {
			newAcce.setSite(site);

			var acce = create(newAcce);
			acce.setInitialReceivedDate(acquisitionBatch.sourceDate);
			if (acce.getInitialReceivedDate() != null) {
				acce.setInitialReceivedDateCode(StringUtils.defaultIfBlank(acquisitionBatch.sourceDateCode, CommunityCodeValues.DATE_FORMAT_DATE.value));
			}

			if (acquisitionBatch.sourceTypeCode != null) {
				var accessionSource = new AccessionSource();
				accessionSource.setAccession(acce);
				accessionSource.setSourceTypeCode(acquisitionBatch.sourceTypeCode);
				accessionSource.setSourceDate(acquisitionBatch.sourceDate);
				accessionSource.setSourceDateCode(StringUtils.defaultIfBlank(acquisitionBatch.sourceDateCode, CommunityCodeValues.DATE_FORMAT_DATE.value));
				accessionSource.setNote(acquisitionBatch.sourceNote);
				accessionSource = accessionSourceRepository.save(accessionSource);
				if (acquisitionBatch.sourceCooperator != null) {
					accessionSourceMapService.create(new AccessionSourceMap(accessionSource, acquisitionBatch.sourceCooperator));
				}
			}
			return acce;

		}).collect(Collectors.toList());
		log.warn("Created {} accessions", accessions.size());

		// Make inventories
		List<Inventory> inventories = accessions.stream().map((accession) -> {
			Inventory inventory = new Inventory();
			inventory.setAccession(accession);
			inventory.setSite(site);
			inventory.setInventoryNumberPart1(acquisitionBatch.inventoryNumberPart1);
			inventory.setInventoryNumberPart2(AccessionService.AUTO_GENERATE_VALUE);
			inventory.setInventoryMaintenancePolicy(inventoryMaintPolicy);
			inventory.setFormTypeCode(acquisitionBatch.formTypeCode);
			inventory.setAvailabilityStatusCode(acquisitionBatch.availabilityStatusCode);
			return inventoryService.create(inventory);
		}).collect(Collectors.toList());
		log.warn("Created {} inventories", inventories.size());

		// Register group
		var groupName = acquisitionBatch.groupName;
		if (StringUtils.isBlank(groupName)) {
			throw new InvalidApiUsageException("Empty group name in acquisition batch");
		}
		AccessionInvGroup group;
		group = accessionInvGroupRepository.findOne(QAccessionInvGroup.accessionInvGroup.groupName.eq(groupName)).orElse(null);
		if (group == null) {
			group = new AccessionInvGroup();
			group.setGroupName(groupName);
			group.setNote(acquisitionBatch.note);
			group.setMethod(method);
			group = accessionInvGroupService.create(group);
			log.info("Registered group id={} {}", group.getId(), group.getGroupName());
		}

		group = accessionInvGroupService.addMembers(group, inventories, null);
		return accessionInvGroupService.load(group.getId());
	}

	@Override
	public Map<Object, Number> accessionOverview(String groupBy, AccessionFilter filter) {
		return overviewHelper.getOverview(Accession.class, QAccession.accession, QAccession.accession.id.countDistinct(), groupBy, filter);
	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public void recalculateAllAccessionNumbers() {
		boolean lastPage = false;
		int pageNumber = 0;

		do {
			Page<Accession> page = repository.findAll(PageRequest.of(pageNumber, 1000));
			lastPage = page.isLast();
			pageNumber += 1;

			log.warn("Updating accessionNumber for {} records, page {}", page.getNumberOfElements(), pageNumber);
			Lists.partition(page.getContent(), 100).forEach(batch -> TransactionHelper.executeInTransaction(false, () -> {
				batch.forEach(a -> repository.setAccessionNumber(a.getId(), GGCE.accessionNumber(a)));
				return true;
			}));
		} while (!lastPage);

		log.info("Done.");
	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public void assignMissingAccessionNumbers() {
		boolean lastPage = false;
		int pageNumber = 0;

		do {
			Page<Accession> page = repository.findAll(QAccession.accession.accessionNumber.isNull(), PageRequest.of(0, 1000));
			lastPage = page.isLast();
			pageNumber++;

			log.warn("Assigning accessionNumber for {} records, page {}", page.getNumberOfElements(), pageNumber);
			Lists.partition(page.getContent(), 100).forEach(batch -> TransactionHelper.executeInTransaction(false, () -> {
				batch.forEach(a -> repository.setAccessionNumber(a.getId(), GGCE.accessionNumber(a)));
				return true;
			}));
		} while (!lastPage && pageNumber < 1000); // at most 1000 loops

		log.info("Done.");
	}

	@Override
	@Transactional
	public void shareAttachment(Long attachId, List<Long> accessionIds) {
		var accessions = repository.findAll(QAccession.accession.id.in(accessionIds), Pageable.unpaged()).getContent();
		var hasUnavailableSite = accessions.stream()
			.map(Accession::getSite)
			.distinct()
			.anyMatch(site -> !ggceSec.actionAllowed("PassportData", "ADMINISTRATION", site));
		if (hasUnavailableSite) {
			throw new AccessDeniedException("Don't have permission to create attachments for the selected accessions");
		}
		var attachment = attachmentService.get(attachId);
		var accessionInventories = accessions.stream()
			.map(a -> a.getInventories().stream().filter(inv -> Inventory.SYSTEM_INVENTORY_FTC.equals(inv.getFormTypeCode())).findFirst().orElse(null))
			.collect(Collectors.toList());
		attachmentService.shareAttachment(attachment, accessionInventories);
	}

	@Autowired
	@Lazy
	private TileMaker tileMaker;

	@Override
	@Transactional(readOnly = true)
	public byte[] getTile(AccessionFilter filter, int zoom, int xtile, int ytile) throws IOException {

		final double latN = CoordUtil.tileToLat(zoom, ytile);
		final double latS = CoordUtil.tileToLat(zoom, ytile + 1);
		final double diffLat = latN - latS;

		final double lonW = CoordUtil.tileToLon(zoom, xtile);
		final double lonE = CoordUtil.tileToLon(zoom, xtile + 1);
		final double diffLon = lonE - lonW;

		if (log.isDebugEnabled()) {
			log.debug("{} <= lat <= {} corr={}", latS, latN, diffLat * .1);
			log.debug("{} <= lon <= {} corr={}", lonW, lonE, diffLon * .1);
		}

		return tileMaker.makeTile(zoom, xtile, ytile, () -> {
			var qAccSrc = QAccessionSource.accessionSource;
			return jpaQueryFactory.select(qAccSrc.latitude, qAccSrc.longitude).distinct().from(qAccSrc)
				.where(qAccSrc.isOrigin.eq("Y").and(qAccSrc.latitude.between(latS - diffLat * .1, latN + diffLat * .1)).and(qAccSrc.longitude.between(lonW - diffLon * .1, lonE + diffLon * .1)).and(ExpressionUtils.allOf(filter.collectPredicates(qAccSrc.accession()))))
				.stream().map(tuple -> new Double[] { tuple.get(qAccSrc.latitude), tuple.get(qAccSrc.longitude) });
		});
	}

	@Override
	@Transactional(readOnly = true)
	public MapInfo<AccessionFilter> mapInfo(FilterInfo<AccessionFilter> filterInfo) {
		var mapInfo = new MapInfo<AccessionFilter>();
		mapInfo.filterCode = filterInfo.filterCode;
		mapInfo.filter = filterInfo.filter;

		var qAccSrc = QAccessionSource.accessionSource;
		mapInfo.count = jpaQueryFactory.select(qAccSrc.accession().countDistinct()).from(qAccSrc).where(qAccSrc.isOrigin.eq("Y").and(ExpressionUtils.allOf(filterInfo.filter.collectPredicates(qAccSrc.accession())))).fetchOne();

		var bounds = jpaQueryFactory.select(qAccSrc.latitude.min(), qAccSrc.longitude.min(), qAccSrc.latitude.max(), qAccSrc.longitude.max()).from(qAccSrc).where(qAccSrc.isOrigin.eq("Y").and(ExpressionUtils.allOf(filterInfo.filter.collectPredicates(qAccSrc.accession())))).fetchOne();
		mapInfo.bounds = new Double[][] { 
			{ bounds.get(0, Double.class), bounds.get(1, Double.class) },
			{ bounds.get(2, Double.class), bounds.get(3, Double.class) }
		};
		return mapInfo;
	}
}