DocumentTemplateAdminController.java

package com.taxonomy.templates;

import com.taxonomy.templates.DocumentTemplateGitRepository.TemplateDescriptor;
import com.taxonomy.templates.DocumentTemplateGitRepository.TemplateDiff;
import com.taxonomy.templates.DocumentTemplateGitRepository.TemplateRevision;
import com.taxonomy.templates.DocumentTemplateService.TemplateFile;
import com.taxonomy.templates.DocumentTemplateService.TemplatePartView;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.CacheControl;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.security.Principal;
import java.util.List;

/**
 * Administrative upload/download, history and package-inspection API.
 */
@RestController
@RequestMapping("/api/admin/document-templates")
public class DocumentTemplateAdminController {

    private final DocumentTemplateService templates;

    public DocumentTemplateAdminController(DocumentTemplateService templates) {
        this.templates = templates;
    }

    @GetMapping
    public List<TemplateDescriptor> list() throws IOException {
        return templates.list();
    }

    /**
     * Accepts the DOTX as the raw request body. This keeps the same bounded package
     * import path as WebDAV and avoids a second multipart buffering limit.
     */
    @PutMapping(
            value = "/{templateId}",
            consumes = {
                    OoxmlTemplatePackageCodec.DOTX_MEDIA_TYPE,
                    MediaType.APPLICATION_OCTET_STREAM_VALUE
            })
    public ResponseEntity<TemplateDescriptor> upload(
            @PathVariable String templateId,
            @RequestParam String displayName,
            @RequestHeader(value = HttpHeaders.IF_MATCH, required = false) String expectedHead,
            HttpServletRequest request,
            Principal principal) throws IOException {
        long contentLength = request.getContentLengthLong();
        if (contentLength == 0) {
            throw new IllegalArgumentException("Choose a non-empty DOTX file");
        }
        if (contentLength > OoxmlTemplatePackageCodec.MAX_ARCHIVE_BYTES) {
            throw new IllegalArgumentException("DOTX archive exceeds the permitted size");
        }
        TemplateDescriptor saved = templates.upload(
                templateId,
                displayName,
                request.getInputStream(),
                expectedHead,
                principal.getName(),
                "Upload document template " + templateId);
        return ResponseEntity.status(201)
                .eTag(etag(saved.headCommit()))
                .body(saved);
    }

    @GetMapping("/{templateId}/download")
    public ResponseEntity<byte[]> download(
            @PathVariable String templateId,
            @RequestParam(required = false) String revision) throws IOException {
        TemplateFile file = revision == null || revision.isBlank()
                ? templates.downloadCurrent(templateId)
                : templates.download(templateId, revision);
        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(file.manifest().mediaType()))
                .contentLength(file.content().length)
                .eTag(file.etag())
                .lastModified(file.lastModified().toEpochMilli())
                .cacheControl(CacheControl.noCache().cachePrivate())
                .header(HttpHeaders.CONTENT_DISPOSITION,
                        ContentDisposition.attachment()
                                .filename(file.manifest().fileName(),
                                        java.nio.charset.StandardCharsets.UTF_8)
                                .build()
                                .toString())
                .body(file.content());
    }

    @GetMapping("/{templateId}/history")
    public List<TemplateRevision> history(@PathVariable String templateId)
            throws IOException {
        return templates.history(templateId);
    }

    @GetMapping("/{templateId}/diff")
    public TemplateDiff diff(
            @PathVariable String templateId,
            @RequestParam String from,
            @RequestParam String to) throws IOException {
        return templates.diff(templateId, from, to);
    }

    @GetMapping("/{templateId}/part")
    public TemplatePartView part(
            @PathVariable String templateId,
            @RequestParam String revision,
            @RequestParam String path) throws IOException {
        return templates.readPart(templateId, revision, path);
    }

    @PostMapping("/{templateId}/restore")
    public ResponseEntity<TemplateDescriptor> restore(
            @PathVariable String templateId,
            @RequestParam String revision,
            @RequestHeader(value = HttpHeaders.IF_MATCH, required = false) String expectedHead,
            Principal principal) throws IOException {
        TemplateDescriptor restored = templates.restore(
                templateId,
                revision,
                expectedHead,
                principal.getName());
        return ResponseEntity.ok()
                .eTag(etag(restored.headCommit()))
                .body(restored);
    }

    private static String etag(String commitId) {
        return "\"" + commitId + "\"";
    }
}