VocabularyServiceImpl.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 java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.persistence.EntityManager;

import org.genesys.server.exception.InvalidApiUsageException;
import org.genesys.server.exception.NotFoundElement;
import org.genesys.server.model.filters.ControlledVocabularyFilter;
import org.genesys.server.model.vocab.ControlledVocabulary;
import org.genesys.server.model.vocab.VocabularyTerm;
import org.genesys.server.persistence.vocab.ControlledVocabularyRepository;
import org.genesys.server.persistence.vocab.VocabularyTermRepository;
import org.genesys.server.service.VocabularyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
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.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import com.querydsl.core.types.Predicate;

/**
 * The Class VocabularyServiceImpl.
 */
@Service
@Transactional(readOnly = true)
@Validated
public class VocabularyServiceImpl implements VocabularyService {
	private static final Logger LOG = LoggerFactory.getLogger(VocabularyServiceImpl.class);

	@Autowired
	private ControlledVocabularyRepository vocabRepository;

	@Autowired
	private VocabularyTermRepository termRepository;

	@Autowired
	private EntityManager entityManager;

	@Override
	@Transactional
	public ControlledVocabulary createVocabulary(final ControlledVocabulary input) {
		final ControlledVocabulary controlledVocabulary = new ControlledVocabulary();
		controlledVocabulary.setUuid(input.getUuid());
		controlledVocabulary.setTitle(input.getTitle());
		controlledVocabulary.setDescription(input.getDescription());
		controlledVocabulary.setVersionTag(input.getVersionTag());
		controlledVocabulary.setUrl(input.getUrl());
		controlledVocabulary.setTermUrlPrefix(input.getTermUrlPrefix());
		controlledVocabulary.setPublished(input.isPublished());
		controlledVocabulary.setOwner(input.getOwner());

		if (input.getTerms() != null && !input.getTerms().isEmpty()) {
			controlledVocabulary.setTerms(persistTerms(input.getTerms()));
		} else {
			controlledVocabulary.setTerms(new ArrayList<>());
		}
		return vocabRepository.save(controlledVocabulary);
	}

	/**
	 * Persist or update terms in vocabulary itself. It updates vocabulary own terms
	 * List
	 *
	 * @param vocabulary the vocabulary
	 * @return 
	 */
	protected List<VocabularyTerm> persistTerms(final List<VocabularyTerm> terms) {
		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."); });

		return termRepository.saveAll(terms);
	}

	@Override
	public ControlledVocabulary getVocabulary(final UUID uuid) {
		final ControlledVocabulary vocabulary = vocabRepository.getByUuid(uuid);
		return lazyLoad(vocabulary);
	}

	@Override
	public ControlledVocabulary getVocabulary(final UUID uuid, final int version) {
		return lazyLoad(vocabRepository.getByUuidAndVersion(uuid, version));
	}

	/**
	 * Lazy load.
	 *
	 * @param vocabulary the vocabulary
	 * @return the controlled vocabulary
	 */
	protected ControlledVocabulary lazyLoad(final ControlledVocabulary vocabulary) {
		if (vocabulary == null) {
			throw new NotFoundElement("No such vocabulary.");
		}

		// Do not load all terms, only first 50
		if (vocabulary.getTerms() != null) {
			vocabulary.setTerms(vocabRepository.listVocabularyTerms(vocabulary, PageRequest.of(0, 50)).getContent());
		}

		if (vocabulary.getOwner() != null) {
			vocabulary.getOwner().getId();
		}

		entityManager.detach(vocabulary);
		return vocabulary;
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#input, 'write')")
	public ControlledVocabulary updateVocabulary(final ControlledVocabulary input) {
		if (input.getUuid() == null || input.getVersion() == null) {
			throw new InvalidDataAccessApiUsageException("No uuid or version provided");
		}

		final ControlledVocabulary vocabulary = vocabRepository.getByUuidAndVersion(input.getUuid(), input.getVersion());

		if (vocabulary == null) {
			throw new ConcurrencyFailureException("Record with that version doesn't exist");
		}

		if (vocabulary.isPublished()) {
			throw new DataAccessResourceFailureException("Published vocabulary can't be updated");
		}

		vocabulary.setDescription(input.getDescription());
		vocabulary.setPublished(input.isPublished());
		vocabulary.setPublisher(input.getPublisher());
		vocabulary.setTitle(input.getTitle());
		vocabulary.setTermUrlPrefix(input.getTermUrlPrefix());
		vocabulary.setUrl(input.getUrl());
		vocabulary.setVersionTag(input.getVersionTag());

		if (input.getTerms() != null) {
			autoUpdateTerms(vocabulary, input.getTerms());
		}

		return vocabRepository.save(vocabulary);
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR')")
	public ControlledVocabulary autoUpdateOrCreateVocabulary(final UUID uuid, final ControlledVocabulary input) {
		final ControlledVocabulary oldVocabulary = vocabRepository.getByUuid(uuid);

		if (oldVocabulary != null) {
			LOG.info("Updating {} vocabulary with {} terms", oldVocabulary.getTitle(), input.getTerms().size());

			if (input.getTerms() != null) {
				autoUpdateTerms(oldVocabulary, input.getTerms());
			}

			return vocabRepository.save(oldVocabulary);
		} else {
			input.setUuid(uuid);
			LOG.info("Creating {} vocabulary with {} terms", input.getTitle(), input.getTerms().size());
			return createVocabulary(input);
		}
	}

	/**
	 * Maps existing term IDs to input.terms by term code (which is the primary key
	 * within a vocabulary)
	 *
	 * @param existing
	 * @param input
	 */
	private void autoUpdateTerms(ControlledVocabulary vocabulary, final List<VocabularyTerm> newTerms) {
		if (vocabulary.getTerms() == null) {
			vocabulary.setTerms(newTerms);
			persistTerms(vocabulary.getTerms());
			
		} else if (newTerms != null) {
			List<VocabularyTerm> existing = vocabulary.getTerms();
			LOG.info("Matching against {} existing terms", existing.size());
			
			// Remove missing ones
			List<VocabularyTerm> removedTerms = existing.stream().filter(old -> newTerms.stream().filter(term -> old.getCode().equals(term.getCode())).findFirst().orElse(null) == null).collect(Collectors.toList());

			termRepository.deleteAll(removedTerms);

			// match existing codes
			newTerms.forEach(inputTerm -> {
				// only when there's a code
				if (inputTerm.getCode() != null) {
					inputTerm.setId(existing.stream()
						// the ones with codes only
						.filter(existingTerm -> existingTerm != null && existingTerm.getCode() != null)
						// find matching term by code
						.filter(existingTerm -> existingTerm.getCode().equals(inputTerm.getCode()))
						// get it's ID
						.map(existingTerm -> existingTerm.getId())
						// get the ID of the existing term by code or null
						.findFirst().orElse(null));

					if (inputTerm.getId() == null) {
						LOG.debug("New vocabulary term {}", inputTerm);
					}
				}
			});
			
			vocabulary.setTerms(persistTerms(newTerms));
		}
	}

	@Override
	public Page<ControlledVocabulary> listVocabularies(final ControlledVocabularyFilter filters, final Pageable page) {
		Predicate predicate = filters.buildPredicate();
		if (predicate != null) {
			return vocabRepository.findAll(filters.buildPredicate(), page);
		} else {
			return vocabRepository.findAll(page);
		}
	}

	@Override
	@Transactional
	@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#input, 'delete')")
	public ControlledVocabulary deleteVocabulary(ControlledVocabulary vocabulary) {
		vocabulary = vocabRepository.getByUuidAndVersion(vocabulary.getUuid(), vocabulary.getVersion());

		if (vocabulary.isPublished()) {
			LOG.warn("Refusing to update a published vocabulary");
			throw new InvalidApiUsageException("Published vocabulary can't be updated");
		}

		vocabRepository.delete(vocabulary);
		return vocabulary;
	}

	@Override
	public VocabularyTerm getVocabularyTerm(final UUID vocabularyUuid, final String code) throws NotFoundElement {
		return vocabRepository.getVocabularyTerm(vocabularyUuid, code);
	}

	@Override
	public List<VocabularyTerm> autocompleteTerms(final UUID vocabularyUuid, final String text) {
		return vocabRepository.autocompleteVocabularyTerm(vocabularyUuid, text, PageRequest.of(0, 20, Sort.by("code")));
	}

	@Override
	public Page<VocabularyTerm> listTerms(final ControlledVocabulary vocabulary, final Pageable page) {
		return vocabRepository.listVocabularyTerms(vocabulary, page);
	}
}