package io.pazuzu.registry.feature; import io.pazuzu.registry.exception.*; import org.springframework.beans.factory.annotation.Autowired; 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.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import io.pazuzu.registry.exception.*; import javax.persistence.criteria.Predicate; import javax.ws.rs.BadRequestException; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @Service public class FeatureService { private final FeatureRepository featureRepository; @Autowired public FeatureService(FeatureRepository featureRepository) { this.featureRepository = featureRepository; } private static void collectRecursively(Collection<Feature> result, Feature f) { result.add(f); f.getDependencies().forEach(item -> collectRecursively(result, item)); } @Transactional(rollbackFor = ServiceException.class) public <T> T createFeature(String name, String description, String author, String snippet, String testSnippet, List<String> dependencyNames, Function<Feature, T> converter) { final Feature newFeature = new Feature(); setFeatureName(name, newFeature); createDependencies(dependencyNames, newFeature); if (null != snippet && !snippet.isEmpty()) { newFeature.setSnippet(snippet); } if (null != testSnippet && !testSnippet.isEmpty()) { newFeature.setTestSnippet(testSnippet); } if (null != description && !description.isEmpty()) { newFeature.setDescription(description); } if (null != author && !author.isEmpty()) { newFeature.setAuthor(author); } newFeature.setStatus(FeatureStatus.PENDING); featureRepository.save(newFeature); return converter.apply(newFeature); } private void setFeatureName(String name, Feature feature) throws BadRequestException { nameGuardCheck(name); feature.setName(name); } private void createDependencies(List<String> dependencyNames, Feature newFeature) { final Set<Feature> dependencies = loadFeatures(dependencyNames); newFeature.setDependencies(dependencies); } private void nameGuardCheck(String name) throws BadRequestException { if (StringUtils.isEmpty(name)) { throw new FeatureNameEmptyException(); } tryLoadExistingFeature(name).ifPresent(f -> { throw new FeatureDuplicateException(String.format("Feature with name %s already exists", f.getName())); }); } @Transactional(rollbackFor = ServiceException.class) public <T> T updateFeature(String name, String newName, String description, String author, String snippet, String testSnippet, List<String> dependencyNames, FeatureStatus status, Function<Feature, T> converter) { final Feature existing = loadExistingFeature(name); if (null != newName && !newName.equals(existing.getName())) { setFeatureName(newName, existing); } // Allow deleting nullable data existing.setSnippet(valueOrNull(snippet)); existing.setTestSnippet(valueOrNull(testSnippet)); existing.setDescription(valueOrNull(description)); existing.setAuthor(valueOrNull(author)); existing.setStatus(status); if (null != dependencyNames) { final Set<Feature> dependencies = loadFeatures(dependencyNames); final List<Feature> recursive = dependencies.stream() .filter(f -> f.containsDependencyRecursively(existing)).collect(Collectors.toList()); if (!recursive.isEmpty()) { throw new FeatureRecursiveDependencyException( "Recursive dependencies found: " + recursive.stream().map(Feature::getName).collect(Collectors.joining(", "))); } existing.setDependencies(dependencies); } featureRepository.save(existing); return converter.apply(existing); } private String valueOrNull(String value) { return null != value && !value.isEmpty() ? value : null; } @Transactional public <T> T getFeature(String featureName, Function<Feature, T> converter) throws ServiceException { return converter.apply(loadExistingFeature(featureName)); } @Transactional(rollbackFor = ServiceException.class) public void deleteFeature(String featureName) throws ServiceException { final Feature feature = loadExistingFeature(featureName); final List<Feature> referencing = featureRepository.findByDependenciesContaining(feature); if (!referencing.isEmpty()) { throw new FeatureReferencedDeleteException( "Can't delete feature because it is referenced from other feature(s): " + referencing.stream().map(Feature::getName).collect(Collectors.joining(", "))); } featureRepository.delete(feature); } public Set<Feature> loadFeatures(List<String> featureNames) throws ServiceException { if (featureNames == null || featureNames.isEmpty()) return Collections.emptySet(); // no need to go to database for this final Set<String> uniqueFeatureNames = new HashSet<>( featureNames.stream().map(t -> safeToLowerCase(t)).collect(Collectors.toList())); Specification<Feature> spec = (root, query, builder) -> { List<Predicate> predicates = new ArrayList<>(); if (uniqueFeatureNames.size() > 1) predicates.add(builder.lower(root.get(Feature_.name)).in(uniqueFeatureNames)); else uniqueFeatureNames.stream().findFirst().map(fn -> predicates.add(builder.equal(builder.lower(root.get(Feature_.name)), fn)) ); return builder.and(predicates.toArray(new Predicate[predicates.size()])); }; final Collection<Feature> foundFeatures = featureRepository.findAll(spec); if (foundFeatures.size() != uniqueFeatureNames.size()) { final Set<String> missingFeaturesNames = new HashSet<>(uniqueFeatureNames); foundFeatures.forEach(f -> missingFeaturesNames.remove(f.getName())); throw new FeatureNotFoundException("Feature missing: " + String.join(",", missingFeaturesNames)); } return new HashSet<>(foundFeatures); } /** * There are Locales, which might have troubles without specifying it here. For example the Turkish Language * has 4 letters 'I'. Since we assume only English Locale, we specify it here. * * @param t * @return String */ private String safeToLowerCase(String t) { return t.toLowerCase(Locale.ENGLISH); } /** * Search feature base on give search criteria, ordered by name. * @param name string the name should contains. * @param author string the author should contains. * @param status the status of the feature, if not present do no filter on status. * @param offset the offset of the result list (must be present) * @param limit the maximum size of the returned list (must be present) * @param converter the converter that will be used to map the feature to the expected result type * @param <T> the list item result type * @return paginated list of feature that match the search criteria with the totcal count. * @throws ServiceException */ @Transactional public <T> FeaturesPage<Feature, T> searchFeatures(String name, String author, FeatureStatus status, Integer offset, Integer limit, Function<Feature, T> converter) throws ServiceException { Specification<Feature> spec = (root, query, builder) -> { List<Predicate> predicates = new ArrayList<>(); if (name != null) { predicates.add(builder.like(builder.lower(root.get(Feature_.name)), escapeLike(name), '|' )); } if (author != null) { predicates.add(builder.like(builder.lower(root.get(Feature_.author)), escapeLike(author), '|')); } if (status != null) { predicates.add(builder.equal(root.get(Feature_.status), status)); } return builder.and(predicates.toArray(new Predicate[predicates.size()])); }; Pageable pageable = new PageRequest(offset / limit, limit, Sort.Direction.ASC, "name"); Page<Feature> page = featureRepository.findAll(spec, pageable); return new FeaturesPage<>(page, converter); } private String escapeLike(String name) { return "%" + safeToLowerCase(name.replace("%", "|%")) + "%"; } public List<Feature> resolveFeatures(List<String> featureNames) { final Set<Feature> expandedList = new HashSet<>(); loadFeatures(featureNames).forEach(f -> collectRecursively(expandedList, f)); return new ArrayList<>(expandedList); } private Feature loadExistingFeature(String featureName) throws FeatureNotFoundException { Optional<Feature> optionalFeature = tryLoadExistingFeature(featureName); return optionalFeature .orElseThrow(() -> new FeatureNotFoundException("Feature missing: " + featureName)); } private Optional<Feature> tryLoadExistingFeature(String featureName) { if (featureName == null || featureName.isEmpty()) throw new FeatureNameEmptyException(); Specification<Feature> spec = (root, query, builder) -> builder.equal(builder.lower(root.get(Feature_.name)), safeToLowerCase(featureName)); return Optional.ofNullable((Feature) featureRepository.findOne(spec)); } }