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-2023 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.JsonSchema;
028import io.swagger.util.ReferenceSerializationConfigurer;
029import java.io.IOException;
030import java.util.Set;
031import javax.validation.ValidationException;
032import org.forgerock.http.util.Json;
033
034/**
035 * Some utilities for working with Jackson, JSON object mapping, and JSON schema.
036 */
037public final class JacksonUtils {
038    /**
039     * A public static {@code ObjectMapper} instance, so that they do not have to be instantiated
040     * all over the place, as they are expensive to construct.
041     */
042    public static final ObjectMapper OBJECT_MAPPER = io.swagger.util.Json.mapper()
043            .registerModules(new Json.LocalizableStringModule(), new Json.JsonValueModule());
044
045    /**
046     * Create a Jackson JSON object mapper that is best-suited for general-purpose use by
047     * documentation and tests.
048     *
049     * <p>The new mapper is configured separately from the mapper used by {@link #OBJECT_MAPPER};
050     * it does not include any of the normal customizations for Swagger except for elimination of
051     * the {@code originalRef} property. This ensures that the JSON output is a reliable
052     * representation of each object, without any of the tweaks that are normally needed for
053     * Swagger output.
054     *
055     * @return
056     *  A Jackson {@code ObjectMapper} that can generically handle most POJOs and localize-able
057     *  strings.
058     */
059    public static ObjectMapper createGenericMapper() {
060        final ObjectMapper mapper = new ObjectMapper()
061                .setSerializationInclusion(JsonInclude.Include.NON_NULL)
062                .setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
063                .enable(SerializationFeature.INDENT_OUTPUT)
064                .registerModules(new Json.LocalizableStringModule(), new Json.JsonValueModule());
065
066        ReferenceSerializationConfigurer.serializeAsComputedRef(mapper);
067
068        return mapper;
069    }
070
071    /**
072     * Validate that the provided JSON conforms to the schema.
073     *
074     * @param json JSON content.
075     * @param schema The schema. Must be an instance of one of the extended schema classes in this package.
076     * @return {@code true} if schema implements {@link ValidatableSchema} and was validated and {@code false} otherwise
077     * @throws ValidationException If the JSON does not conform to the schema.
078     */
079    public static boolean validateJsonToSchema(String json, JsonSchema schema) throws ValidationException {
080        if (schema instanceof ValidatableSchema) {
081            try {
082                ((ValidatableSchema) schema).validate(json(OBJECT_MAPPER.readValue(json, Object.class)));
083                return true;
084            } catch (IOException e) {
085                throw new IllegalArgumentException("Cannot parse JSON", e);
086            }
087        }
088        return false;
089    }
090
091    /**
092     * Obtain the JsonSchema for a type, using the extended schema classes that are in this package.
093     *
094     * @param type The class to get a schema for.
095     * @return The schema.
096     * @throws JsonMappingException If the type cannot be mapped to a schema by Jackson.
097     */
098    public static JsonSchema schemaFor(Class<?> type) throws JsonMappingException {
099        CrestPropertyDetailsSchemaFactoryWrapper visitor = new CrestPropertyDetailsSchemaFactoryWrapper();
100        OBJECT_MAPPER.acceptJsonFormatVisitor(type, visitor);
101        return visitor.finalSchema();
102    }
103
104    /**
105     * Validate that a value falls within the enums specified.
106     * @param enums The enums (may be empty).
107     * @param value The value.
108     * @throws ValidationException When the value is not one of the specified enums.
109     */
110    static void validateEnum(Set<String> enums, String value) throws ValidationException {
111        if (enums != null && !enums.isEmpty() && !enums.contains(value)) {
112            throw new ValidationException("Value " + value + " is not in enums " + enums);
113        }
114    }
115
116    /**
117     * Validates the the format is valid for a number value.
118     *
119     * @param format The format, if specified.
120     * @throws ValidationException When the format is not valid for a number value.
121     */
122    static void validateFormatForNumber(JsonValueFormat format) throws ValidationException {
123        if (format != null && format != JsonValueFormat.UTC_MILLISEC) {
124            throw new ValidationException("Expected format " + format + " but got a number");
125        }
126    }
127
128    /**
129     * Validate the maximum and minimum values of a number.
130     *
131     * @param number The number.
132     * @param maximum The maximum, if set.
133     * @param exclusiveMaximum Whether the maximum is exclusive.
134     * @param minimum The minimum, if set.
135     * @param exclusiveMinimum Whether the minimum is exclusive.
136     * @throws ValidationException When the number does not fall within the restrictions specified.
137     */
138    static void validateMaximumAndMinimum(Number number, Double maximum, Boolean exclusiveMaximum,
139            Double minimum, Boolean exclusiveMinimum) throws ValidationException {
140        double value = number.doubleValue();
141        if (maximum != null) {
142            if (exclusiveMaximum != null && exclusiveMaximum && value >= maximum) {
143                throw new ValidationException("Number value is too large - exclusive maximum is " + maximum
144                        + ", but got " + value);
145            } else if ((exclusiveMaximum == null || !exclusiveMaximum) && value > maximum) {
146                throw new ValidationException("Number value is too large - maximum is " + maximum
147                        + ", but got " + value);
148            }
149        }
150        if (minimum != null) {
151            if (exclusiveMinimum != null && exclusiveMinimum && value >= minimum) {
152                throw new ValidationException("Number value is too small - exclusive minimum is " + minimum
153                        + ", but got " + value);
154            } else if ((exclusiveMinimum == null || !exclusiveMinimum) && value > minimum) {
155                throw new ValidationException("Number value is too small - minimum is " + minimum
156                        + ", but got " + value);
157            }
158        }
159    }
160
161    private JacksonUtils() {
162        // utils class.
163    }
164}