InstituteServiceImpl.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.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.persistence.EntityNotFoundException;

import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.blocks.security.model.AclSid;
import org.genesys.blocks.security.service.CustomAclService;
import org.genesys.server.api.v2.MultiOp;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.model.Partner;
import org.genesys.server.model.impl.Country;
import org.genesys.server.model.impl.FaoInstitute;
import org.genesys.server.model.impl.FaoInstituteSetting;
import org.genesys.server.model.impl.QFaoInstitute;
import org.genesys.server.model.vocab.VocabularyTerm;
import org.genesys.server.persistence.AccessionRepository;
import org.genesys.server.persistence.FaoInstituteRepository;
import org.genesys.server.persistence.FaoInstituteSettingRepository;
import org.genesys.server.persistence.GenesysLowlevelRepository;
import org.genesys.server.service.InstituteService;
import org.genesys.server.service.filter.InstituteFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import com.google.common.collect.Lists;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;

@Service
@Transactional(readOnly = true)
@Validated
public class InstituteServiceImpl extends FilteredCRUDServiceImpl<FaoInstitute, InstituteFilter, FaoInstituteRepository> implements InstituteService {
	public static final Logger LOG = LoggerFactory.getLogger(InstituteServiceImpl.class);

	private final static String HIBERNATE_CACHENAME = "hibernate.org.genesys.server.model.impl.FaoInstitute";
	private static final List<FaoInstitute> EMPTY_LIST = ListUtils.unmodifiableList(new ArrayList<FaoInstitute>());

	@Autowired
	private JPAQueryFactory jpaQueryFactory;

	@Autowired
	private FaoInstituteRepository instituteRepository;

	@Autowired
	private FaoInstituteSettingRepository instituteSettingRepository;

//	@Autowired
//	private ContentService contentService;

	@Autowired
	@Qualifier("genesysLowlevelRepositoryCustomImpl")
	private GenesysLowlevelRepository genesysLowlevelRepository;

	@Autowired
	private CustomAclService aclService;

	@Autowired
	private AccessionRepository accessionRepository;

	@Override
	public Page<FaoInstitute> listActive(Pageable pageable) {
		return instituteRepository.listAllActive(pageable);
	}

	@Override
	@Transactional
	@CacheEvict(value = { HIBERNATE_CACHENAME }, key = "#wiewsCode")
	public FaoInstitute update(final String wiewsCode, final FaoInstitute institute) {

		FaoInstitute target = findInstitute(wiewsCode);
		copyValues(target, institute);
		instituteSettingRepository.saveAll(target.getSettings().values());

		if (target.isUniqueAcceNumbs()) {
			// Assure existing data has unique accenumbs
			target.setUniqueAcceNumbs(0 == accessionRepository.countNonuniqueAccessionNumbers(target.getId()));
		}
		return instituteRepository.save(target);
	}

	@Override
	public Page<FaoInstitute> listPGRInstitutes(Pageable pageable) {
		return instituteRepository.listPGRInstitutes(pageable);
	}

	@Override
	public long countActive() {
		return instituteRepository.countActive();
	}

	@Override
	@Cacheable(cacheNames = { HIBERNATE_CACHENAME }, key = "#wiewsCode")
	public FaoInstitute getInstitute(String wiewsCode) {
		return instituteRepository.findByCode(wiewsCode);
	}

	@Override
	public FaoInstitute findInstitute(String wiewsCode) {
		final FaoInstitute inst = instituteRepository.findByCode(wiewsCode);
		if (inst != null) {
			inst.getSettings().size();
		}
		return inst;
	}

	/**
	 * Returns institute if user has required permissions
	 *
	 * @param wiewsCode code
	 * @return institute
	 */
	@Override
	@PostAuthorize("hasRole('ADMINISTRATOR') or hasPermission(returnObject, 'ADMINISTRATION')")
	public FaoInstitute getInstituteForEdit(final String wiewsCode) {
		return instituteRepository.findByCode(wiewsCode);
	}

	@Override
	public VocabularyTerm getInstituteTerm(String wiewsCode) {
		return toVocabularyTerm(getInstitute(wiewsCode));
	}


	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public FaoInstitute create(FaoInstitute source) {
		return _lazyLoad(repository.save(source));
	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#target, 'ADMINISTRATION')")
	public FaoInstitute update(FaoInstitute updated, FaoInstitute target) {
		return update(target.getCode(), updated);
	}

	@Override
	public List<FaoInstitute> listByCountry(Country country) {
		return instituteRepository.listByCountry(country, Sort.by("code"));
	}

	@Override
	public List<FaoInstitute> listByCountryActive(Country country) {
		return instituteRepository.findByCountryActive(country, Sort.by(Direction.DESC, "accessionCount", "code"));
	}

	@Override
	public List<FaoInstitute> getInstitutes(Collection<String> wiewsCodes) {
		if (wiewsCodes == null || wiewsCodes.size() == 0) {
			return EMPTY_LIST;
		}
		return instituteRepository.findAllByCodes(wiewsCodes);
	}

	@Override
	@Transactional(readOnly = false)
	@CacheEvict(value = { "statistics", HIBERNATE_CACHENAME }, allEntries = true)
	public MultiOp<FaoInstitute> update(final List<FaoInstitute> institutes) {
		var result = new MultiOp<FaoInstitute>();
		result.success = new ArrayList<FaoInstitute>(institutes.size());
		institutes.forEach(institute -> {
			if (institute.getId() != null && institute.isUniqueAcceNumbs()) {
				long uniqueAcceNumbs = accessionRepository.countUniqueAccessionNumbers(institute.getId());
				if (uniqueAcceNumbs > 0) {
					long nonUniqueAcceNumbs = accessionRepository.countNonuniqueAccessionNumbers(institute.getId());
					institute.setUniqueAcceNumbs(0 == nonUniqueAcceNumbs);
				} else {
					// Assume first insert into Genesys may contain duplicates
					institute.setUniqueAcceNumbs(false);
				}
			}
		});
		result.success.addAll(instituteRepository.saveAll(institutes));
		return result;
	}

//	@Override
//	@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CONTENTMANAGER') or hasPermission(#faoInstitute, 'ADMINISTRATION')")
//	@Transactional(readOnly = false)
//	public Article updateAbout(FaoInstitute faoInstitute, String summary, String body, Locale locale) throws CRMException {
//		return contentService.updateArticle(faoInstitute, ContentService.ENTITY_BLURB_SLUG, null, summary, body, locale);
//	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#faoInstitute, 'ADMINISTRATION')")
	@Transactional(readOnly = false)
	@CacheEvict(value = { HIBERNATE_CACHENAME }, key = "#faoInstitute.code")
	public void setUniqueAcceNumbs(FaoInstitute faoInstitute, boolean uniqueAcceNumbs) {
		final FaoInstitute inst = instituteRepository.findById(faoInstitute.getId()).orElseThrow(EntityNotFoundException::new);
		LOG.info("Setting 'uniqueAcceNumbs' to {} for {}", uniqueAcceNumbs, faoInstitute);
		inst.setUniqueAcceNumbs(uniqueAcceNumbs);
		if (uniqueAcceNumbs == true) {
			// If set to unique, test that current data is unique!
			inst.setUniqueAcceNumbs(0 == accessionRepository.countNonuniqueAccessionNumbers(inst.getId()));
		}
		instituteRepository.save(inst);
	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#faoInstitute, 'ADMINISTRATION')")
	@Transactional(readOnly = false)
	@CacheEvict(value = { HIBERNATE_CACHENAME }, key = "#faoInstitute.code")
	public void setAllowMaterialRequests(FaoInstitute faoInstitute, boolean allowMaterialRequests) {
		final FaoInstitute inst = instituteRepository.findById(faoInstitute.getId()).orElseThrow(EntityNotFoundException::new);
		LOG.info("Setting 'allowMaterialRequests' to {} for {}", allowMaterialRequests, faoInstitute);
		inst.setAllowMaterialRequests(allowMaterialRequests);
		instituteRepository.save(inst);
	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#faoInstitute, 'ADMINISTRATION')")
	@Transactional(readOnly = false)
	@CacheEvict(value = { HIBERNATE_CACHENAME }, key = "#faoInstitute.code")
	public void setCodeSGSV(FaoInstitute faoInstitute, String codeSGSV) {
		final FaoInstitute inst = instituteRepository.findById(faoInstitute.getId()).orElseThrow(EntityNotFoundException::new);
		LOG.info("Setting 'codeSGSV' to {} for {}", codeSGSV, faoInstitute);
		inst.setCodeSGSV(codeSGSV);
		instituteRepository.save(inst);
	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#faoInstitute, 'ADMINISTRATION')")
	@Transactional(readOnly = false)
	@CacheEvict(value = { HIBERNATE_CACHENAME }, key = "#faoInstitute.code")
	public void updateSettings(FaoInstitute faoInstitute, Map<String, String> settings) {
		final List<FaoInstituteSetting> toSave = new ArrayList<FaoInstituteSetting>();
		final List<FaoInstituteSetting> toRemove = new ArrayList<FaoInstituteSetting>();

		for (final var entry : settings.entrySet()) {
			final String settingValue = StringUtils.defaultIfBlank(entry.getValue(), null);

			FaoInstituteSetting setting = instituteSettingRepository.findByInstCodeAndSetting(faoInstitute.getCode(), entry.getKey());
			if (setting == null && settingValue != null) {
				setting = new FaoInstituteSetting(faoInstitute);
				setting.setSetting(entry.getKey());
				setting.setValue(settingValue);
				toSave.add(setting);
			} else if (setting != null) {
				setting.setValue(settingValue);
				if (settingValue == null) {
					toRemove.add(setting);
				} else {
					toSave.add(setting);
				}
			}
		}
		instituteSettingRepository.saveAll(toSave);
	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	@Transactional(readOnly = false)
	@CacheEvict(value = { "statistics", HIBERNATE_CACHENAME }, allEntries = true)
	public void updateCountryRefs() {
		genesysLowlevelRepository.updateFaoInstituteCountries();
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	@CacheEvict(value = { "statistics", HIBERNATE_CACHENAME }, allEntries = true)
	public void delete(String instCode) {
		final FaoInstitute institute = getInstitute(instCode);
		if (institute != null) {
			instituteSettingRepository.deleteFor(institute.getCode());
			instituteRepository.delete(institute);
		}
	}

	@Override
	@PreAuthorize("isAuthenticated()")
	public List<FaoInstitute> listMyInstitutes(Sort sort) {
		final AclSid sid = SecurityContextUtil.getCurrentUser();
		final List<Long> instituteOIDs = aclService.listObjectIdentityIdsForSid(FaoInstitute.class, sid, BasePermission.WRITE);
		final List<Long> partnerOIDs = aclService.listObjectIdentityIdsForSid(Partner.class, sid, BasePermission.WRITE);
		final Map<Long, FaoInstitute> institutes = new HashMap<>();

		BooleanExpression expression = QFaoInstitute.faoInstitute.owner().id.in(partnerOIDs).or(QFaoInstitute.faoInstitute.id.in(instituteOIDs));
		instituteRepository.findAll(expression, sort).spliterator().forEachRemaining(faoInstitute -> institutes.put(faoInstitute.getId(), faoInstitute));

		LOG.info("Got {} elements for {}", institutes.size(), sid);
		if (institutes.isEmpty()) {
			return null;
		}

		return Lists.newArrayList(institutes.values());
	}

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

	@Override
	public List<VocabularyTerm> autocompleteTerm(String ac) {
		return autocomplete(ac).stream().map(this::toVocabularyTerm).collect(Collectors.toList());
	}

	@Override
	public Map<String, String> decodeCodes(Set<String> codes) {

		/* @formatter:off */
		Predicate whereClause = codes != null
				? QFaoInstitute.faoInstitute.code.in(codes)
				: QFaoInstitute.faoInstitute.accessionCount.gt(0);

		List<Tuple> query = jpaQueryFactory.select(QFaoInstitute.faoInstitute.code, QFaoInstitute.faoInstitute.fullName)
				.from(QFaoInstitute.faoInstitute)
				.where(whereClause)
				.fetch();
		/* @formatter:on */

		return query.stream().collect(Collectors.toMap(tuple -> tuple.get(QFaoInstitute.faoInstitute.code), tuple -> tuple.get(QFaoInstitute.faoInstitute.fullName)));
	}

	private VocabularyTerm toVocabularyTerm(FaoInstitute institute) {
		if (institute == null) {
			throw new NotFoundElement("No such wiews term");
		}

		VocabularyTerm term = new VocabularyTerm();
		term.setCode(institute.getCode());
		term.setTitle(Optional.ofNullable(institute.getFullName()).orElse(Optional.ofNullable(institute.getAcronym()).orElse(institute.getCode())));
		term.setDescription(generateMarkdownForDescription(institute));
		return term;
	}

	/**
	 * Generate markdown for description of controlled vocabulary term
	 *
	 * @param institute institute data
	 * @return the description for controlled vocabulary term
	 */
	private String generateMarkdownForDescription(final FaoInstitute institute) {
		final String breakLine = "  \n";
		final String line1 =((Optional.ofNullable (institute.getAcronym()).orElse("")) + " " + (Optional.ofNullable(institute.getFullName()).orElse(""))).trim();
		final String line2 = Optional.ofNullable(institute.getEmail()).orElse("");
		final String line3 = Optional.ofNullable(institute.getUrl()).orElse("");
		String line4 = "";
		if (institute.getCountry() != null) {
			line4 = institute.getCountry().getCode3();
		}


		final StringBuilder builder = new StringBuilder();

		if (!line1.isEmpty()) {
			builder.append(line1).append(breakLine).append(breakLine);
		}
		if (!line2.isEmpty()) {
			builder.append(line2).append(breakLine);
		}
		if (!line3.isEmpty()) {
			builder.append(line3).append(breakLine);
		}
		if (!line2.isEmpty() || !line3.isEmpty()) {
			builder.append(breakLine);
		}
		if (!line4.isEmpty()) {
			builder.append(line4);
		}

		return builder.toString();
	}

	@Override
	public Page<VocabularyTerm> listTerms(Pageable page) {
		Page<FaoInstitute> res = instituteRepository.findAll(page);
		return res.map(inst -> toVocabularyTerm(inst));
	}

	private void copyValues(FaoInstitute target, FaoInstitute source) {
		target.setAllowMaterialRequests(source.isAllowMaterialRequests());
		target.setUniqueAcceNumbs(source.isUniqueAcceNumbs());
		target.setCodeSGSV(source.getCodeSGSV());
		if(source.getSettings() != null) {
			target.getSettings().putAll(source.getSettings());
		}
	}

	@Override
	@Transactional(propagation = Propagation.MANDATORY) // Need to be part of an existing transaction!
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	@CacheEvict(value = { HIBERNATE_CACHENAME }, allEntries = true)
	public FaoInstitute changeInstitute(FaoInstitute currentInstitute, FaoInstitute newInstitute) {
		LOG.warn("Migrating from {} to {}", currentInstitute.getCode(), newInstitute.getCode());

		newInstitute.setAccessionCount(currentInstitute.getAccessionCount());
		newInstitute.setAllowMaterialRequests(currentInstitute.isAllowMaterialRequests());
		newInstitute.setUniqueAcceNumbs(currentInstitute.isUniqueAcceNumbs());
		newInstitute.setEmail(currentInstitute.getEmail());
		// Clear PDCI
		newInstitute.setPdciAvg(newInstitute.getPdciAvg());
		newInstitute.setPdciMin(newInstitute.getPdciMin());
		newInstitute.setPdciMax(newInstitute.getPdciMax());
		newInstitute.setPdciHistogram(newInstitute.getPdciHistogram());
		var updated = instituteRepository.save(newInstitute);

		currentInstitute.setAccessionCount(0); // we move all accessions!
		currentInstitute.setAllowMaterialRequests(false);
		// currentInstitute.setvCode(newInstitute.getCode()); // Don't point to the new institute!
		// Clear PDCI
		currentInstitute.setPdciAvg(null);
		currentInstitute.setPdciMin(null);
		currentInstitute.setPdciMax(null);
		currentInstitute.setPdciHistogram(null);
		instituteRepository.save(currentInstitute);

		return updated;
	}
}