DescriptorServiceImpl.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.QDescriptor.descriptor;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
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.filerepository.FolderNotEmptyException;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.NoSuchRepositoryFileException;
import org.genesys.filerepository.model.RepositoryFolder;
import org.genesys.filerepository.model.RepositoryImage;
import org.genesys.filerepository.service.RepositoryService;
import org.genesys.server.component.security.AsAdminInvoker;
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.dataset.Dataset;
import org.genesys.server.model.filters.DescriptorFilter;
import org.genesys.server.model.filters.FilterHelpers;
import org.genesys.server.model.traits.Descriptor;
import org.genesys.server.model.traits.DescriptorLang;
import org.genesys.server.model.traits.DescriptorList;
import org.genesys.server.model.traits.QDescriptor;
import org.genesys.server.model.vocab.QVocabularyTerm;
import org.genesys.server.model.vocab.VocabularyTerm;
import org.genesys.server.model.vocab.VocabularyTermLang;
import org.genesys.server.persistence.DescriptorLangRepository;
import org.genesys.server.persistence.PartnerRepository;
import org.genesys.server.persistence.traits.DescriptorRepository;
import org.genesys.server.persistence.vocab.VocabularyTermRepository;
import org.genesys.server.service.DescriptorService;
import org.genesys.server.service.DescriptorTranslationService;
import org.genesys.server.service.DescriptorTranslationService.TranslatedDescriptor;
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.server.service.TranslatorService.TranslatorException;
import org.genesys.server.service.VersionManager;
import org.genesys.server.service.VocabularyTermTranslationService;
import org.genesys.util.HibernateUtil;
import org.genesys.util.JPAUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
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.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.BooleanExpression;
/**
* The Class DescriptorServiceImpl.
*/
@Slf4j
@Service("catalogDescriptorService")
@Transactional(readOnly = true)
@Validated
public class DescriptorServiceImpl extends FilteredTranslatedCRUDServiceImpl<
Descriptor, DescriptorLang, DescriptorTranslationService.TranslatedDescriptor, DescriptorFilter, DescriptorRepository>
implements DescriptorService {
private static final Predicate[] EMPTY_PREDICATE_ARRAY = new Predicate[] {};
/** The descriptor repository. */
@Autowired
private DescriptorRepository descriptorRepository;
@Autowired
private VocabularyTermRepository termRepository;
@Autowired
private DownloadService downloadService;
/** The securityUtils. */
@Autowired
private SecurityUtils securityUtils;
@Autowired
private EntityManager entityManager;
@Autowired
private VersionManager versionManager;
@Autowired(required = false)
private ElasticsearchService elasticsearchService;
/** The file repo service. */
@Autowired
private RepositoryService repositoryService;
@Autowired
private CustomAclService aclService;
/** Execute code as admin */
@Autowired
protected AsAdminInvoker asAdminInvoker;
@Autowired
private PartnerRepository partnerRepository;
@Autowired(required = false)
private TranslatorService translatorService;
@Component(value = "DescriptorTranslationSupport")
protected static class DescriptorTranslationSupport
extends BaseTranslationSupport<
Descriptor, DescriptorLang, DescriptorTranslationService.TranslatedDescriptor, DescriptorFilter, DescriptorLangRepository>
implements DescriptorTranslationService {
@Autowired
@Lazy
private VocabularyTermTranslationService vocabularyTermTranslationService;
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#source.entity, 'write')")
public DescriptorLang create(DescriptorLang source) {
return super.create(source);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#target.entity, 'write')")
public DescriptorLang update(DescriptorLang updated, DescriptorLang target) {
return super.update(updated, target);
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#source.entity, 'write')")
public DescriptorLang remove(DescriptorLang source) {
return super.remove(source);
}
@Override
public TranslatedDescriptor getTranslated(Descriptor input, Locale locale) {
var td = super.getTranslated(input, locale);
td.setTerms(vocabularyTermTranslationService.getTranslated(td.entity.getTerms(), locale));
return td;
}
@Override
public List<TranslatedDescriptor> getTranslated(List<Descriptor> input, Locale locale) {
var tds = super.getTranslated(input, locale);
tds.forEach(td -> {
td.setTerms(vocabularyTermTranslationService.getTranslated(td.entity.getTerms(), locale));
});
return tds;
}
}
/**
* {@inheritDoc}
*/
@Transactional
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#input.owner, 'write')")
public Descriptor create(final Descriptor input) {
return lazyLoad(createFast(input));
}
@Transactional
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#input.owner, 'write')")
public Descriptor createFast(Descriptor input) {
log.info("Creating descriptor: {} - {}", input.getTitle(), input.getDataType());
input.setOwner(partnerRepository.findById(input.getOwner().getId()).orElseThrow());
updateTerms(input);
final Descriptor descriptor = new Descriptor();
descriptor.apply(input);
descriptor.setUuid(input.getUuid());
descriptor.setVersionTag(input.getVersionTag());
// can not be published when creating
descriptor.setState(PublishState.DRAFT);
final Descriptor saved = descriptorRepository.save(descriptor);
// 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;
}
private Descriptor getDescriptor(final Descriptor input) {
Descriptor descriptor = null;
if (input.getUuid() != null) {
descriptor = descriptorRepository.findByUuid(input.getUuid());
} else {
descriptor = descriptorRepository.findById(input.getId()).orElse(null);
}
if (descriptor == null) {
throw new NotFoundElement("Record not found by uuid=" + input.getUuid() + " or id=" + input.getId());
}
if (input.getVersion() != null && !descriptor.getVersion().equals(input.getVersion())) {
throw new ConcurrencyFailureException("Object version changed to " + descriptor.getVersion() + ", you provided " + input.getVersion());
}
return descriptor;
}
private Descriptor getUnpublishedDescriptor(Descriptor descriptor) {
descriptor = getDescriptor(descriptor);
if (descriptor.isPublished()) {
throw new InvalidApiUsageException("Cannot modify a published Descriptor.");
}
return descriptor;
}
/**
* {@inheritDoc}
*/
@Override
@PostFilter("hasRole('ADMINISTRATOR') or filterObject.published or hasPermission(filterObject, 'READ')")
public List<Descriptor> searchMatchingDescriptor(final Descriptor input) {
try {
input.trimStringsToNull();
log.debug("searchMatching: {}", new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(input));
} catch (IOException e) {
}
List<Predicate> searches = Lists.newArrayList();
// version + title
searches.add(descriptor.versionTag.eq(input.getVersionTag()).and(descriptor.title.eq(input.getTitle())));
if (StringUtils.isNotBlank(input.getDescription())) {
// description
searches.add(descriptor.description.eq(input.getDescription()));
}
if (StringUtils.isNotBlank(input.getDescription())) {
// title + dataType
searches.add(descriptor.title.eq(input.getTitle()).and(descriptor.dataType.eq(input.getDataType())));
}
if (input.getCategory() != null) {
// title + category
searches.add(descriptor.title.eq(input.getTitle()).and(descriptor.category.eq(input.getCategory())));
}
if (StringUtils.isNotBlank(input.getUom())) {
// uom
searches.add(descriptor.dataType.eq(input.getDataType()).and(descriptor.uom.eq(input.getUom())));
}
final Predicate predicate = StringUtils.isBlank(input.getCrop()) ? descriptor.crop.isNull().andAnyOf(searches.toArray(EMPTY_PREDICATE_ARRAY)) : descriptor.crop.eq(input.getCrop()).andAnyOf(searches.toArray(EMPTY_PREDICATE_ARRAY));
List<Descriptor> matches = new ArrayList<>();
descriptorRepository.findAll(predicate).forEach(match -> matches.add(lazyLoad(match)));
return matches;
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#descriptor, 'write')")
public Descriptor updateImage(Descriptor descriptor, final MultipartFile file, final RepositoryImage imageMetadata) throws IOException, InvalidRepositoryPathException,
InvalidRepositoryFileDataException, NoSuchRepositoryFileException {
if (securityUtils.hasRole(UserRole.ADMINISTRATOR)) {
descriptor = getDescriptor(descriptor); // Editorial changes work regardless of state
} else {
descriptor = getUnpublishedDescriptor(descriptor);
}
if (!file.getContentType().startsWith("image/")) {
throw new InvalidApiUsageException("Invalid image type: " + file.getContentType() + " is not permitted.");
}
RepositoryImage repositoryImage = descriptor.getImage();
if (repositoryImage != null) {
descriptor = removeImage(descriptor);
}
String originalFilename = file.getOriginalFilename();
String ext = "";
if (originalFilename != null) {
int dotIndex = originalFilename.lastIndexOf(".");
ext = dotIndex > 0 ? originalFilename.substring(dotIndex) : "";
}
// Make descriptor folder
var descriptorFolder = ensureDescriptorFolder(descriptor);
repositoryImage = repositoryService.addFile(descriptorFolder.getFolderPath(), descriptor.getUuid().toString() + ext, file.getContentType(), file.getInputStream(), imageMetadata);
descriptor.setImage(repositoryImage);
return lazyLoad(descriptorRepository.save(descriptor));
}
private RepositoryFolder ensureDescriptorFolder(final Descriptor descriptor) {
try {
final Path descriptorPath = getRepositoryImageFolder(descriptor);
final Partner partner = descriptor.getOwner();
// Ensure folder ownership
return asAdminInvoker.invoke(() -> {
// Ensure target folder exists for the Descriptor
return repositoryService.ensureFolder(descriptorPath, partner);
});
} catch (Exception e) {
log.warn("Could not create folder properties: {}", e.getMessage());
throw new InvalidApiUsageException("Could not create folder", e);
}
}
@Override
@Transactional(rollbackFor = Throwable.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#descriptor, 'write')")
public Descriptor removeImage(Descriptor descriptor) throws InvalidRepositoryPathException, IOException {
if (securityUtils.hasRole(UserRole.ADMINISTRATOR)) {
descriptor = getDescriptor(descriptor); // Editorial changes work regardless of state
} else {
descriptor = getUnpublishedDescriptor(descriptor);
}
deleteDescriptorImage(descriptor);
return lazyLoad(descriptorRepository.save(descriptor));
}
private void deleteDescriptorImage(Descriptor descriptor) throws IOException {
RepositoryImage descriptorImage = descriptor.getImage();
descriptor.setImage(null);
if (descriptorImage != null) {
descriptorImage = HibernateUtil.unproxy(descriptorImage);
Path folderPath = descriptorImage.getFolder().getFolderPath();
try {
repositoryService.removeFile(descriptorImage);
} catch (NoSuchRepositoryFileException e) {
// That's fine
log.warn("Could not remove descriptor image: {}", e.getMessage());
}
try {
repositoryService.deleteFolder(folderPath);
} catch (Throwable e) {
// Weird
log.warn("Could not remove descriptor image folder: {}", e.getMessage());
}
}
}
private Path getRepositoryImageFolder(final Descriptor descriptor) {
assert (descriptor != null);
assert (descriptor.getUuid() != null);
String uuid = descriptor.getUuid().toString();
return Paths.get("/descriptor", uuid.substring(0, 3), uuid).toAbsolutePath();
}
/**
* Persist or update terms in descriptor itself. It updates descriptor's own
* terms List
*
* @param descriptor the descriptor
*/
protected void updateTerms(final Descriptor descriptor) {
final List<VocabularyTerm> terms = descriptor.getTerms();
if (terms != null && !terms.isEmpty()) {
terms.stream().collect(Collectors.groupingBy(VocabularyTerm::getCode))
.entrySet().stream().filter(e -> e.getValue().size() > 1).findFirst()
.ifPresent(e -> {
throw new InvalidApiUsageException("Terms with duplicate codes are not allowed.");
}
);
if (descriptor.getId() != null) {
var remainingTermIds = terms.stream().map(VocabularyTerm::getId).filter(id -> id != null).collect(Collectors.toSet());
var existedForRemove = termRepository.findAll(
QVocabularyTerm.vocabularyTerm.descriptor().id.eq(descriptor.getId())
.and(QVocabularyTerm.vocabularyTerm.id.notIn(remainingTermIds))
);
if (existedForRemove.iterator().hasNext()) {
termRepository.deleteAll(existedForRemove);
}
}
terms.forEach(term -> term.setOriginalLanguageTag(descriptor.getOriginalLanguageTag())); // Force set same language
final List<VocabularyTerm> r = termRepository.saveAllAndFlush(terms);
terms.clear();
log.debug("Adding terms {}", r);
terms.addAll(r);
}
}
/**
* {@inheritDoc}
*/
@Transactional
@Override
public Descriptor update(final Descriptor descriptor) {
final Descriptor loaded = getUnpublishedDescriptor(descriptor);
return update(descriptor, loaded);
}
@Override
@Transactional
public Descriptor update(Descriptor updated, Descriptor target) {
return lazyLoad(updateFast(updated, target));
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#target, 'WRITE')")
public Descriptor updateFast(Descriptor updated, Descriptor target) {
log.info("Updating descriptor uuid={} id={}", updated.getUuid(), updated.getId());
final Partner owner = target.getOwner();
if (updated.getOwner() != null && !target.getOwner().getUuid().equals(updated.getOwner().getUuid())) {
throw new InvalidApiUsageException("Descriptor owner can't be changed");
}
updateTerms(updated);
target.apply(updated);
// Keep owner
target.setOwner(owner);
target.setState(PublishState.DRAFT);
return descriptorRepository.save(target);
}
/**
* {@inheritDoc}
*/
@Transactional
@Override
@PreAuthorize("hasRole('ADMINISTRATOR')")
public Descriptor forceUpdateDescriptor(final Descriptor descriptor) {
final Descriptor loaded = getDescriptor(descriptor); // Editorial changes work regardless of state
log.info("Updating descriptor uuid={} id={}", descriptor.getUuid(), descriptor.getId());
final Partner owner = loaded.getOwner();
if (descriptor.getOwner() != null && !loaded.getOwner().equals(descriptor.getOwner())) {
throw new InvalidApiUsageException("Descriptor owner can't be changed");
}
updateTerms(descriptor);
loaded.apply(descriptor);
// Keep owner
loaded.setOwner(owner);
// loaded.setState(PublishState.DRAFT); // Editoral changes do not modify state
return lazyLoad(descriptorRepository.save(loaded));
}
/**
* {@inheritDoc}
*/
@Override
@PostAuthorize("hasRole('ADMINISTRATOR') || returnObject==null || returnObject.isPublished() || hasPermission(returnObject, 'write')")
public Descriptor getDescriptor(final UUID uuid) {
return descriptorRepository.findByUuid(uuid);
}
/**
* {@inheritDoc}
*/
@Override
@PostAuthorize("hasRole('ADMINISTRATOR') || returnObject==null || returnObject.isPublished() || hasPermission(returnObject, 'write')")
public Descriptor loadDescriptor(final UUID uuid) {
return lazyLoad(descriptorRepository.findByUuid(uuid));
}
@Override
@PostAuthorize("hasRole('ADMINISTRATOR') || returnObject==null || returnObject.entity.isPublished() || hasPermission(returnObject.entity, 'write')")
public DescriptorTranslationService.TranslatedDescriptor loadTranslatedDescriptor(UUID uuid) throws NotFoundElement {
return translate(descriptorRepository.findByUuid(uuid));
}
/**
* {@inheritDoc}
*/
@Override
@PostAuthorize("hasRole('ADMINISTRATOR') || returnObject==null || returnObject.isPublished() || hasPermission(returnObject, 'read')")
public Descriptor loadDescriptor(final UUID uuid, final int version) {
return lazyLoad(descriptorRepository.findByUuidAndVersion(uuid, version));
}
/**
* {@inheritDoc}
*/
@Override
@PreAuthorize("isAuthenticated()")
public Page<Descriptor> listDescriptorsForCurrentUser(final DescriptorFilter filter, final Pageable page) throws IOException, SearchException {
Pageable markdownSortPageRequest = JPAUtils.toMarkdownSort(page, "title");
Page<Descriptor> res;
if (securityUtils.hasRole(UserRole.ADMINISTRATOR)) {
if (filter.isFulltextQuery()) {
res = elasticsearchService.findAll(Descriptor.class, filter, markdownSortPageRequest);
} else {
res = descriptorRepository.findAll(filter.buildPredicate(), markdownSortPageRequest);
}
return new PageImpl<>(res.getContent(), page, res.getTotalElements());
} else {
final HashSet<Long> partners = new HashSet<>(securityUtils.listObjectIdentityIdsForCurrentUser(Partner.class, BasePermission.WRITE));
DescriptorFilter partnerFilter = filter.copy(DescriptorFilter.class);
partnerFilter.owner().id(partners);
if (filter.isFulltextQuery()) {
res = elasticsearchService.findAll(Descriptor.class, partnerFilter, markdownSortPageRequest);
} else {
res = descriptorRepository.findAll(partnerFilter.buildPredicate(), markdownSortPageRequest);
}
}
return new PageImpl<>(res.getContent(), page, res.getTotalElements());
}
@Override
@PreAuthorize("isAuthenticated()")
public Page<Descriptor> listAccessibleDescriptors(DescriptorFilter descriptorFilter, Pageable page) throws IOException, SearchException {
Pageable markdownSortPageRequest = JPAUtils.toMarkdownSort(page, "title");
Page<Descriptor> res;
if (securityUtils.hasRole(UserRole.ADMINISTRATOR)) {
res = listDescriptorsForCurrentUser(descriptorFilter, markdownSortPageRequest);
} else {
final HashSet<Long> partners = new HashSet<>(securityUtils.listObjectIdentityIdsForCurrentUser(Partner.class, BasePermission.WRITE));
BooleanExpression and = QDescriptor.descriptor.state.in(PublishState.PUBLISHED).or(descriptor.owner().id.in(partners)).and(descriptorFilter.buildPredicate());
if (descriptorFilter.isFulltextQuery()) {
and = and.andAnyOf(FilterHelpers.containsAll(descriptorFilter.get_text(), QDescriptor.descriptor.title, QDescriptor.descriptor.columnName, QDescriptor.descriptor.description, QDescriptor.descriptor.crop));
}
res = descriptorRepository.findAll(and, markdownSortPageRequest);
}
return new PageImpl<>(res.getContent(), page, res.getTotalElements());
}
/**
* {@inheritDoc}
*/
@Override
public Page<Descriptor> listDescriptors(final DescriptorFilter descriptorFilter, final Pageable page) throws SearchException {
Pageable markdownSortPageRequest = JPAUtils.toMarkdownSort(page, "title");
Page<Descriptor> res;
if (descriptorFilter.isFulltextQuery()) {
res = elasticsearchService.findAll(Descriptor.class, descriptorFilter, page);
} else {
res = descriptorRepository.findAll(new BooleanBuilder().and(descriptorFilter.buildPredicate()).and(QDescriptor.descriptor.state.in(PublishState.PUBLISHED)),
markdownSortPageRequest);
}
return new PageImpl<>(res.getContent(), page, res.getTotalElements());
}
@Override
public Page<TranslatedDescriptor> listDescriptorsDetails(DescriptorFilter filter, Pageable page) throws SearchException {
var descriptors = listDescriptors(filter, page);
descriptors.getContent().forEach(d -> d.getTerms().size());
return new PageImpl<>(translationSupport.getTranslated(descriptors.getContent()), page, descriptors.getTotalElements());
}
/**
* {@inheritDoc}
*
* @throws IOException
* @throws InvalidRepositoryPathException
* @throws FolderNotEmptyException
* @throws NoSuchRepositoryFileException
*/
@Transactional(rollbackFor = Throwable.class)
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#descriptor, 'delete')")
public Descriptor removeDescriptor(final Descriptor descriptor) throws IOException {
final Descriptor loadedDescriptor = getUnpublishedDescriptor(descriptor);
deleteDescriptorImage(loadedDescriptor);
descriptorRepository.delete(loadedDescriptor);
return loadedDescriptor;
}
@Transactional(rollbackFor = Throwable.class)
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#entity, 'delete')")
public Descriptor remove(Descriptor entity) {
try {
return removeDescriptor(entity);
} catch (Exception e) {
throw new InvalidApiUsageException(e);
}
}
/**
* Lazy load for objects in descriptor.
*
* @param descriptor descriptor
* @return descriptor with loaded inner objects
*/
private Descriptor lazyLoad(final Descriptor descriptor) {
if (descriptor == null) {
throw new NotFoundElement("No such descriptor");
}
descriptor.lazyLoad();
return descriptor;
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR')")
public List<Descriptor> upsertDescriptors(final List<Descriptor> sources) {
final List<Descriptor> updates = new ArrayList<>();
for (final Descriptor source : sources) {
updates.add(upsertDescriptor(source));
}
return updates;
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR')")
public Descriptor upsertDescriptor(final Descriptor source) {
Descriptor target;
if (source.getVersion() != null) {
target = getUnpublishedDescriptor(source);
} else {
target = new Descriptor();
target.setUuid(source.getUuid());
}
updateTerms(source);
target.apply(source);
return descriptorRepository.save(target);
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR')")
public Descriptor approveDescriptor(final Descriptor descriptor) {
final Descriptor loaded = getUnpublishedDescriptor(descriptor);
if (loaded.getState() == PublishState.DRAFT) {
throw new InvalidApiUsageException("Descriptor should be sent for review before publication");
}
loaded.setState(PublishState.PUBLISHED);
if (loaded.getImage() != null) {
// Relax permissions on descriptor image: allow USERS and ANONYMOUS to read the image
aclService.makePubliclyReadable(HibernateUtil.unproxy(loaded.getImage()), true);
}
return lazyLoad(descriptorRepository.saveAndFlush(loaded));
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#descriptor, 'administration')")
public Descriptor reviewDescriptor(final Descriptor descriptor) {
final Descriptor loaded = getUnpublishedDescriptor(descriptor);
if (loaded.getState() == PublishState.REVIEWING) {
throw new InvalidApiUsageException("The Descriptor is already under approval");
}
loaded.setState(PublishState.REVIEWING);
return lazyLoad(descriptorRepository.save(loaded));
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(isolation = Isolation.READ_UNCOMMITTED, noRollbackFor = InvalidApiUsageException.class)
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#descriptor, 'administration')")
public Descriptor rejectDescriptor(final Descriptor descriptor) {
final Descriptor loaded = getDescriptor(descriptor);
if (loaded.isPublished() && !securityUtils.hasRole(UserRole.ADMINISTRATOR)) {
if (loaded.getLastModifiedDate() != null && Instant.now().minus(24, ChronoUnit.HOURS).isAfter(loaded.getLastModifiedDate())) {
throw new InvalidApiUsageException("Cannot be un-published. More than 24 hours have passed since the publication.");
}
}
{
for (final Dataset referencedDataset : loaded.getDatasets()) {
if (referencedDataset.getState() == PublishState.PUBLISHED) {
throw new InvalidApiUsageException("Cannot be un-published. The descriptor is referenced by a published dataset.");
}
}
for (final DescriptorList referencedDescriptorList : loaded.getDescriptorLists()) {
if (referencedDescriptorList.getState() == PublishState.PUBLISHED) {
throw new InvalidApiUsageException("Cannot be un-published. The descriptor is referenced by a published descriptor list.");
}
}
}
loaded.setState(PublishState.DRAFT);
if (loaded.getImage() != null) {
// Tighten permissions on descriptor image
aclService.makePubliclyReadable(HibernateUtil.unproxy(loaded.getImage()), false);
}
return lazyLoad(descriptorRepository.save(loaded));
}
/**
* {@inheritDoc}
*/
@Override
@PreAuthorize("#descriptor.isPublished() || hasRole('ADMINISTRATOR') || hasPermission(#descriptor, 'read')")
@PostFilter("hasRole('ADMINISTRATOR') || filterObject==null || filterObject.isPublished() || hasPermission(filterObject, 'write')")
public List<DescriptorList> getDescriptorLists(final Descriptor descriptor) {
final List<DescriptorList> list = descriptorRepository.listDescriptorLists(descriptor);
list.forEach(d -> entityManager.detach(d));
return list;
}
/**
* {@inheritDoc}
*/
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') || #descriptor.isPublished() || hasPermission(#descriptor, 'read')")
@PostFilter("hasRole('ADMINISTRATOR') || filterObject==null || filterObject.isPublished() || hasPermission(filterObject, 'write')")
public List<Dataset> getDatasets(final Descriptor descriptor) {
final List<Dataset> list = descriptorRepository.listDatasets(descriptor);
list.forEach(d -> entityManager.detach(d));
return list;
}
@Override
@Transactional
@PreAuthorize("#descriptor.isPublished() || hasRole('ADMINISTRATOR') || hasPermission(#descriptor, 'read')")
public Descriptor nextVersion(final Descriptor descriptor, final boolean major) {
final Descriptor source = getDescriptor(descriptor);
log.info("Creating new version for descriptor uuid={} id={}", descriptor.getUuid(), descriptor.getId());
final Descriptor copy = new Descriptor();
copy.apply(source);
copy.setUuid(null);
copy.setState(PublishState.DRAFT);
copy.setVersionTag(versionManager.next(descriptor.getVersionTag(), major));
copy.setOwner(descriptor.getOwner());
return lazyLoad(descriptorRepository.save(copy));
}
@Override
public void exportDescriptors(DescriptorFilter filter, OutputStream outputStream) throws IOException {
List<Descriptor> descriptors = (List<Descriptor>) descriptorRepository.findAll(filter.buildPredicate());
downloadService.writeXlsxDescriptor(descriptors, outputStream);
}
@Override
public long countDescriptors(DescriptorFilter filter) throws SearchException {
if (filter.isFulltextQuery()) {
return elasticsearchService.count(DescriptorList.class, filter);
}
return descriptorRepository.count(filter.buildPredicate());
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR') || hasPermission(#original, 'write')")
public DescriptorLang machineTranslate(Descriptor 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 DescriptorLang();
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
@Transactional(propagation = Propagation.MANDATORY, readOnly = true)
@PreAuthorize("hasRole('ADMINISTRATOR') || hasPermission(#descriptor, 'write')")
public List<VocabularyTermLang> machineTranslateTerms(Descriptor descriptor, String targetLanguage) {
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(), descriptor.getOriginalLanguageTag())) {
var enTranslation = translationSupport.getTranslated(descriptor, Locale.ENGLISH);
if (enTranslation == null) {
throw new InvalidApiUsageException("English text is not available.");
}
builder
.sourceLang(enTranslation.getTranslation().getLanguageTag())
// .context(enTranslation.getDescription())
.texts(enTranslation.getTerms().stream().map(term -> {
return List.of(
Map.entry("title" + term.getEntity().getId(), new FormattedText(TextFormat.markdown, term.getTranslation().getTitle())),
Map.entry("description" + term.getEntity().getId(), new FormattedText(TextFormat.markdown, term.getTranslation().getDescription()))
);
}).flatMap(x -> x.stream()).filter(entry -> Objects.nonNull(entry.getValue().getText())).collect(Collectors.toMap(Entry::getKey, Entry::getValue)));
} else {
// Translations to English use the original texts
builder
.sourceLang(descriptor.getOriginalLanguageTag())
// .context(original.getDescription())
.texts(descriptor.getTerms().stream().map(term -> {
return List.of(
Map.entry("title" + term.getId(), new FormattedText(TextFormat.markdown, term.getTitle())),
Map.entry("description" + term.getId(), new FormattedText(TextFormat.markdown, term.getDescription()))
);
}).flatMap(x -> x.stream()).filter(entry -> Objects.nonNull(entry.getValue().getText())).collect(Collectors.toMap(Entry::getKey, Entry::getValue)));
}
try {
var translations = translatorService.translate(builder.build());
return descriptor.getTerms().stream().map(term -> {
var mt = new VocabularyTermLang();
mt.setEntity(term);
mt.setMachineTranslated(true);
mt.setLanguageTag(targetLanguage);
if (StringUtils.isNotBlank(term.getTitle())) {
mt.setTitle(translations.getTexts().get("title" + term.getId()));
}
if (StringUtils.isNotBlank(term.getDescription())) {
mt.setDescription(translations.getTexts().get("description" + term.getId()));
}
return mt;
}).filter(Objects::nonNull).collect(Collectors.toList());
} catch (TranslatorException e) {
return null;
}
}
}