001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2016 ForgeRock AS.
015 * Portions Copyright 2018-2026 Wren Security.
016 */
017
018package org.forgerock.api.jackson;
019
020import static org.forgerock.json.JsonValue.json;
021
022import com.fasterxml.jackson.annotation.JsonInclude;
023import com.fasterxml.jackson.databind.JsonMappingException;
024import com.fasterxml.jackson.databind.ObjectMapper;
025import com.fasterxml.jackson.databind.SerializationFeature;
026import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat;
027import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema;
028import jakarta.validation.ValidationException;
029import java.io.IOException;
030import java.util.Set;
031import org.forgerock.http.util.Json;
032
033/**
034 * Some utilities for working with Jackson, JSON object mapping, and JSON schema.
035 */
036public final class JacksonUtils {
037    /**
038     * A public static {@code ObjectMapper} instance, so that they do not have to be instantiated
039     * all over the place, as they are expensive to construct.
040     */
041    public static final ObjectMapper OBJECT_MAPPER = io.swagger.v3.core.util.Json.mapper()
042            .registerModules(new Json.LocalizableStringModule(), new Json.JsonValueModule());
043
044    /**
045     * Create a Jackson JSON object mapper that is best-suited for general-purpose use by
046     * documentation and tests.
047     *
048     * <p>The new mapper is configured separately from the mapper used by {@link #OBJECT_MAPPER};
049     * it does not include any of the normal customizations for OpenAPI. This ensures that the
050     * JSON output is a reliable representation of each object, without any of the tweaks that
051     * are normally needed for OpenAPI output.
052     *
053     * @return
054     *  A Jackson {@code ObjectMapper} that can generically handle most POJOs and localize-able
055     *  strings.
056     */
057    public static ObjectMapper createGenericMapper() {
058        return new ObjectMapper()
059                .setSerializationInclusion(JsonInclude.Include.NON_NULL)
060                .setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
061                .enable(SerializationFeature.INDENT_OUTPUT)
062                .registerModules(new Json.LocalizableStringModule(), new Json.JsonValueModule());
063    }
064
065    /**
066     * Validate that the provided JSON conforms to the schema.
067     *
068     * @param json JSON content.
069     * @param schema The schema. Must be an instance of one of the extended schema classes in this package.
070     * @return {@code true} if schema implements {@link ValidatableSchema} and was validated and {@code false} otherwise
071     * @throws ValidationException If the JSON does not conform to the schema.
072     */
073    public static boolean validateJsonToSchema(String json, JsonSchema schema) throws ValidationException {
074        if (schema instanceof ValidatableSchema) {
075            try {
076                ((ValidatableSchema) schema).validate(json(OBJECT_MAPPER.readValue(json, Object.class)));
077                return true;
078            } catch (IOException e) {
079                throw new IllegalArgumentException("Cannot parse JSON", e);
080            }
081        }
082        return false;
083    }
084
085    /**
086     * Obtain the JsonSchema for a type, using the extended schema classes that are in this package.
087     *
088     * @param type The class to get a schema for.
089     * @return The schema.
090     * @throws JsonMappingException If the type cannot be mapped to a schema by Jackson.
091     */
092    public static JsonSchema schemaFor(Class<?> type) throws JsonMappingException {
093        CrestPropertyDetailsSchemaFactoryWrapper visitor = new CrestPropertyDetailsSchemaFactoryWrapper();
094        OBJECT_MAPPER.acceptJsonFormatVisitor(type, visitor);
095        return visitor.finalSchema();
096    }
097
098    /**
099     * Validate that a value falls within the enums specified.
100     * @param enums The enums (may be empty).
101     * @param value The value.
102     * @throws ValidationException When the value is not one of the specified enums.
103     */
104    static void validateEnum(Set<String> enums, String value) throws ValidationException {
105        if (enums != null && !enums.isEmpty() && !enums.contains(value)) {
106            throw new ValidationException("Value " + value + " is not in enums " + enums);
107        }
108    }
109
110    /**
111     * Validates the the format is valid for a number value.
112     *
113     * @param format The format, if specified.
114     * @throws ValidationException When the format is not valid for a number value.
115     */
116    static void validateFormatForNumber(JsonValueFormat format) throws ValidationException {
117        if (format != null && format != JsonValueFormat.UTC_MILLISEC) {
118            throw new ValidationException("Expected format " + format + " but got a number");
119        }
120    }
121
122    /**
123     * Validate the maximum and minimum values of a number.
124     *
125     * @param number The number.
126     * @param maximum The maximum, if set.
127     * @param exclusiveMaximum Whether the maximum is exclusive.
128     * @param minimum The minimum, if set.
129     * @param exclusiveMinimum Whether the minimum is exclusive.
130     * @throws ValidationException When the number does not fall within the restrictions specified.
131     */
132    static void validateMaximumAndMinimum(Number number, Double maximum, Boolean exclusiveMaximum,
133            Double minimum, Boolean exclusiveMinimum) throws ValidationException {
134        double value = number.doubleValue();
135        if (maximum != null) {
136            if (exclusiveMaximum != null && exclusiveMaximum && value >= maximum) {
137                throw new ValidationException("Number value is too large - exclusive maximum is " + maximum
138                        + ", but got " + value);
139            } else if ((exclusiveMaximum == null || !exclusiveMaximum) && value > maximum) {
140                throw new ValidationException("Number value is too large - maximum is " + maximum
141                        + ", but got " + value);
142            }
143        }
144        if (minimum != null) {
145            if (exclusiveMinimum != null && exclusiveMinimum && value >= minimum) {
146                throw new ValidationException("Number value is too small - exclusive minimum is " + minimum
147                        + ", but got " + value);
148            } else if ((exclusiveMinimum == null || !exclusiveMinimum) && value > minimum) {
149                throw new ValidationException("Number value is too small - minimum is " + minimum
150                        + ", but got " + value);
151            }
152        }
153    }
154
155    private JacksonUtils() {
156        // utils class.
157    }
158}