|
2 | 2 |
|
3 | 3 | import com.fasterxml.jackson.databind.JsonNode;
|
4 | 4 | import com.fasterxml.jackson.databind.node.ArrayNode;
|
5 |
| -import com.networknt.schema.CollectorContext; |
6 |
| -import com.networknt.schema.JsonType; |
7 |
| -import com.networknt.schema.SchemaValidatorsConfig; |
8 |
| -import com.networknt.schema.ValidatorState; |
| 5 | +import com.networknt.schema.*; |
9 | 6 |
|
10 | 7 | import java.util.Iterator;
|
11 | 8 |
|
12 | 9 | public class JsonNodeUtil {
|
| 10 | + private static final String TYPE = "type"; |
| 11 | + private static final String ENUM = "enum"; |
| 12 | + private static final String REF = "$ref"; |
| 13 | + private static final String NULLABLE = "nullable"; |
13 | 14 |
|
14 | 15 | public static boolean isNodeNullable(JsonNode schema){
|
15 |
| - JsonNode nullable = schema.get("nullable"); |
| 16 | + JsonNode nullable = schema.get(NULLABLE); |
16 | 17 | if (nullable != null && nullable.asBoolean()) {
|
17 | 18 | return true;
|
18 | 19 | }
|
@@ -42,10 +43,224 @@ public static boolean matchOneOfTypeNode(JsonNode oneOfSchemaNode, JsonType node
|
42 | 43 | Iterator iterator = oneOfSchemaNode.elements();
|
43 | 44 | while (iterator.hasNext()){
|
44 | 45 | JsonNode oneOfTypeNode = (JsonNode) iterator.next();
|
45 |
| - JsonNode typeTextNode = oneOfTypeNode.get("type"); |
| 46 | + JsonNode typeTextNode = oneOfTypeNode.get(TYPE); |
46 | 47 | if(typeTextNode != null && typeTextNode.asText().equals(nodeType.toString())) //If the nodeType is oneOf the type defined in the oneOf , return true
|
47 | 48 | return true;
|
48 | 49 | }
|
49 | 50 | return false;
|
50 | 51 | }
|
| 52 | + |
| 53 | + public static boolean equalsToSchemaType(JsonNode node, JsonSchema schema, SchemaValidatorsConfig config) { |
| 54 | + // in the case that node type is not the same as schema type, try to convert node to the |
| 55 | + // same type of schema. In REST API, query parameters, path parameters and headers are all |
| 56 | + // string type and we must convert, otherwise, all schema validations will fail. |
| 57 | + JsonType schemaType = getSchemaJsonType(schema); |
| 58 | + return equalsToSchemaType(node,schemaType,schema.getParentSchema(),config); |
| 59 | + |
| 60 | + } |
| 61 | + |
| 62 | + public static JsonType getSchemaJsonType(JsonSchema schema){ |
| 63 | + JsonNode typeNode = schema.getSchemaNode().get(TYPE); |
| 64 | + if(typeNode!= null) return JsonType.valueOf(typeNode.asText().toUpperCase()); |
| 65 | + return JsonType.UNKNOWN; |
| 66 | + } |
| 67 | + |
| 68 | + |
| 69 | + |
| 70 | + public static boolean equalsToSchemaType(JsonNode node, JsonType schemaType, JsonSchema parentSchema, SchemaValidatorsConfig config) { |
| 71 | + // in the case that node type is not the same as schema type, try to convert node to the |
| 72 | + // same type of schema. In REST API, query parameters, path parameters and headers are all |
| 73 | + // string type and we must convert, otherwise, all schema validations will fail. |
| 74 | + |
| 75 | + JsonType nodeType = TypeFactory.getValueNodeType(node, config); |
| 76 | + |
| 77 | + if (nodeType != schemaType) { |
| 78 | + if (schemaType == JsonType.ANY) { |
| 79 | + return true; |
| 80 | + } |
| 81 | + |
| 82 | + if (schemaType == JsonType.NUMBER && nodeType == JsonType.INTEGER) { |
| 83 | + return true; |
| 84 | + } |
| 85 | + |
| 86 | + ValidatorState state = (ValidatorState) CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY); |
| 87 | + if(JsonType.NULL.equals(nodeType) ){ |
| 88 | + if(state.isComplexValidator() && JsonNodeUtil.isNodeNullable(parentSchema.getParentSchema().getSchemaNode(), config) || JsonNodeUtil.isNodeNullable(parentSchema.getSchemaNode()) ){ |
| 89 | + return true; |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + // Skip the type validation when the schema is an enum object schema. Since the current type |
| 94 | + // of node itself can be used for type validation. |
| 95 | + if (isEnumObjectSchema(parentSchema)) { |
| 96 | + return true; |
| 97 | + } |
| 98 | + if (config.isTypeLoose()) { |
| 99 | + // if typeLoose is true, everything can be a size 1 array |
| 100 | + if (schemaType == JsonType.ARRAY) { |
| 101 | + return true; |
| 102 | + } |
| 103 | + if (nodeType == JsonType.STRING) { |
| 104 | + if (schemaType == JsonType.INTEGER) { |
| 105 | + if (isInteger(node.textValue())) { |
| 106 | + return true; |
| 107 | + } |
| 108 | + } else if (schemaType == JsonType.BOOLEAN) { |
| 109 | + if (isBoolean(node.textValue())) { |
| 110 | + return true; |
| 111 | + } |
| 112 | + } else if (schemaType == JsonType.NUMBER) { |
| 113 | + if (isNumeric(node.textValue())) { |
| 114 | + return true; |
| 115 | + } |
| 116 | + } |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + return false; |
| 121 | + } |
| 122 | + return true; |
| 123 | + } |
| 124 | + public static boolean isInteger(String str) { |
| 125 | + if (str == null || str.equals("")) { |
| 126 | + return false; |
| 127 | + } |
| 128 | + |
| 129 | + // all code below could be replaced with |
| 130 | + //return str.matrch("[-+]?(?:0|[1-9]\\d*)") |
| 131 | + int i = 0; |
| 132 | + if (str.charAt(0) == '-' || str.charAt(0) == '+') { |
| 133 | + if (str.length() == 1) { |
| 134 | + return false; |
| 135 | + } |
| 136 | + i = 1; |
| 137 | + } |
| 138 | + for (; i < str.length(); i++) { |
| 139 | + char c = str.charAt(i); |
| 140 | + if (c < '0' || c > '9') { |
| 141 | + return false; |
| 142 | + } |
| 143 | + } |
| 144 | + return true; |
| 145 | + } |
| 146 | + |
| 147 | + public static boolean isBoolean(String s) { |
| 148 | + return "true".equals(s) || "false".equals(s); |
| 149 | + } |
| 150 | + |
| 151 | + public static boolean isNumeric(String str) { |
| 152 | + if (str == null || str.equals("")) { |
| 153 | + return false; |
| 154 | + } |
| 155 | + |
| 156 | + // all code below could be replaced with |
| 157 | + //return str.matrch("[-+]?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?") |
| 158 | + int i = 0; |
| 159 | + int len = str.length(); |
| 160 | + |
| 161 | + if (str.charAt(i) == MINUS || str.charAt(i) == PLUS) { |
| 162 | + if (str.length() == 1) { |
| 163 | + return false; |
| 164 | + } |
| 165 | + i = 1; |
| 166 | + } |
| 167 | + |
| 168 | + char c = str.charAt(i++); |
| 169 | + |
| 170 | + if (c == CHAR_0) { |
| 171 | + // TODO: if leading zeros are supported (counter to JSON spec) handle it here |
| 172 | + if (i < len) { |
| 173 | + c = str.charAt(i++); |
| 174 | + if (c != DOT && c != CHAR_E && c != CHAR_e) { |
| 175 | + return false; |
| 176 | + } |
| 177 | + } |
| 178 | + } else if (CHAR_1 <= c && c <= CHAR_9) { |
| 179 | + while (i < len && CHAR_0 <= c && c <= CHAR_9) { |
| 180 | + c = str.charAt(i++); |
| 181 | + } |
| 182 | + } else { |
| 183 | + return false; |
| 184 | + } |
| 185 | + |
| 186 | + if (c == DOT) { |
| 187 | + if (i >= len) { |
| 188 | + return false; |
| 189 | + } |
| 190 | + c = str.charAt(i++); |
| 191 | + while (i < len && CHAR_0 <= c && c <= CHAR_9) { |
| 192 | + c = str.charAt(i++); |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + if (c == CHAR_E || c == CHAR_e) { |
| 197 | + if (i >= len) { |
| 198 | + return false; |
| 199 | + } |
| 200 | + c = str.charAt(i++); |
| 201 | + if (c == PLUS || c == MINUS) { |
| 202 | + if (i >= len) { |
| 203 | + return false; |
| 204 | + } |
| 205 | + c = str.charAt(i++); |
| 206 | + } |
| 207 | + while (i < len && CHAR_0 <= c && c <= CHAR_9) { |
| 208 | + c = str.charAt(i++); |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + return i >= len && (CHAR_0 <= c && c <= CHAR_9); |
| 213 | + } |
| 214 | + |
| 215 | + private static final char CHAR_0 = '0'; |
| 216 | + private static final char CHAR_1 = '1'; |
| 217 | + private static final char CHAR_9 = '9'; |
| 218 | + private static final char MINUS = '-'; |
| 219 | + private static final char PLUS = '+'; |
| 220 | + private static final char DOT = '.'; |
| 221 | + private static final char CHAR_E = 'E'; |
| 222 | + private static final char CHAR_e = 'e'; |
| 223 | + |
| 224 | + /** |
| 225 | + * Check if the type of the JsonNode's value is number based on the |
| 226 | + * status of typeLoose flag. |
| 227 | + * |
| 228 | + * @param node the JsonNode to check |
| 229 | + * @param config the SchemaValidatorsConfig to depend on |
| 230 | + * @return boolean to indicate if it is a number |
| 231 | + */ |
| 232 | + public static boolean isNumber(JsonNode node, SchemaValidatorsConfig config) { |
| 233 | + if (node.isNumber()) { |
| 234 | + return true; |
| 235 | + } else if (config.isTypeLoose()) { |
| 236 | + if (TypeFactory.getValueNodeType(node, config) == JsonType.STRING) { |
| 237 | + return isNumeric(node.textValue()); |
| 238 | + } |
| 239 | + } |
| 240 | + return false; |
| 241 | + } |
| 242 | + |
| 243 | + private static boolean isEnumObjectSchema(JsonSchema jsonSchema) { |
| 244 | + // There are three conditions for enum object schema |
| 245 | + // 1. The current schema contains key "type", and the value is object |
| 246 | + // 2. The current schema contains key "enum", and the value is an array |
| 247 | + // 3. The parent schema if refer from components, which means the corresponding enum object class would be generated |
| 248 | + JsonNode typeNode = null; |
| 249 | + JsonNode enumNode = null; |
| 250 | + JsonNode refNode = null; |
| 251 | + |
| 252 | + if (jsonSchema != null) { |
| 253 | + if (jsonSchema.getSchemaNode() != null) { |
| 254 | + typeNode = jsonSchema.getSchemaNode().get(TYPE); |
| 255 | + enumNode = jsonSchema.getSchemaNode().get(ENUM); |
| 256 | + } |
| 257 | + if (jsonSchema.getParentSchema() != null && jsonSchema.getParentSchema().getSchemaNode() != null) { |
| 258 | + refNode = jsonSchema.getParentSchema().getSchemaNode().get(REF); |
| 259 | + } |
| 260 | + } |
| 261 | + if (typeNode != null && enumNode != null && refNode != null) { |
| 262 | + return TypeFactory.getSchemaNodeType(typeNode) == JsonType.OBJECT && enumNode.isArray(); |
| 263 | + } |
| 264 | + return false; |
| 265 | + } |
51 | 266 | }
|
0 commit comments