/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.struts2.json;

import org.apache.struts2.action.Action;
import org.apache.struts2.action.ParameterNameAware;
import org.apache.struts2.action.ParameterValueAware;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.inject.Container;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.interceptor.AbstractInterceptor;
import org.apache.struts2.interceptor.parameter.ParameterAuthorizer;
import org.apache.struts2.security.AcceptedPatternsChecker;
import org.apache.struts2.security.ExcludedPatternsChecker;
import org.apache.struts2.util.TextParseUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.WildcardUtil;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.dispatcher.PrepareOperations;
import org.apache.struts2.json.annotations.SMDMethod;
import org.apache.struts2.json.rpc.RPCError;
import org.apache.struts2.json.rpc.RPCErrorCode;
import org.apache.struts2.json.rpc.RPCResponse;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.beans.IntrospectionException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.*;
import java.util.regex.Pattern;

/**
 * Populates an action from a JSON string
 */
public class JSONInterceptor extends AbstractInterceptor {

    private static final long serialVersionUID = 4950170304212158803L;
    private static final Logger LOG = LogManager.getLogger(JSONInterceptor.class);

    private boolean enableSMD = false;
    private boolean enableGZIP = false;
    private boolean wrapWithComments;
    private boolean prefix;
    private String defaultEncoding = "UTF-8";
    private boolean ignoreHierarchy = true;
    private String root;
    private List<Pattern> excludeProperties;
    private List<Pattern> includeProperties;
    private boolean ignoreSMDMethodInterfaces = true;
    private JSONPopulator populator = new JSONPopulator();
    private JSONCleaner dataCleaner = null;
    private boolean debug = false;
    private boolean noCache = false;
    private boolean excludeNullProperties;
    private String callbackParameter;
    private String jsonContentType = "application/json";
    private String jsonRpcContentType = "application/json-rpc";

    private Container container;
    private ParameterAuthorizer parameterAuthorizer;
    private ExcludedPatternsChecker excludedPatterns;
    private AcceptedPatternsChecker acceptedPatterns;
    private int maxElements = JSONReader.DEFAULT_MAX_ELEMENTS;
    private int maxDepth = JSONReader.DEFAULT_MAX_DEPTH;
    private int maxLength = 2_097_152;  // 2MB
    private int maxStringLength = JSONReader.DEFAULT_MAX_STRING_LENGTH;
    private int maxKeyLength = JSONReader.DEFAULT_MAX_KEY_LENGTH;
    private int paramNameMaxLength = 100;
    private Set<Pattern> excludedValuePatterns;
    private Set<Pattern> acceptedValuePatterns;
    private boolean applyPropertyFiltersToInput = false;

    @SuppressWarnings("unchecked")
    public String intercept(ActionInvocation invocation) throws Exception {
        HttpServletRequest request = ServletActionContext.getRequest();
        HttpServletResponse response = ServletActionContext.getResponse();

        String requestContentType = readContentType(request);
        String requestContentTypeEncoding = readContentTypeEncoding(request);

        Object rootObject = null;
        final ValueStack stack = invocation.getStack();
        if (this.root != null) {
            rootObject = stack.findValue(this.root);

            if (rootObject == null) {
                throw new RuntimeException("Invalid root expression: '" + this.root + "'.");
            }
        }

        if (jsonContentType.equalsIgnoreCase(requestContentType)) {
            // load JSON object
            JSONUtil jsonUtil = getJSONUtil();
            applyLimitsToReader(jsonUtil);
            Object obj = jsonUtil.deserializeInput(request.getReader(), maxLength);

            // JSON array (this.root cannot be null in this case)
            if(obj instanceof List && this.root != null) {
                String mapKey = this.root;
                rootObject = null;

                if(this.root.indexOf('.') != -1) {
                    mapKey = this.root.substring(this.root.lastIndexOf('.') + 1);

                    rootObject = stack.findValue(this.root.substring(0, this.root.lastIndexOf('.')));
                    if (rootObject == null) {
                        throw new RuntimeException("JSON array: Invalid root expression: '" + this.root + "'.");
                    }
                }

                // create a map with a list inside
                Map m = new HashMap();
                m.put(mapKey, new ArrayList((List) obj));
                obj = m;
            }

            if (obj instanceof Map) {
                Map json = (Map) obj;

                // clean up the values
                if (dataCleaner != null)
                    dataCleaner.clean("", json);

                if (rootObject == null) // model overrides action
                    rootObject = invocation.getStack().peek();

                // enforce name/value acceptability (patterns, length, *Aware) and @StrutsParameter authorization on JSON body keys
                filterUnacceptableKeys(json, rootObject, invocation.getAction());

                // populate fields
                populator.populateObject(rootObject, json);
            } else {
                LOG.error("Unable to deserialize JSON object from request");
                throw new JSONException("Unable to deserialize JSON object from request");
            }
        } else if (jsonRpcContentType.equalsIgnoreCase(requestContentType)) {
            JSONUtil jsonUtil = getJSONUtil();
            Object result;
            if (this.enableSMD) {
                // load JSON object
                applyLimitsToReader(jsonUtil);
                Object obj = jsonUtil.deserializeInput(request.getReader(), maxLength);

                if (obj instanceof Map) {
                    Map smd = (Map) obj;

                    if (rootObject == null) { // model makes no sense when using RPC
                        rootObject = invocation.getAction();
                    }

                    // invoke method
                    try {
                        result = this.invoke(rootObject, smd);
                    } catch (Exception e) {
                        RPCResponse rpcResponse = new RPCResponse();
                        rpcResponse.setId(smd.get("id").toString());
                        rpcResponse.setError(new RPCError(e, RPCErrorCode.EXCEPTION, getDebug()));

                        result = rpcResponse;
                    }
                } else {
                    String message = "SMD request was not in the right format. See http://json-rpc.org";

                    RPCResponse rpcResponse = new RPCResponse();
                    rpcResponse.setError(new RPCError(message, RPCErrorCode.INVALID_PROCEDURE_CALL));
                    result = rpcResponse;
                }
            } else {
                String message = "Request with content type of 'application/json-rpc' was received but SMD is "
                        + "not enabled for this interceptor. Set 'enableSMD' to true to enable it";

                RPCResponse rpcResponse = new RPCResponse();
                rpcResponse.setError(new RPCError(message, RPCErrorCode.SMD_DISABLED));
                result = rpcResponse;
            }

            String json = jsonUtil.serialize(result, excludeProperties, getIncludeProperties(),
                    ignoreHierarchy, excludeNullProperties);
            json = addCallbackIfApplicable(request, json);
            boolean writeGzip = enableGZIP && JSONUtil.isGzipInRequest(request);
            JSONUtil.writeJSONToResponse(new SerializationParams(response, requestContentTypeEncoding,
                    this.wrapWithComments, json, true, writeGzip, noCache, -1, -1, prefix, "application/json"));

            return Action.NONE;
        } else {
            LOG.debug("Accept header parameter must be '{}' or '{}'. Ignoring request with Content Type '{}'", jsonContentType, jsonRpcContentType, requestContentType);
        }

        return invocation.invoke();
    }

    private void applyLimitsToReader(JSONUtil jsonUtil) {
        JSONReader reader = jsonUtil.getReader();
        reader.setMaxElements(maxElements);
        reader.setMaxDepth(maxDepth);
        reader.setMaxStringLength(maxStringLength);
        reader.setMaxKeyLength(maxKeyLength);
    }

    @SuppressWarnings("rawtypes")
    private void filterUnacceptableKeys(Map json, Object target, Object action) {
        filterUnacceptableKeysRecursive(json, "", target, action);
    }

    @SuppressWarnings("rawtypes")
    private void filterUnacceptableKeysRecursive(Map json, String prefix, Object target, Object action) {
        Iterator<Map.Entry> it = json.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry entry = it.next();
            if (!(entry.getKey() instanceof String key)) {
                // Defensive: a custom JSONReader could produce non-String keys. Skip — we cannot
                // construct a parameter path for filtering, and JSONPopulator wouldn't bind these anyway.
                LOG.debug("Skipping JSON entry with non-String key [{}] of type [{}] under prefix [{}]",
                        entry.getKey(), keyTypeName(entry.getKey()), prefix);
                continue;
            }
            String fullPath = prefix.isEmpty() ? key : prefix + "." + key;

            Object value = entry.getValue();
            boolean leaf = !(value instanceof Map) && !(value instanceof java.util.List);

            // Per-node checks (length, excluded patterns, @StrutsParameter authorization, property
            // filters) apply to every node. Name-allowlist checks (accepted patterns,
            // ParameterNameAware) apply only at leaf keys, so an intermediate node is never gated by
            // a pattern written for a leaf path — mirroring ParametersInterceptor, which only ever
            // evaluates complete leaf names.
            if (!isAcceptableNode(fullPath, target, action) || (leaf && !isAcceptableLeafName(fullPath, action))) {
                it.remove();
                continue;
            }

            if (value instanceof Map) {
                filterUnacceptableKeysRecursive((Map) value, fullPath, target, action);
            } else if (value instanceof java.util.List) {
                filterUnacceptableList((java.util.List) value, fullPath, target, action);
            } else if (!isAcceptableValue(fullPath, value, action)) {
                it.remove();
            }
        }
    }

    private static String keyTypeName(Object key) {
        return key == null ? "null" : key.getClass().getName();
    }

    @SuppressWarnings("rawtypes")
    private void filterUnacceptableList(java.util.List list, String prefix, Object target, Object action) {
        // Use prefix+"[0]" so list element properties pick up one extra '[' in their path,
        // matching the indexed-path semantics of ParametersInterceptor (e.g. "items[0].key").
        String elementPrefix = prefix + "[0]";
        Iterator it = list.iterator();
        while (it.hasNext()) {
            Object item = it.next();
            if (item instanceof Map) {
                filterUnacceptableKeysRecursive((Map) item, elementPrefix, target, action);
            } else if (item instanceof java.util.List) {
                filterUnacceptableList((java.util.List) item, elementPrefix, target, action);
            // Scalar list elements are full leaf binding targets: apply the per-node checks, the leaf
            // name-allowlist checks and the value checks at the element path (e.g. "items[0]"), so a
            // scalar element is gated exactly as ParametersInterceptor gates the flat name "items[0]".
            } else if (!isAcceptableNode(elementPrefix, target, action)
                    || !isAcceptableLeafName(elementPrefix, action)
                    || !isAcceptableValue(elementPrefix, item, action)) {
                it.remove();
            }
        }
    }

    /**
     * Checks applied to every node of the JSON tree (both intermediate objects/arrays and leaves):
     * key length, excluded name patterns, {@code @StrutsParameter} authorization, and — when
     * enabled — the interceptor's own property filters. Excluded patterns are prefix-safe and
     * authorization is intentionally hierarchical (a parent must be authorized for a child to be
     * reachable), so these are correct to evaluate at intermediate nodes.
     */
    private boolean isAcceptableNode(String fullPath, Object target, Object action) {
        if (fullPath.length() > paramNameMaxLength) {
            LOG.warn("JSON body parameter [{}] is too long, allowed length is [{}]; rejected", fullPath, paramNameMaxLength);
            return false;
        }
        if (excludedPatterns != null && excludedPatterns.isExcluded(fullPath).isExcluded()) {
            LOG.warn("JSON body parameter [{}] matches an excluded pattern; rejected", fullPath);
            return false;
        }
        if (!parameterAuthorizer.isAuthorized(fullPath, target, action)) {
            LOG.warn("JSON body parameter [{}] rejected by @StrutsParameter authorization on [{}]",
                    fullPath, target.getClass().getName());
            return false;
        }
        if (applyPropertyFiltersToInput && !isAcceptedByPropertyFilters(fullPath)) {
            LOG.debug("JSON body parameter [{}] rejected by excludeProperties/includeProperties on input", fullPath);
            return false;
        }
        return true;
    }

    /**
     * Name-allowlist checks applied only at leaf keys (scalar values, including scalar array
     * elements): accepted name patterns and the {@link ParameterNameAware} action callback. These
     * are deliberately not applied to intermediate nodes: their patterns/decisions target the full
     * dotted binding path, and gating an intermediate node against a leaf-specific rule would drop
     * the whole subtree before the leaf is ever evaluated. This matches ParametersInterceptor, which
     * only ever evaluates complete leaf names.
     */
    private boolean isAcceptableLeafName(String fullPath, Object action) {
        if (acceptedPatterns != null && !acceptedPatterns.isAccepted(fullPath).isAccepted()) {
            LOG.warn("JSON body parameter [{}] does not match any accepted pattern; rejected", fullPath);
            return false;
        }
        if (action instanceof ParameterNameAware nameAware && !nameAware.acceptableParameterName(fullPath)) {
            LOG.debug("JSON body parameter [{}] rejected by ParameterNameAware action", fullPath);
            return false;
        }
        return true;
    }

    private boolean isAcceptedByPropertyFilters(String fullPath) {
        if (excludeProperties != null) {
            for (Pattern pattern : excludeProperties) {
                if (pattern.matcher(fullPath).matches()) {
                    return false;
                }
            }
        }
        if (includeProperties != null && !includeProperties.isEmpty()) {
            for (Pattern pattern : includeProperties) {
                if (pattern.matcher(fullPath).matches()) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }

    private boolean isAcceptableValue(String fullPath, Object value, Object action) {
        String stringValue = value == null ? null : String.valueOf(value);
        if (action instanceof ParameterValueAware valueAware && !valueAware.acceptableParameterValue(stringValue)) {
            LOG.debug("JSON body value for parameter [{}] rejected by ParameterValueAware action", fullPath);
            return false;
        }
        if (stringValue == null || stringValue.isEmpty()) {
            return true;
        }
        if (isValueExcluded(stringValue)) {
            LOG.warn("JSON body value [{}] for parameter [{}] matches an excluded value pattern; rejected", stringValue, fullPath);
            return false;
        }
        if (!isValueAccepted(stringValue)) {
            LOG.warn("JSON body value [{}] for parameter [{}] does not match any accepted value pattern; rejected", stringValue, fullPath);
            return false;
        }
        return true;
    }

    private boolean isValueExcluded(String value) {
        if (excludedValuePatterns == null || excludedValuePatterns.isEmpty()) {
            return false;
        }
        for (Pattern pattern : excludedValuePatterns) {
            if (pattern.matcher(value).matches()) {
                return true;
            }
        }
        return false;
    }

    private boolean isValueAccepted(String value) {
        if (acceptedValuePatterns == null || acceptedValuePatterns.isEmpty()) {
            return true;
        }
        for (Pattern pattern : acceptedValuePatterns) {
            if (pattern.matcher(value).matches()) {
                return true;
            }
        }
        return false;
    }

    protected String readContentType(HttpServletRequest request) {
        String contentType = request.getHeader("Content-Type");
        LOG.debug("Content Type from request: {}", contentType);

        if (contentType != null && contentType.contains(";")) {
            contentType = contentType.substring(0, contentType.indexOf(";")).trim();
        }
        return contentType;
    }

    protected String readContentTypeEncoding(HttpServletRequest request) {
        String contentTypeEncoding = request.getHeader("Content-Type");
        LOG.debug("Content Type encoding from request: {}", contentTypeEncoding);

        if (contentTypeEncoding != null && contentTypeEncoding.contains(";charset=")) {
            contentTypeEncoding = contentTypeEncoding.substring(contentTypeEncoding.indexOf(";charset=") + ";charset=".length()).trim();
        } else {
            contentTypeEncoding = defaultEncoding;
        }

        LOG.debug("Content Type encoding to be used in de-serialisation: {}", contentTypeEncoding);
        return contentTypeEncoding;
    }

    @SuppressWarnings("unchecked")
    public RPCResponse invoke(Object object, Map data) throws IllegalArgumentException,
            IllegalAccessException, InvocationTargetException, JSONException, InstantiationException,
            NoSuchMethodException, IntrospectionException {

        RPCResponse response = new RPCResponse();

        // validate id
        Object id = data.get("id");
        if (id == null) {
            String message = "'id' is required for JSON RPC";
            response.setError(new RPCError(message, RPCErrorCode.METHOD_NOT_FOUND));
            return response;
        }
        // could be a numeric value
        response.setId(id.toString());

        // the map is going to have: 'params', 'method' and 'id' (for the
        // client to identify the response)
        Class clazz = object.getClass();

        // parameters
        List parameters = (List) data.get("params");
        int parameterCount = parameters != null ? parameters.size() : 0;

        // method
        String methodName = (String) data.get("method");
        if (methodName == null) {
            String message = "'method' is required for JSON RPC";
            response.setError(new RPCError(message, RPCErrorCode.MISSING_METHOD));
            return response;
        }

        Method method = this.getMethod(clazz, methodName, parameterCount);
        if (method == null) {
            String message = "Method " + methodName + " could not be found in action class.";
            response.setError(new RPCError(message, RPCErrorCode.METHOD_NOT_FOUND));
            return response;
        }

        // parameters
        if (parameterCount > 0) {
            Class[] parameterTypes = method.getParameterTypes();
            Type[] genericTypes = method.getGenericParameterTypes();
            List invocationParameters = new ArrayList();

            // validate size
            if (parameterTypes.length != parameterCount) {
                // size mismatch
                String message = "Parameter count in request, " + parameterCount
                        + " do not match expected parameter count for " + methodName + ", "
                        + parameterTypes.length;

                response.setError(new RPCError(message, RPCErrorCode.PARAMETERS_MISMATCH));
                return response;
            }

            // convert parameters
            for (int i = 0; i < parameters.size(); i++) {
                Object parameter = parameters.get(i);
                Class paramType = parameterTypes[i];
                Type genericType = genericTypes[i];

                // clean up the values
                if (dataCleaner != null) {
                    parameter = dataCleaner.clean("[" + i + "]", parameter);
                }

                Object converted = populator.convert(paramType, genericType, parameter, method);
                invocationParameters.add(converted);
            }

            response.setResult(method.invoke(object, invocationParameters.toArray()));
        } else {
            response.setResult(method.invoke(object, new Object[0]));
        }

        return response;
    }

    @SuppressWarnings("unchecked")
    private Method getMethod(Class clazz, String name, int parameterCount) {
        Method[] smdMethods = JSONUtil.listSMDMethods(clazz, ignoreSMDMethodInterfaces);

        for (Method method : smdMethods) {
            if (checkSMDMethodSignature(method, name, parameterCount)) {
                return method;
            }
        }
        return null;
    }

    /**
     * Look for a method in clazz carrying the SMDMethod annotation with
     * matching name and parametersCount
     *
     * @return true if matches name and parameterCount
     */
    private boolean checkSMDMethodSignature(Method method, String name, int parameterCount) {

        SMDMethod smdMethodAnntotation = method.getAnnotation(SMDMethod.class);
        if (smdMethodAnntotation != null) {
            String alias = smdMethodAnntotation.name();
            boolean paramsMatch = method.getParameterTypes().length == parameterCount;
            if (((alias.length() == 0) && method.getName().equals(name) && paramsMatch)
                    || (alias.equals(name) && paramsMatch)) {
                return true;
            }
        }

        return false;
    }

    protected String addCallbackIfApplicable(HttpServletRequest request, String json) {
        if ((callbackParameter != null) && (callbackParameter.length() > 0)) {
            String callbackName = request.getParameter(callbackParameter);
            if ((callbackName != null) && (callbackName.length() > 0))
                json = callbackName + "(" + json + ")";
        }
        return json;
    }

    public boolean isEnableSMD() {
        return this.enableSMD;
    }

    public void setEnableSMD(boolean enableSMD) {
        this.enableSMD = enableSMD;
    }

    /**
     * Ignore annotations on methods in interfaces You may need to set to this
     * true if your action is a proxy/enhanced as annotations are not inherited
     *
     * @param ignoreSMDMethodInterfaces set the flag for ignore SMD method interfaces
     */
    public void setIgnoreSMDMethodInterfaces(boolean ignoreSMDMethodInterfaces) {
        this.ignoreSMDMethodInterfaces = ignoreSMDMethodInterfaces;
    }

    /**
     * Wrap generated JSON with comments. Only used if SMD is enabled.
     *
     * @param wrapWithComments Wrap generated JSON with comments.
     */
    public void setWrapWithComments(boolean wrapWithComments) {
        this.wrapWithComments = wrapWithComments;
    }

    @Inject(StrutsConstants.STRUTS_I18N_ENCODING)
    public void setDefaultEncoding(String val) {
        this.defaultEncoding = val;
    }

    /**
     * @param ignoreHierarchy Ignore properties defined on base classes of the root object.
     */
    public void setIgnoreHierarchy(boolean ignoreHierarchy) {
        this.ignoreHierarchy = ignoreHierarchy;
    }

    /**
     * Sets the root object to be deserialized, defaults to the Action
     *
     * @param root
     *            OGNL expression of root object to be serialized
     */
    public void setRoot(String root) {
        this.root = root;
    }

    /**
     * Sets the JSONPopulator to be used
     *
     * @param populator
     *            JSONPopulator
     */
    public void setJSONPopulator(JSONPopulator populator) {
        this.populator = populator;
    }

    /**
     * Sets the JSONCleaner to be used
     *
     * @param dataCleaner
     *            JSONCleaner
     */
    public void setJSONCleaner(JSONCleaner dataCleaner) {
        this.dataCleaner = dataCleaner;
    }

    /**
     * @return true if debugging is turned on
     */
    public boolean getDebug() {
        Boolean devModeOverride = PrepareOperations.getDevModeOverride();
        return devModeOverride != null ? devModeOverride : this.debug;
    }

    /**
     * Turns debugging on or off
     *
     * @param debug
     *            true or false
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    @Inject(StrutsConstants.STRUTS_DEVMODE)
    public void setDevMode(String mode) {
        setDebug(BooleanUtils.toBoolean(mode));
    }

    /**
     * Sets a comma-delimited list of regular expressions to match properties
     * that should be excluded from the JSON output.
     *
     * @param commaDelim
     *            A comma-delimited list of regular expressions
     */
    public void setExcludeProperties(String commaDelim) {
        Set<String> excludePatterns = JSONUtil.asSet(commaDelim);
        if (excludePatterns != null) {
            this.excludeProperties = new ArrayList<>(excludePatterns.size());
            for (String pattern : excludePatterns) {
                this.excludeProperties.add(Pattern.compile(pattern));
            }
        }
    }

    /**
     * Sets a comma-delimited list of wildcard expressions to match
     * properties that should be excluded from the JSON output.
     *
     * @param commaDelim
     *            A comma-delimited list of wildcard expressions
     */
    public void setExcludeWildcards(String commaDelim) {
        Set<String> excludePatterns = JSONUtil.asSet(commaDelim);
        if (excludePatterns != null) {
            this.excludeProperties = new ArrayList<>(excludePatterns.size());
            for (String pattern : excludePatterns) {
                this.excludeProperties.add(WildcardUtil.compileWildcardPattern(pattern));
            }
        }
    }

    /**
     * Sets a comma-delimited list of regular expressions to match properties
     * that should be included from the JSON output.
     *
     * @param commaDelim
     *            A comma-delimited list of regular expressions
     */
    public void setIncludeProperties(String commaDelim) {
        includeProperties = JSONUtil.processIncludePatterns(JSONUtil.asSet(commaDelim), JSONUtil.REGEXP_PATTERN);
    }

    /**
     * Sets a comma-delimited list of wildcard expressions to match
     * properties that should be included from the JSON output.  The
     * standard boilerplate (id, error, debug) are automatically included,
     * as appropriate, so you only need to provide patterns for the
     * contents of "result".
     *
     * @param commaDelim
     *            A comma-delimited list of wildcard expressions
     */
    public void setIncludeWildcards(String commaDelim) {
        includeProperties = JSONUtil.processIncludePatterns(JSONUtil.asSet(commaDelim), JSONUtil.WILDCARD_PATTERN);
        if (includeProperties != null) {
            includeProperties.add(Pattern.compile("id"));
            includeProperties.add(Pattern.compile("result"));
            includeProperties.add(Pattern.compile("error"));
            includeProperties.add(WildcardUtil.compileWildcardPattern("error.code"));
        }
    }

    /**
     * @return  the appropriate set of includes, based on debug setting.
     * Derived classes can override if there are additional, custom
     * debug-only parameters.
     */
    protected List getIncludeProperties() {
        if (includeProperties != null && getDebug()) {
            List<Pattern> list = new ArrayList<>(includeProperties);
            list.add(Pattern.compile("debug"));
            list.add(WildcardUtil.compileWildcardPattern("error.*"));
            return list;
        } else {
            return includeProperties;
        }
    }

    public boolean isEnableGZIP() {
        return enableGZIP;
    }

    /**
     * Setting this property to "true" will compress the output.
     *
     * @param enableGZIP
     *            Enable compressed output
     */
    public void setEnableGZIP(boolean enableGZIP) {
        this.enableGZIP = enableGZIP;
    }

    public boolean isNoCache() {
        return noCache;
    }

    /**
     * Add headers to response to prevent the browser from caching the response
     *
     * @param noCache no cache
     */
    public void setNoCache(boolean noCache) {
        this.noCache = noCache;
    }

    public boolean isExcludeNullProperties() {
        return excludeNullProperties;
    }

    /**
     * @param excludeNullProperties  Do not serialize properties with a null value
     */
    public void setExcludeNullProperties(boolean excludeNullProperties) {
        this.excludeNullProperties = excludeNullProperties;
    }

    public void setCallbackParameter(String callbackParameter) {
        this.callbackParameter = callbackParameter;
    }

    public String getCallbackParameter() {
        return callbackParameter;
    }

    /**
     * @param prefix Add "{} &amp;&amp; " to generated JSON
     */
    public void setPrefix(boolean prefix) {
        this.prefix = prefix;
    }

    public void setJsonContentType(String jsonContentType) {
        this.jsonContentType = jsonContentType;
    }

    public void setJsonRpcContentType(String jsonRpcContentType) {
        this.jsonRpcContentType = jsonRpcContentType;
    }

    @Inject
    public void setContainer(Container container) {
        this.container = container;
    }

    protected JSONUtil getJSONUtil() {
        return container.getInstance(JSONUtil.class);
    }

    @Inject
    public void setParameterAuthorizer(ParameterAuthorizer parameterAuthorizer) {
        this.parameterAuthorizer = parameterAuthorizer;
    }

    @Inject
    public void setExcludedPatterns(ExcludedPatternsChecker excludedPatterns) {
        this.excludedPatterns = excludedPatterns;
    }

    @Inject
    public void setAcceptedPatterns(AcceptedPatternsChecker acceptedPatterns) {
        this.acceptedPatterns = acceptedPatterns;
    }

    /**
     * If the dotted JSON key path exceeds the configured maximum length it will not be accepted.
     *
     * @param paramNameMaxLength maximum length of a JSON key path
     */
    public void setParamNameMaxLength(int paramNameMaxLength) {
        this.paramNameMaxLength = paramNameMaxLength;
    }

    /**
     * Sets a comma-delimited list of regular expressions to match JSON leaf values
     * that should be removed. Opt-in: no patterns configured means no value filtering.
     *
     * @param commaDelim comma-delimited regular expressions
     */
    public void setExcludedValuePatterns(String commaDelim) {
        this.excludedValuePatterns = compileValuePatterns(commaDelim);
    }

    /**
     * Sets a comma-delimited list of regular expressions; when set, only JSON leaf values
     * matching one of them are accepted. Opt-in: no patterns configured means no value filtering.
     *
     * @param commaDelim comma-delimited regular expressions
     */
    public void setAcceptedValuePatterns(String commaDelim) {
        this.acceptedValuePatterns = compileValuePatterns(commaDelim);
    }

    /**
     * When enabled, the interceptor's {@code excludeProperties}/{@code includeProperties} patterns —
     * otherwise used only for serialization output — also gate which JSON keys are populated on input.
     * Opt-in; defaults to {@code false} to preserve existing behavior.
     *
     * @param applyPropertyFiltersToInput true to apply property filters to deserialization
     */
    public void setApplyPropertyFiltersToInput(boolean applyPropertyFiltersToInput) {
        this.applyPropertyFiltersToInput = applyPropertyFiltersToInput;
    }

    private static Set<Pattern> compileValuePatterns(String commaDelim) {
        Set<String> raw = TextParseUtil.commaDelimitedStringToSet(commaDelim);
        Set<Pattern> compiled = new HashSet<>(raw.size());
        for (String pattern : raw) {
            compiled.add(Pattern.compile(pattern, Pattern.CASE_INSENSITIVE));
        }
        return Collections.unmodifiableSet(compiled);
    }

    @Inject(value = JSONConstants.JSON_MAX_ELEMENTS, required = false)
    public void setMaxElements(String maxElements) {
        this.maxElements = Integer.parseInt(maxElements);
    }

    @Inject(value = JSONConstants.JSON_MAX_DEPTH, required = false)
    public void setMaxDepth(String maxDepth) {
        this.maxDepth = Integer.parseInt(maxDepth);
    }

    @Inject(value = JSONConstants.JSON_MAX_LENGTH, required = false)
    public void setMaxLength(String maxLength) {
        this.maxLength = Integer.parseInt(maxLength);
    }

    @Inject(value = JSONConstants.JSON_MAX_STRING_LENGTH, required = false)
    public void setMaxStringLength(String maxStringLength) {
        this.maxStringLength = Integer.parseInt(maxStringLength);
    }

    @Inject(value = JSONConstants.JSON_MAX_KEY_LENGTH, required = false)
    public void setMaxKeyLength(String maxKeyLength) {
        this.maxKeyLength = Integer.parseInt(maxKeyLength);
    }
}
