TaggingService.java

package jasper.service;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.fge.jsonpatch.JsonPatchException;
import com.github.fge.jsonpatch.Patch;
import io.micrometer.core.annotation.Timed;
import jasper.component.ConfigCache;
import jasper.component.Ingest;
import jasper.component.Tagger;
import jasper.component.Validate;
import jasper.domain.Plugin;
import jasper.errors.DuplicateTagException;
import jasper.errors.InvalidPatchException;
import jasper.errors.ModifiedException;
import jasper.errors.NotFoundException;
import jasper.repository.RefRepository;
import jasper.security.Auth;
import jasper.service.dto.DtoMapper;
import jasper.service.dto.RefDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.List;

import static org.apache.commons.lang3.StringUtils.isNotBlank;

@Service
public class TaggingService {
	private static final Logger logger = LoggerFactory.getLogger(TaggingService.class);

	@Autowired
	RefRepository refRepository;

	@Autowired
	ConfigCache configs;

	@Autowired
	Ingest ingest;

	@Autowired
	Tagger tagger;

	@Autowired
	Auth auth;

	@Autowired
	DtoMapper mapper;

	@Autowired
	Validate validate;

	@PreAuthorize("@auth.canTag(#tag, #url, #origin)")
	@Timed(value = "jasper.service", extraTags = {"service", "tag"}, histogram = true)
	public Instant create(String tag, String url, String origin) {
		var maybeRef = refRepository.findOneByUrlAndOrigin(url, origin);
		if (maybeRef.isEmpty()) throw new NotFoundException("Ref " + origin + " " + url);
		var ref = maybeRef.get();
		if (ref.hasTag(tag)) throw new DuplicateTagException(tag);
		ref.addTag(tag);
		ingest.update(auth.getOrigin(), ref);
		return ref.getModified();
	}

	@PreAuthorize("@auth.canUntag(#tag, #url, #origin)")
	@Timed(value = "jasper.service", extraTags = {"service", "tag"}, histogram = true)
	public Instant delete(String tag, String url, String origin) {
		if (tag.equals("locked")) {
			throw new AccessDeniedException("Cannot unlock Ref");
		}
		var maybeRef = refRepository.findOneByUrlAndOrigin(url, origin);
		if (maybeRef.isEmpty()) throw new NotFoundException("Ref " + origin + " " + url);
		var ref = maybeRef.get();
		if (!ref.hasTag(tag)) return ref.getModified();
		if (ref.hasTag("locked") && ref.hasPlugin(tag)) {
			throw new AccessDeniedException("Cannot untag locked Ref with plugin data");
		}
		ref.removeTag(tag);
		ingest.update(auth.getOrigin(), ref);
		return ref.getModified();
	}

	@PreAuthorize("@auth.canPatchTags(#tags, #url, #origin)")
	@Timed(value = "jasper.service", extraTags = {"service", "tag"}, histogram = true)
	public Instant tag(List<String> tags, String url, String origin) {
		if (tags.contains("-locked")) {
			throw new AccessDeniedException("Cannot unlock Ref");
		}
		var maybeRef = refRepository.findOneByUrlAndOrigin(url, origin);
		if (maybeRef.isEmpty()) throw new NotFoundException("Ref " + origin + " " + url);
		var ref = maybeRef.get();
		if (ref.hasTag("locked")) {
			for (var t : tags) {
				if (t.startsWith("-") && ref.hasPlugin(t.substring(1))) {
					throw new AccessDeniedException("Cannot untag locked Ref with plugin data");
				}
			}
		}
		ref.addTags(tags);
		ingest.update(auth.getOrigin(), ref);
		return ref.getModified();
	}

	@PreAuthorize("@auth.isLoggedIn() and @auth.hasRole('USER')")
	@Timed(value = "jasper.service", extraTags = {"service", "tag"}, histogram = true)
	public RefDto getResponse(String url) {
		var ref = tagger.getResponseRef(auth.getUserTag().tag, auth.getOrigin(), url);
		return mapper.domainToDto(ref);
	}

	@PreAuthorize("@auth.isLoggedIn() and @auth.hasRole('USER') and @auth.canAddTag(#tag)")
	@Timed(value = "jasper.service", extraTags = {"service", "tag"}, histogram = true)
	public void createResponse(String tag, String url) {
		var ref = tagger.getResponseRef(auth.getUserTag().tag, auth.getOrigin(), url);
		if (isNotBlank(tag) && !ref.hasTag(tag)) {
			ref.addTag(tag);
			try {
				ingest.updateResponse(auth.getOrigin(), ref);
			} catch (ModifiedException e) {
				// TODO: infinite retrys?
				createResponse(tag, url);
			}
		}
	}

	@PreAuthorize("@auth.isLoggedIn() and @auth.hasRole('USER') and @auth.canAddTag(#tag)")
	@Timed(value = "jasper.service", extraTags = {"service", "tag"}, histogram = true)
	public void deleteResponse(String tag, String url) {
		var ref = tagger.getResponseRef(auth.getUserTag().tag, auth.getOrigin(), url);
		ref.removeTag(tag);
		try {
			ingest.updateResponse(auth.getOrigin(), ref);
		} catch (ModifiedException e) {
			// TODO: infinite retrys?
			deleteResponse(tag, url);
		}
	}

	@PreAuthorize("@auth.isLoggedIn() and @auth.hasRole('USER') and @auth.canPatchTags(#tags)")
	@Timed(value = "jasper.service", extraTags = {"service", "tag"}, histogram = true)
	public void respond(List<String> tags, String url, Patch patch) {
		var ref = tagger.getResponseRef(auth.getUserTag().tag, auth.getOrigin(), url);
		for (var tag : tags) {
			var newTag = !tag.startsWith("-") && !ref.hasTag(tag);
			ref.addTag(tag);
			if (newTag) {
				configs.getPlugin(tag, auth.getOrigin())
					.map(Plugin::getDefaults)
					.ifPresent(defaults -> ref.setPlugin(tag, defaults));
			}
		}
		if (patch != null) {
			try {
				var plugins = (ObjectNode) patch.apply(ref.getPlugins() == null ? validate.pluginDefaults(auth.getOrigin(), ref) : ref.getPlugins());
				ref.addPlugins(ref.getTags(), plugins);
			} catch (JsonPatchException e) {
				throw new InvalidPatchException("Ref " + auth.getOrigin() + " " + url, e);
			}
		}
		try {
			ingest.updateResponse(auth.getOrigin(), ref);
		} catch (ModifiedException e) {
			// TODO: infinite retrys?
			respond(tags, url, patch);
		}
	}
}