RestApiTemplate.java

package com.bonitasoft.processbuilder.extension.template;

import com.bonitasoft.processbuilder.extension.template.auth.AuthConfig;
import com.bonitasoft.processbuilder.extension.template.auth.NoAuthConfig;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * Represents a REST API template configuration.
 * <p>
 * Use this class to create type-safe REST API configurations that can be
 * stored in PBConfiguration. This class supports both:
 * </p>
 * <ul>
 *   <li><b>REST API Configurations</b>: Actual API configurations with real credentials</li>
 *   <li><b>REST API Templates</b>: Template definitions with placeholders ({{baseUrl}}, {{token}}, etc.)</li>
 * </ul>
 * <p>
 * When {@code isTemplate} is true, the template includes additional metadata like
 * {@code templateVersion} and {@code requiredFields} that help the UI render
 * configuration forms for users to fill in placeholders.
 * </p>
 *
 * @author Process Builder Team
 * @since 2025-02-06
 */
public record RestApiTemplate(
        String name,
        String displayName,
        String description,
        String baseUrl,
        int timeoutMs,
        boolean verifySsl,
        AuthConfig auth,
        Map<String, String> headers,
        List<Method> methods,
        // Template-specific fields
        boolean isTemplate,
        String templateVersion,
        List<String> requiredFields
) {

    /**
     * Represents a method/endpoint in the REST API.
     */
    public record Method(
            String name,
            String displayName,
            String description,
            String httpMethod,
            String path,
            Map<String, String> queryParams,
            Map<String, String> headers,
            String bodyTemplate
    ) {
        public Method {
            if (httpMethod == null || httpMethod.isBlank()) httpMethod = "GET";
            if (path == null) path = "";
            queryParams = queryParams != null ? Map.copyOf(queryParams) : Map.of();
            headers = headers != null ? Map.copyOf(headers) : Map.of();
        }

        public Method(String name, String displayName, String path) {
            this(name, displayName, null, "GET", path, null, null, null);
        }

        public JsonNode toJson(ObjectMapper mapper) {
            ObjectNode node = mapper.createObjectNode();
            node.put("name", name);
            node.put("displayName", displayName != null ? displayName : name);
            if (description != null && !description.isBlank()) node.put("description", description);
            node.put("httpMethod", httpMethod);
            node.put("path", path);
            if (!queryParams.isEmpty()) {
                ObjectNode qp = mapper.createObjectNode();
                queryParams.forEach(qp::put);
                node.set("queryParams", qp);
            }
            if (!headers.isEmpty()) {
                ObjectNode h = mapper.createObjectNode();
                headers.forEach(h::put);
                node.set("headers", h);
            }
            if (bodyTemplate != null && !bodyTemplate.isBlank()) {
                node.put("bodyTemplate", bodyTemplate);
            }
            return node;
        }
    }

    public RestApiTemplate {
        Objects.requireNonNull(name, "Name cannot be null");
        Objects.requireNonNull(baseUrl, "Base URL cannot be null");
        if (displayName == null || displayName.isBlank()) displayName = name;
        if (timeoutMs <= 0) timeoutMs = 30000;
        if (auth == null) auth = NoAuthConfig.INSTANCE;
        headers = headers != null ? Map.copyOf(headers) : Map.of("Accept", "application/json", "Content-Type", "application/json");
        methods = methods != null ? List.copyOf(methods) : List.of();
        // Template-specific fields: default templateVersion to "2.0" for templates
        if (isTemplate && (templateVersion == null || templateVersion.isBlank())) {
            templateVersion = "2.0";
        }
        requiredFields = requiredFields != null ? List.copyOf(requiredFields) : List.of();
    }

    /**
     * Converts this template to JSON format for storage in PBConfiguration.configValue.
     * <p>
     * When {@code isTemplate} is true, includes template metadata (isTemplate, templateVersion, requiredFields).
     * </p>
     */
    public JsonNode toJson(ObjectMapper mapper) {
        ObjectNode root = mapper.createObjectNode();
        // Template-specific fields at the top when this is a template definition
        if (isTemplate) {
            root.put("isTemplate", true);
            root.put("templateVersion", templateVersion != null ? templateVersion : "2.0");
        }
        root.put("baseUrl", baseUrl);
        root.put("timeoutMs", timeoutMs);
        root.put("verifySsl", verifySsl);
        if (!headers.isEmpty()) {
            ObjectNode h = mapper.createObjectNode();
            headers.forEach(h::put);
            root.set("headers", h);
        }
        root.set("auth", auth.toJson(mapper));
        // Required fields for templates (tells UI which fields user must fill)
        if (isTemplate && !requiredFields.isEmpty()) {
            ArrayNode rf = mapper.createArrayNode();
            for (String field : requiredFields) rf.add(field);
            root.set("requiredFields", rf);
        }
        if (!methods.isEmpty()) {
            ArrayNode methodsArray = mapper.createArrayNode();
            for (Method method : methods) methodsArray.add(method.toJson(mapper));
            root.set("methods", methodsArray);
        }
        return root;
    }

    /**
     * Converts this template to JSON format with encrypted sensitive fields.
     * <p>
     * When {@code isTemplate} is true, includes template metadata (isTemplate, templateVersion, requiredFields).
     * Note: Templates with placeholders don't need encryption since they don't contain real credentials.
     * </p>
     */
    public JsonNode toJsonEncrypted(ObjectMapper mapper) {
        ObjectNode root = mapper.createObjectNode();
        // Template-specific fields at the top when this is a template definition
        if (isTemplate) {
            root.put("isTemplate", true);
            root.put("templateVersion", templateVersion != null ? templateVersion : "2.0");
        }
        root.put("baseUrl", baseUrl);
        root.put("timeoutMs", timeoutMs);
        root.put("verifySsl", verifySsl);
        if (!headers.isEmpty()) {
            ObjectNode h = mapper.createObjectNode();
            headers.forEach(h::put);
            root.set("headers", h);
        }
        root.set("auth", auth.toJsonEncrypted(mapper));
        // Required fields for templates
        if (isTemplate && !requiredFields.isEmpty()) {
            ArrayNode rf = mapper.createArrayNode();
            for (String field : requiredFields) rf.add(field);
            root.set("requiredFields", rf);
        }
        if (!methods.isEmpty()) {
            ArrayNode methodsArray = mapper.createArrayNode();
            for (Method method : methods) methodsArray.add(method.toJson(mapper));
            root.set("methods", methodsArray);
        }
        return root;
    }

    /**
     * Converts this template to a JSON string for storage.
     */
    public String toJsonString(ObjectMapper mapper) {
        try {
            return mapper.writeValueAsString(toJson(mapper));
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize template to JSON", e);
        }
    }

    /**
     * Converts this template to a JSON string with encrypted sensitive fields.
     */
    public String toJsonStringEncrypted(ObjectMapper mapper) {
        try {
            return mapper.writeValueAsString(toJsonEncrypted(mapper));
        } catch (Exception e) {
            throw new RuntimeException("Failed to serialize template to JSON", e);
        }
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private String name;
        private String displayName;
        private String description;
        private String baseUrl;
        private int timeoutMs = 30000;
        private boolean verifySsl = true;
        private AuthConfig auth = NoAuthConfig.INSTANCE;
        private final Map<String, String> headers = new LinkedHashMap<>();
        private final List<Method> methods = new ArrayList<>();
        // Template-specific fields
        private boolean isTemplate = false;
        private String templateVersion = "2.0";
        private final List<String> requiredFields = new ArrayList<>();

        public Builder name(String name) { this.name = name; return this; }
        public Builder displayName(String displayName) { this.displayName = displayName; return this; }
        public Builder description(String description) { this.description = description; return this; }
        public Builder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; }
        public Builder timeoutMs(int timeoutMs) { this.timeoutMs = timeoutMs; return this; }
        public Builder verifySsl(boolean verifySsl) { this.verifySsl = verifySsl; return this; }
        public Builder auth(AuthConfig auth) { this.auth = auth; return this; }
        public Builder header(String name, String value) { this.headers.put(name, value); return this; }
        public Builder headers(Map<String, String> headers) { this.headers.putAll(headers); return this; }
        public Builder addMethod(Method method) { this.methods.add(method); return this; }
        public Builder addMethod(String name, String httpMethod, String path) {
            return addMethod(new Method(name, name, null, httpMethod, path, null, null, null));
        }
        public Builder addMethod(String name, String httpMethod, String path, Map<String, String> queryParams) {
            return addMethod(new Method(name, name, null, httpMethod, path, queryParams, null, null));
        }

        /**
         * Adds a method with full configuration including displayName, description, and body template.
         *
         * @param name         Method identifier
         * @param displayName  Human-readable name
         * @param description  Method description
         * @param httpMethod   HTTP method (GET, POST, PUT, DELETE, PATCH)
         * @param path         Endpoint path (can include placeholders like {{id}})
         * @param queryParams  Query parameters (can include placeholders)
         * @param bodyTemplate Body template (can include placeholders like {{field}})
         * @return this builder
         */
        public Builder addMethod(String name, String displayName, String description,
                                 String httpMethod, String path,
                                 Map<String, String> queryParams, String bodyTemplate) {
            return addMethod(new Method(name, displayName, description, httpMethod, path, queryParams, null, bodyTemplate));
        }

        /**
         * Marks this as a template definition (includes isTemplate, templateVersion, requiredFields in JSON).
         *
         * @return this builder
         */
        public Builder asTemplate() {
            this.isTemplate = true;
            return this;
        }

        /**
         * Sets the template version (default: "2.0").
         *
         * @param version Template version string
         * @return this builder
         */
        public Builder templateVersion(String version) {
            this.templateVersion = version;
            return this;
        }

        /**
         * Adds a required field that users must fill when using this template.
         *
         * @param fieldName Name of the required field (e.g., "baseUrl", "token", "username")
         * @return this builder
         */
        public Builder requiredField(String fieldName) {
            this.requiredFields.add(fieldName);
            return this;
        }

        /**
         * Adds multiple required fields.
         *
         * @param fields List of required field names
         * @return this builder
         */
        public Builder requiredFields(List<String> fields) {
            this.requiredFields.addAll(fields);
            return this;
        }

        /**
         * Adds multiple required fields (varargs version).
         *
         * @param fields Required field names
         * @return this builder
         */
        public Builder requiredFields(String... fields) {
            for (String field : fields) this.requiredFields.add(field);
            return this;
        }

        public RestApiTemplate build() {
            if (headers.isEmpty()) {
                headers.put("Accept", "application/json");
                headers.put("Content-Type", "application/json");
            }
            return new RestApiTemplate(name, displayName, description, baseUrl, timeoutMs, verifySsl, auth,
                    Collections.unmodifiableMap(new LinkedHashMap<>(headers)),
                    Collections.unmodifiableList(new ArrayList<>(methods)),
                    isTemplate, templateVersion,
                    Collections.unmodifiableList(new ArrayList<>(requiredFields)));
        }
    }
}