DescriptorListServiceImpl.java

/*
 * Copyright 2018 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 static org.genesys.server.model.traits.QDescriptorList.descriptorList;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.security.model.AclSid;
import org.genesys.blocks.security.serialization.Permissions;
import org.genesys.blocks.security.service.CustomAclService;
import org.genesys.server.component.aspect.NotifyForReview;
import org.genesys.server.component.aspect.NotifyOnPublished;
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.filters.DescriptorListFilter;
import org.genesys.server.model.traits.Descriptor;
import org.genesys.server.model.traits.DescriptorList;
import org.genesys.server.model.traits.DescriptorListLang;
import org.genesys.server.persistence.DescriptorListLangRepository;
import org.genesys.server.persistence.traits.DescriptorListRepository;
import org.genesys.server.service.DescriptorListService;
import org.genesys.server.service.DescriptorListTranslationService;
import org.genesys.server.service.DescriptorService;
import org.genesys.server.service.DescriptorTranslationService;
import org.genesys.server.service.DownloadService;
import org.genesys.server.service.ElasticsearchService;
import org.genesys.server.service.TranslatorService;
import org.genesys.server.service.TranslatorService.FormattedText;
import org.genesys.server.service.TranslatorService.TextFormat;
import org.genesys.util.JPAUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.ConcurrencyFailureException;
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.data.domain.Sort;
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.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import com.google.common.collect.Sets;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;

/**
 * Service for working with {@link DescriptorList}.
 *
 * @author Andrey Lugovskoy
 */
@Slf4j
@Service
@Transactional(readOnly = true)
@Validated
public class DescriptorListServiceImpl extends FilteredTranslatedCRUDServiceImpl<
	DescriptorList, DescriptorListLang, DescriptorListTranslationService.TranslatedDescriptorList, DescriptorListFilter, DescriptorListRepository>
	implements DescriptorListService {

	@Autowired
	private DescriptorListRepository descriptorListRepository;

	/** The securityUtils. */
	@Autowired
	private SecurityUtils securityUtils;

	@Autowired
	private DescriptorService descriptorService;

	@Autowired
	private DownloadService downloadService;

	@Autowired(required = false)
	private ElasticsearchService elasticsearchService;

	@Autowired
	private CustomAclService aclService;

	@Autowired(required = false)
	private TranslatorService translatorService;

	@Autowired
	DescriptorTranslationService descriptorTranslationService;

	@Component(value = "DescriptorListTranslationSupport")
	protected static class DescriptorListTranslationSupport
		extends BaseTranslationSupport<
		DescriptorList, DescriptorListLang, DescriptorListTranslationService.TranslatedDescriptorList, DescriptorListFilter, DescriptorListLangRepository>
		implements DescriptorListTranslationService {

		@Override
		@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#source.entity, 'ADMINISTRATION')")
		public DescriptorListLang create(DescriptorListLang source) {
			return super.create(source);
		}

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

		@Override
		@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#source.entity, 'ADMINISTRATION')")
		public DescriptorListLang remove(DescriptorListLang source) {
			return super.remove(source);
		}
	}

	@Override
	@PostAuthorize("returnObject == null || returnObject.isPublished() || hasRole('ADMINISTRATOR') || hasPermission(returnObject, 'read')")
	public DescriptorList get(UUID uuid) {
		return repository.findByUuid(uuid);
	}

	@Override
	public long countDescriptorLists(DescriptorListFilter filter) throws SearchException {
		if (filter.isFulltextQuery()) {
			return elasticsearchService.count(DescriptorList.class, filter);
		}
		return descriptorListRepository.count(filter.buildPredicate());
	}

	private DescriptorList getDescriptorList(final DescriptorList input) {
		final DescriptorList descriptorList = descriptorListRepository.findById(input.getId()).orElse(null);

		if (descriptorList == null) {
			throw new NotFoundElement("Record not found by id=" + input.getId());
		}

		if (!descriptorList.getVersion().equals(input.getVersion())) {
			log.warn("Subset versions don't match anymore");
			throw new ConcurrencyFailureException("Object version changed to " + descriptorList.getVersion() + ", you provided " + input.getVersion());
		}

		return descriptorList;
	}

	private DescriptorList getUnpublishedDescriptorList(final DescriptorList input) {
		DescriptorList loadedDescriptorList = getDescriptorList(input);
		if (loadedDescriptorList.isPublished()) {
			throw new InvalidApiUsageException("Cannot modify a published Descriptor List.");
		}
		return loadedDescriptorList;
	}

	/**
	 * {@inheritDoc}
	 */
	@Transactional
	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#input.owner, 'write')")
	public DescriptorList create(final DescriptorList input) {
		return createFast(input);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#input.owner, 'write')")
	public DescriptorList createFast(DescriptorList input) {
		log.info("Create descriptor list {}", input);

		final DescriptorList descriptorList = new DescriptorList();
		copyValues(descriptorList, input);
		descriptorList.setOwner(input.getOwner());

		// can not be published when creating
		descriptorList.setState(PublishState.DRAFT);

		final DescriptorList saved = descriptorListRepository.save(descriptorList);

		// Grant all permissions to the Partner's SID
		final AclSid sid = aclService.ensureAuthoritySid(saved.getOwner().getAuthorityName());
		aclService.setPermissions(saved, sid, new Permissions().grantAll());

		return saved;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or (! #input.isPublished() and hasPermission(#input, 'write'))")
	public DescriptorList update(final DescriptorList input) {
		final DescriptorList descriptorList = getUnpublishedDescriptorList(input);

		return update(input, descriptorList);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or (! #target.isPublished() and hasPermission(#target, 'write'))")
	public DescriptorList update(DescriptorList updated, DescriptorList target) {
		return lazyLoad(updateFast(updated, target));
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or (! #target.isPublished() and hasPermission(#target, 'write'))")
	public DescriptorList updateFast(DescriptorList updated, DescriptorList target) {
		log.info("Updating descriptor list {}", updated);

		if (updated.getOwner() != null && !target.getOwner().equals(updated.getOwner())) {
			throw new InvalidApiUsageException("Descriptor list owner can't be changed");
		}

		copyValues(target, updated);

		return descriptorListRepository.save(target);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or (! #descriptorList.isPublished() and hasPermission(#descriptorList, 'DELETE'))")
	public DescriptorList remove(DescriptorList descriptorList) {
		descriptorList = getUnpublishedDescriptorList(descriptorList);

		descriptorListRepository.delete(descriptorList);
		return descriptorList;
	}

	/**
	 * {@inheritDoc}
	 */
	@Transactional
	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or (! #descriptorList.isPublished() and hasPermission(#descriptorList, 'WRITE'))")
	public DescriptorList removeDescriptors(final DescriptorList descriptorList, final Descriptor... descriptors) {
		final DescriptorList loaded = getUnpublishedDescriptorList(descriptorList);

		log.info("Remove descriptors {} of descriptor list{}.", descriptors, descriptorList);
		if (descriptors == null || descriptors.length == 0) {
			// Noop
			return descriptorList;
		}

		// Which UUIDs to remove?
		final Set<UUID> descriptorUuids = Arrays.stream(descriptors).map(descriptor -> descriptor.getUuid()).collect(Collectors.toSet());

		// Keep descriptors that are not in the list
		loaded.setDescriptors(loaded.getDescriptors().stream().filter(descriptor -> !descriptorUuids.contains(descriptor.getUuid())).collect(Collectors.toList()));

		return lazyLoad(descriptorListRepository.save(loaded));
	}

	/**
	 * {@inheritDoc}
	 */
	@Transactional
	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or (! #descriptorList.isPublished() and hasPermission(#descriptorList, 'WRITE'))")
	public DescriptorList addDescriptors(final DescriptorList descriptorList, final Descriptor... descriptors) {
		final DescriptorList loaded = getUnpublishedDescriptorList(descriptorList);

		if (descriptors == null || descriptors.length == 0) {
			// Noop
			return descriptorList;
		}

		// append to end
		for (final Descriptor descriptor : descriptors) {
			if (loaded.getDescriptors().contains(descriptor)) {
				log.info("Not adding existing descriptor uuid={} title={} to descriptionList uuid={} title={}", descriptor.getUuid(), descriptor.getTitle(), descriptorList.getUuid(), descriptorList
					.getTitle());
			} else {
				log.info("Add descriptor uuid={} title={} to descriptionList uuid={} title={}", descriptor.getUuid(), descriptor.getTitle(), descriptorList.getUuid(), descriptorList
					.getTitle());
				loaded.getDescriptors().add(descriptor);
			}
		}

		return lazyLoad(descriptorListRepository.save(loaded));
	}

	/**
	 * {@inheritDoc}
	 */
	@Transactional
	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') or (! #descriptorList.isPublished() and hasPermission(#descriptorList, 'WRITE'))")
	public DescriptorList setDescriptors(final DescriptorList descriptorList, final Descriptor[] descriptors) {
		final DescriptorList loaded = getUnpublishedDescriptorList(descriptorList);

		if (descriptors == null || descriptors.length == 0) {
			// Noop
			return descriptorList;
		}

		// Re-add them in order
		loaded.getDescriptors().clear();
		for (final Descriptor descriptor : descriptors) {
			log.info("Add descriptor uuid={} title={} to descriptionList uuid={} title={}", descriptor.getUuid(), descriptor.getTitle(), descriptorList.getUuid(), descriptorList
				.getTitle());
			loaded.getDescriptors().add(descriptor);
		}

		return lazyLoad(descriptorListRepository.save(loaded));
	}

	private DescriptorList lazyLoad(final DescriptorList descriptorList) {
		if (descriptorList == null) {
			throw new NotFoundElement("No such descriptor list");
		}
		if (descriptorList.getOwner() != null) {
			descriptorList.getOwner().getId();
		}

		return descriptorList;
	}

	private List<Descriptor> lazyLoadDescriptors(List<Descriptor> descriptors) {

		if (descriptors != null) {
			descriptors.size();
			descriptors.forEach(descriptor -> {
				descriptor.getTerms().size();
				if (descriptor.getImage() != null) {
					descriptor.getImage().getId();
				}
			});
		}
		return descriptors;
	}

	@Override
	@PostAuthorize("hasRole('ADMINISTRATOR') || returnObject==null || returnObject.isPublished() || hasPermission(returnObject, 'read')")
	public DescriptorList load(final long id) {
		return lazyLoad(descriptorListRepository.findById(id).orElse(null));
	}

	@Override
	@PostAuthorize("hasRole('ADMINISTRATOR') || returnObject==null || returnObject.isPublished() || hasPermission(returnObject, 'read')")
	public DescriptorList loadDescriptorList(final UUID uuid) {
		return lazyLoad(descriptorListRepository.findByUuid(uuid));
	}

	@Override
	@PostAuthorize("hasRole('ADMINISTRATOR') || returnObject==null || returnObject.entity.isPublished() || hasPermission(returnObject.entity, 'read')")
	public DescriptorListTranslationService.TranslatedDescriptorList loadTranslatedDescriptorList(final UUID uuid) {
		return translate(lazyLoad(descriptorListRepository.findByUuid(uuid)));
	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') || #descriptorList.isPublished() || hasPermission(#descriptorList, 'read')")
	public List<Descriptor> loadDescriptors(DescriptorList descriptorList) {
		descriptorList = descriptorListRepository.findByUuid(descriptorList.getUuid());

		return lazyLoadDescriptors(descriptorList.getDescriptors());
	}

	@Override
	@PostAuthorize("hasRole('ADMINISTRATOR') || returnObject==null || returnObject.isPublished() || hasPermission(returnObject, 'read')")
	public DescriptorList loadDescriptorList(final UUID uuid, final int version) {
		return lazyLoad(descriptorListRepository.findByUuidAndVersion(uuid, version));
	}

	/**
	 * {@inheritDoc}
	 */
	private void copyValues(final DescriptorList target, final DescriptorList source) {
		// do not copy source#owner
		target.setCrop(source.getCrop());
		target.setDescription(source.getDescription());
		target.setState(source.getState());
		target.setTitle(source.getTitle());
		target.setUrl(source.getUrl());
		target.setVersionTag(source.getVersionTag());
		target.setUuid(source.getUuid());
		target.setBibliographicCitation(source.getBibliographicCitation());
		target.setPublisher(source.getPublisher());
		target.setOriginalLanguageTag(source.getOriginalLanguageTag());
	}

	@Override
	@PostAuthorize("hasRole('ADMINISTRATOR') || returnObject==null || returnObject.isPublished() || hasPermission(returnObject, 'read')")
	public DescriptorList loadDescriptorList(final DescriptorList input) {
		return loadDescriptorList(input.getUuid(), input.getVersion());
	}

	private DescriptorList loadDescriptorList(final UUID uuid, final Integer version) {
		if (uuid == null) {
			throw new InvalidApiUsageException("Required parameter uuid is missing");
		}
		final DescriptorList descriptorList = descriptorListRepository.findByUuid(uuid);

		if (descriptorList == null) {
			throw new NotFoundElement("Record not found by uuid=" + uuid);
		}

		if (version != null && !descriptorList.getVersion().equals(version)) {
			throw new ConcurrencyFailureException("Object version changed to " + descriptorList.getVersion() + ", you provided " + version);
		}

		return descriptorList;
	}

	@Override
	public Page<DescriptorList> list(final DescriptorListFilter filters, final Pageable page) throws SearchException {
		final BooleanBuilder published = new BooleanBuilder();
		published.and(descriptorList.state.in(PublishState.PUBLISHED));
		if (filters.isFulltextQuery()) {
			return elasticsearchService.findAll(DescriptorList.class, filters, published, page);
		}
		Pageable markdownSortPageRequest = JPAUtils.toMarkdownSort(page, "title");
		Page<DescriptorList> res = descriptorListRepository.findAll(published.and(filters.buildPredicate()), markdownSortPageRequest);
		return new PageImpl<DescriptorList>(res.getContent(), page, res.getTotalElements());
	}

	@Override
	public Page<DescriptorListTranslationService.TranslatedDescriptorList> listFiltered(final DescriptorListFilter filters, final Pageable page) throws SearchException {
		final BooleanBuilder published = new BooleanBuilder();
		published.and(descriptorList.state.in(PublishState.PUBLISHED));
		Page<DescriptorList> res;
		if (filters.isFulltextQuery()) {
			res = elasticsearchService.findAll(DescriptorList.class, filters, published, page);
		} else {
			Pageable markdownSortPageRequest = JPAUtils.toMarkdownSort(page, "title");
			res = descriptorListRepository.findAll(published.and(filters.buildPredicate()), markdownSortPageRequest);
		}
		
		return new PageImpl<>(translationSupport.getTranslated(res.getContent()), page, res.getTotalElements());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Map<String, ElasticsearchService.TermResult> getSuggestions(DescriptorListFilter filter) throws SearchException, IOException {
		assert (filter != null);

		Set<String> suggestions = Sets.newHashSet("crop");
		Map<String, ElasticsearchService.TermResult> suggestionRes = new HashMap<>(suggestions.size());

		for (String suggestionKey : suggestions) {
			DescriptorListFilter suggestionFilter = filter.copy(DescriptorListFilter.class);
			suggestionFilter.state(PublishState.PUBLISHED);
			try {
				suggestionFilter.clearFilter(suggestionKey);
			} catch (NoSuchFieldException | IllegalAccessException e) {
				log.error("Error while clearing filter: ", e.getMessage());
			}

			ElasticsearchService.TermResult suggestion = elasticsearchService.termStatisticsAuto(DescriptorList.class, suggestionFilter, 100, suggestionKey);
			suggestionRes.put(suggestionKey, suggestion);
		}
		return suggestionRes;
	}

	@Override
	public Page<DescriptorList> listDescriptorListsForCurrentUser(final DescriptorListFilter filter, final Pageable page) throws SearchException {
		final Pageable markdownSortPageRequest = JPAUtils.toMarkdownSort(page, "title");
		Page<DescriptorList> res;
		if (securityUtils.hasRole(UserRole.ADMINISTRATOR)) {
			if (filter.isFulltextQuery()) {
				res = elasticsearchService.findAll(DescriptorList.class, filter, markdownSortPageRequest);
			} else {
				res = descriptorListRepository.findAll(filter.buildPredicate(), markdownSortPageRequest);
			}
		} else {
			final HashSet<Long> partners = new HashSet<>(securityUtils.listObjectIdentityIdsForCurrentUser(Partner.class, BasePermission.WRITE));
			if (filter.isFulltextQuery()) {
				res = elasticsearchService.findAll(DescriptorList.class, filter, descriptorList.owner().id.in(partners), markdownSortPageRequest);
			} else {
				res = descriptorListRepository.findAll(descriptorList.owner().id.in(partners).and(filter.buildPredicate()), markdownSortPageRequest);
			}
		}
		return new PageImpl<DescriptorList>(res.getContent(), page, res.getTotalElements());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	@NotifyOnPublished
	public DescriptorList approveDescriptorList(final DescriptorList descriptorList) {
		final DescriptorList loaded = getUnpublishedDescriptorList(descriptorList);

		if (loaded.getState() == PublishState.DRAFT) {
			throw new InvalidApiUsageException("Descriptor List should be sent for review before publication");
		}

		for (final Descriptor descriptor : loaded.getDescriptors()) {
			if (descriptor.getState() != PublishState.PUBLISHED) {
				log.info("Publishing descriptor {}", descriptor);
				try {
					descriptorService.approveDescriptor(descriptor);
				} catch (InvalidApiUsageException e) {
					throw e;
				}
			}
		}

		loaded.setState(PublishState.PUBLISHED);
		return lazyLoad(descriptorListRepository.saveAndFlush(loaded));
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#descriptorList, 'WRITE')")
	@NotifyForReview
	public DescriptorList reviewDescriptorList(final DescriptorList descriptorList) {
		final DescriptorList loaded = getUnpublishedDescriptorList(descriptorList);

		if (loaded.getState() == PublishState.REVIEWING) {
			throw new InvalidApiUsageException("The DescriptorList is already under approval");
		}

		for (final Descriptor descriptor : loaded.getDescriptors()) {
			if (descriptor.getState() == PublishState.DRAFT) {
				log.info("Send to review descriptor {}", descriptor);
				try {
					descriptorService.reviewDescriptor(descriptor);
				} catch (InvalidApiUsageException e) {
					throw e;
				}
			}
		}

		loaded.setState(PublishState.REVIEWING);
		return lazyLoad(descriptorListRepository.save(loaded));
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#descriptorList, 'administration')")
	public DescriptorList rejectDescriptorList(final DescriptorList descriptorList) {
		final DescriptorList loaded = descriptorListRepository.findByUuidAndVersion(descriptorList.getUuid(), descriptorList.getVersion());
		if (loaded == null) {
			throw new NotFoundElement("No DescriptorList with specified uuid and version");
		}

		if (loaded.isPublished() && !securityUtils.hasRole(UserRole.ADMINISTRATOR)) {
			long oneDay = 24 * 60 * 60 * 1000;
			if (loaded.getLastModifiedDate() != null && loaded.getLastModifiedDate().toEpochMilli() <= (System.currentTimeMillis() - oneDay)) {
				throw new InvalidApiUsageException("Cannot be un-published. More than 24 hours have passed since the publication.");
			}
		}

		loaded.setState(PublishState.DRAFT);
		descriptorListRepository.save(loaded);

		for (final Descriptor referencedDescriptor : loaded.getDescriptors()) {
			log.info("Rejecting descriptor {}", referencedDescriptor);
			try {
				descriptorService.rejectDescriptor(referencedDescriptor);
			} catch (final InvalidApiUsageException e) {
				log.info("Not unpublishing a descriptor referenced in a published dataset or descriptor list: {}", e.getMessage());
			}
		}

		return lazyLoad(descriptorListRepository.save(loaded));
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<DescriptorList> autocompleteDescriptorLists(final String text) {
		Pageable markdownSortPageRequest = JPAUtils.toMarkdownSort(PageRequest.of(0, 15, Sort.by("title")), "title");

		if (StringUtils.isBlank(text)) {
			final Predicate predicate = descriptorList.state.in(PublishState.PUBLISHED);
			return descriptorListRepository.findAll(predicate, markdownSortPageRequest).getContent();
		} else {
			final Predicate predicate = descriptorList.state.in(PublishState.PUBLISHED).and(descriptorList.title.containsIgnoreCase(text));
			return descriptorListRepository.findAll(predicate, markdownSortPageRequest).getContent();
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	@PreAuthorize("#descriptorList.isPublished() || hasRole('ADMINISTRATOR') || hasPermission(#descriptorList, 'read')")
	public void exportDescriptorList(final DescriptorList descriptorList, final OutputStream outputStream) throws IOException {
		downloadService.writeXlsxDescriptorList(descriptorListRepository.getReferenceById(descriptorList.getId()), outputStream);
	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') || hasPermission(#original, 'write')")
	public DescriptorListLang machineTranslate(DescriptorList original, String targetLanguage) throws TranslatorService.TranslatorException {
		if (Objects.equals(original.getOriginalLanguageTag(), targetLanguage)) {
			throw new InvalidApiUsageException("Source and target language are the same");
		}

		var mt = new DescriptorListLang();
		mt.setMachineTranslated(true);
		mt.setLanguageTag(targetLanguage);
		mt.setEntity(original);

		if (translatorService == null) return mt;

		var builder = TranslatorService.TranslationStructuredRequest.builder()
			.targetLang(targetLanguage);

			// Translations to other languages use the English version (either original or translated)
		if (! Objects.equals(Locale.ENGLISH.getLanguage(), targetLanguage) && ! Objects.equals(Locale.ENGLISH.getLanguage(), original.getOriginalLanguageTag())) {
			var enTranslation = translationSupport.getLang(original, Locale.ENGLISH.getLanguage());
			if (enTranslation == null) {
				throw new InvalidApiUsageException("English text is not available.");
			}

			builder
				.sourceLang(enTranslation.getLanguageTag())
				// .context(enTranslation.getDescription())
				.texts(Map.of(
					"title", new FormattedText(TextFormat.markdown, enTranslation.getTitle()),
					"description", new FormattedText(TextFormat.markdown, enTranslation.getDescription())
				));

		} else {
			// Translations to English use the original texts
			builder
				.sourceLang(original.getOriginalLanguageTag())
				// .context(original.getDescription())
				.texts(Map.of(
					"title", new FormattedText(TextFormat.markdown, original.getTitle()),
					"description", new FormattedText(TextFormat.markdown, original.getDescription())
				));
		}

		var translations = translatorService.translate(builder.build());

		if (StringUtils.isNotBlank(original.getTitle())) {
			mt.setTitle(translations.getTexts().get("title"));
		}
		if (StringUtils.isNotBlank(original.getDescription())) {
			mt.setDescription(translations.getTexts().get("description"));
		}
		return mt;
	}

	@Override
	@PreAuthorize("hasRole('ADMINISTRATOR') || #descriptorList.isPublished() || hasPermission(#descriptorList, 'read')")
	public List<DescriptorTranslationService.TranslatedDescriptor> loadTranslatedDescriptors(DescriptorList descriptorList) {
		var reloaded = descriptorListRepository.findById(descriptorList.getId()).orElseThrow();
		return descriptorTranslationService.getTranslated(reloaded.getDescriptors());
	}

}