CitationServiceImpl.java

/*
 * Copyright 2024 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 com.querydsl.core.BooleanBuilder;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.genesys.server.api.v2.EntityUuidAndVersion;
import org.genesys.server.component.security.SecurityUtils;
import org.genesys.server.exception.InvalidApiUsageException;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.exception.SearchException;
import org.genesys.server.model.Partner;
import org.genesys.server.model.PublishState;
import org.genesys.server.model.UserRole;
import org.genesys.server.model.bib.Citation;
import org.genesys.server.model.bib.QCitation;
import org.genesys.server.model.filters.CitationFilter;
import org.genesys.server.model.impl.User;
import org.genesys.server.persistence.CitationRepository;
import org.genesys.server.service.CitationService;
import org.genesys.server.service.UserService;
import org.genesys.util.JPAUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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.Transactional;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import static org.genesys.blocks.security.SecurityContextUtil.hasRole;

@Service
public class CitationServiceImpl extends FilteredCRUDService2Impl<Citation, CitationFilter, CitationRepository> implements CitationService {

	@Autowired
	private UserService userService;

	@Autowired
	private JPAQueryFactory jpaQueryFactory;

	@Autowired
	private SecurityUtils securityUtils;

	@Override
	@PostAuthorize("hasRole('ADMINISTRATOR') or hasRole('CITATIONMANAGER') or returnObject.state.name().equals('PUBLISHED') or hasPermission(returnObject.owner, 'ADMINISTRATION')")
	@Transactional(readOnly = true)
	public Citation get(long id) {
		return super.get(id);
	}

	@Override
	@PostAuthorize("hasRole('ADMINISTRATOR') or hasRole('CITATIONMANAGER') or returnObject.state.name().equals('PUBLISHED') or hasPermission(returnObject.owner, 'ADMINISTRATION')")
	@Transactional(readOnly = true)
	public Citation get(UUID uuid) {
		return repository.findByUuid(uuid).orElseThrow(() -> new NotFoundElement("No record with uuid=" + uuid));
	}

	@Override
	@PostAuthorize("hasRole('ADMINISTRATOR') or hasRole('CITATIONMANAGER') or returnObject.state.name().equals('PUBLISHED') or hasPermission(returnObject.owner, 'ADMINISTRATION')")
	@Transactional(readOnly = true)
	public Citation get(EntityUuidAndVersion ref) {
		return repository.findByUuidAndVersion(ref.getUuid(), ref.getVersion()).orElseThrow(() -> new NotFoundElement("No such record"));
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CITATIONMANAGER') or hasPermission(#source.owner, 'ADMINISTRATION')")
	public Citation createFast(Citation source) {
		source.setState(PublishState.DRAFT);
		return super.createFast(source);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CITATIONMANAGER') or hasPermission(#source.owner, 'ADMINISTRATION')")
	public Citation create(Citation source) {
		final Citation citation = new Citation();
		citation.apply(source);
		final Citation saved = repository.save(citation);

		return _lazyLoad(saved);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CITATIONMANAGER') or hasPermission(#updated.owner, 'ADMINISTRATION')")
	public Citation update(Citation updated) {
		return super.update(updated);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CITATIONMANAGER') or hasPermission(#target.owner, 'ADMINISTRATION')")
	public Citation updateFast(@NotNull @Valid Citation updated, Citation target) {
		if (target.getState() == PublishState.PUBLISHED && !securityUtils.hasRole(UserRole.ADMINISTRATOR)) {
			throw new InvalidApiUsageException("Cannot update a published citation.");
		}
		target.apply(updated);
		if (updated.getCollections() != null) {
			target.setCollections(updated.getCollections());
		}
		if (updated.getTaxonomyFamilies() != null) {
			target.setTaxonomyFamilies(updated.getTaxonomyFamilies());
		}
		if (updated.getTaxonomySpecies() != null) {
			target.setTaxonomySpecies(updated.getTaxonomySpecies());
		}
		if (updated.getInstituteCodes() != null) {
			target.setInstituteCodes(updated.getInstituteCodes());
		}
		return repository.save(target);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CITATIONMANAGER') or hasPermission(#target.owner, 'ADMINISTRATION')")
	public Citation update(Citation updated, Citation target) {
		return _lazyLoad(updateFast(updated, target));
	}

	@Override
	@Transactional(readOnly = true)
	public Page<Citation> list(CitationFilter filter, Pageable page) throws SearchException {
		Page<Citation> res;

		if (filter != null && filter.isFulltextQuery()) {
			var predicate = QCitation.citation.state.eq(PublishState.PUBLISHED);
			res = elasticsearchService.findAll(Citation.class, filter, predicate, page);
		} else {
			Pageable markdownSortPageRequest = JPAUtils.toMarkdownSort(page, "title");
			var filterPublished = new CitationFilter().state(Set.of(PublishState.PUBLISHED));
			filter = filter == null ? filterPublished : (CitationFilter) filterPublished.AND(filter);

			res = repository.findAll(new BooleanBuilder().and(filter.buildPredicate()), markdownSortPageRequest);
		}
		return res;
	}

	@Override
	@Transactional(readOnly = true)
	public Page<Citation> listMyCitations(CitationFilter filter, Pageable page) throws SearchException {
		BooleanBuilder predicate = new BooleanBuilder();

		if (!(hasRole("ADMINISTRATOR") || hasRole("CITATIONMANAGER"))) {
			final User user = userService.getMe();
			if (user == null) {
				throw new InvalidApiUsageException("User not found in the system");
			}
			final HashSet<Long> partnersIds = new HashSet<>(securityUtils.listObjectIdentityIdsForCurrentUser(Partner.class, BasePermission.ADMINISTRATION));
			predicate.and(QCitation.citation.owner().id.in(partnersIds));
		}

		Page<Citation> result;
		if (filter != null && filter.isFulltextQuery()) {
			result = elasticsearchService.findAll(Citation.class, filter, predicate, page);
		} else {
			if (filter != null) {
				predicate.and(filter.buildPredicate());
			}
			result = repository.findAll(predicate, page);
		}

		return result;
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CITATIONMANAGER')")
	public Citation remove(Citation entity) {
		return super.remove(get(entity));
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CITATIONMANAGER')")
	public Citation approveCitation(final Citation citation) {
		var loaded = get(citation.getId());
		if (!PublishState.REVIEWING.equals(loaded.getState())) {
			throw new InvalidApiUsageException("Citation should be submitted before publication");
		}
		loaded.setState(PublishState.PUBLISHED);
		return repository.save(loaded);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CITATIONMANAGER')")
	public Citation rejectCitation(final Citation citation) {
		var loaded = get(citation.getId());
		loaded.setState(PublishState.DRAFT);
		return repository.save(loaded);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasRole('CITATIONMANAGER')")
	public Citation reviewCitation(final Citation citation) {
		var loaded = get(citation.getId());
		if (loaded.getState() == PublishState.REVIEWING) {
			throw new InvalidApiUsageException("The citation is already under approval");
		}
		loaded.setState(PublishState.REVIEWING);
		return repository.save(loaded);
	}

	@Override
	@Transactional(readOnly = true)
	// This can be public
	public List<String> getCollections(Partner owner) {
		var collections = Expressions.stringPath("collections");
		return jpaQueryFactory
			.selectDistinct(collections)
			.from(QCitation.citation)
			.join(QCitation.citation.collections, collections)
			.where(QCitation.citation.owner().eq(owner))
			.orderBy(collections.asc())
			.fetch();
	}

	@Override
	@Transactional(readOnly = true)
	// This can be public
	public List<String> getCollections(CitationFilter filter) {
		var cPredicate = QCitation.citation.state.eq(PublishState.PUBLISHED);
		if (filter != null) {
			cPredicate = cPredicate.and(filter.buildPredicate());
		}
		var collections = Expressions.stringPath("collections");
		return jpaQueryFactory
			.selectDistinct(collections)
			.from(QCitation.citation)
			.join(QCitation.citation.collections, collections)
			.where(cPredicate)
			.orderBy(collections.asc())
			.fetch();
	}
}