DocumentTemplatePartComparisonController.java

package com.taxonomy.templates;

import com.taxonomy.templates.DocumentTemplateGitRepository.TemplateNotFoundException;
import com.taxonomy.templates.DocumentTemplateService.TemplatePartView;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;

/** Read-only comparison using the existing validated, canonical template service. */
@Controller
public final class DocumentTemplatePartComparisonController {
    private final DocumentTemplateService templates;

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

    @GetMapping("/admin/document-templates/{templateId}/compare-part")
    public String comparePart(
            @PathVariable String templateId,
            @RequestParam String from,
            @RequestParam String to,
            @RequestParam String partPath,
            Model model) throws IOException {
        try {
            from = DocumentTemplateService.canonicalComparisonRevision(from);
            to = DocumentTemplateService.canonicalComparisonRevision(to);
        } catch (IllegalArgumentException exception) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "Two immutable template revisions are required", exception);
        }
        try {
            OoxmlTemplatePackageCodec.validatePartPath(partPath);
        } catch (IllegalArgumentException exception) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
                    "A relative package-part path is required", exception);
        }
        try {
            var comparison = templates.comparePart(templateId, from, to, partPath);
            var change = comparison.change();
            TemplatePartView before = comparison.before();
            TemplatePartView after = comparison.after();
            model.addAttribute("templateId", templateId);
            model.addAttribute("fromRevision", from);
            model.addAttribute("toRevision", to);
            model.addAttribute("partPath", partPath);
            model.addAttribute("partChange", change == null ? "UNCHANGED" : change.name());
            model.addAttribute("beforePart", before);
            model.addAttribute("afterPart", after);
            if (tooLarge(before) || tooLarge(after)) {
                model.addAttribute("comparisonMode", "LIMIT");
            } else if (notText(before) || notText(after)) {
                model.addAttribute("comparisonMode", "BINARY");
            } else {
                var result = TemplateTextDiff.compare(text(before), text(after));
                model.addAttribute("comparisonMode", result.limited() ? "LIMIT" : "TEXT");
                model.addAttribute("comparisonRows", result.rows());
                model.addAttribute("comparisonBlocks", result.blocks());
            }
            return "document-template-part-comparison";
        } catch (TemplateNotFoundException exception) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND,
                    "The selected template revision or package part does not exist", exception);
        }
    }

    private static boolean tooLarge(TemplatePartView part) {
        return part != null && part.size() > DocumentTemplateService.COMPARISON_TEXT_PREVIEW_BYTES;
    }

    private static boolean notText(TemplatePartView part) {
        // Exact UTF-8 decoding is decided by the service. NUL remains an explicit binary/UTF-16 signal.
        return part != null && (part.textContent() == null
                || part.textContent().indexOf('\0') >= 0);
    }

    private static String text(TemplatePartView part) {
        return part == null ? "" : part.textContent();
    }
}