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}