001package gudusoft.gsqlparser.slpc.validation; 002 003import gudusoft.gsqlparser.slpc.model.SlpcObject; 004 005import java.math.BigInteger; 006import java.util.ArrayList; 007import java.util.HashSet; 008import java.util.List; 009import java.util.Set; 010import java.util.regex.Pattern; 011 012/** 013 * Dependency-free Draft-07 evaluator for the keyword vocabulary used by the 014 * checked-in SLPC v0.2 schema. The schema remains the source of truth: this 015 * class reads it at runtime rather than reproducing its fields in Java. 016 */ 017public final class SlpcSchemaValidator { 018 019 private final SlpcObject rootSchema; 020 021 public SlpcSchemaValidator(SlpcObject rootSchema) { 022 if (rootSchema == null) throw new IllegalArgumentException("schema must not be null"); 023 this.rootSchema = rootSchema; 024 } 025 026 public List<SlpcValidationError> validate(Object instance) { 027 ArrayList<SlpcValidationError> errors = new ArrayList<SlpcValidationError>(); 028 evaluate(rootSchema, instance, "$", errors); 029 return errors; 030 } 031 032 public List<SlpcValidationError> validateDefinition(String definitionName, Object instance) { 033 SlpcObject definitions = rootSchema.object("definitions"); 034 if (definitions == null || !definitions.contains(definitionName)) { 035 throw new IllegalArgumentException("missing JSON Schema definition: " + definitionName); 036 } 037 ArrayList<SlpcValidationError> errors = new ArrayList<SlpcValidationError>(); 038 evaluate(definitions.get(definitionName), instance, "$", errors); 039 return errors; 040 } 041 042 private void evaluate(Object schemaValue, Object instance, String path, List<SlpcValidationError> errors) { 043 if (Boolean.TRUE.equals(schemaValue)) return; 044 if (Boolean.FALSE.equals(schemaValue)) { 045 error(errors, path, "value is rejected by boolean schema"); 046 return; 047 } 048 if (!(schemaValue instanceof SlpcObject)) { 049 throw new IllegalArgumentException("invalid JSON Schema node at " + path); 050 } 051 SlpcObject schema = (SlpcObject) schemaValue; 052 053 String ref = schema.string("$ref"); 054 if (ref != null) { 055 evaluate(resolve(ref), instance, path, errors); 056 return; 057 } 058 059 if (schema.contains("type") && !matchesType(schema.get("type"), instance)) { 060 error(errors, path, printable(instance) + " is not of type " + printable(schema.get("type"))); 061 return; 062 } 063 if (schema.contains("const") && !jsonEquals(schema.get("const"), instance)) { 064 error(errors, path, printable(instance) + " is not equal to const " + printable(schema.get("const"))); 065 } 066 List<Object> enumValues = schema.array("enum"); 067 if (enumValues != null && !containsJson(enumValues, instance)) { 068 error(errors, path, printable(instance) + " is not one of " + printable(enumValues)); 069 } 070 071 evaluateCombinators(schema, instance, path, errors); 072 073 if (instance instanceof SlpcObject) evaluateObject(schema, (SlpcObject) instance, path, errors); 074 if (instance instanceof List<?>) evaluateArray(schema, (List<?>) instance, path, errors); 075 if (instance instanceof String) evaluateString(schema, (String) instance, path, errors); 076 if (instance instanceof BigInteger) evaluateInteger(schema, (BigInteger) instance, path, errors); 077 } 078 079 private void evaluateCombinators(SlpcObject schema, Object instance, String path, List<SlpcValidationError> errors) { 080 List<Object> allOf = schema.array("allOf"); 081 if (allOf != null) for (Object branch : allOf) evaluate(branch, instance, path, errors); 082 083 List<Object> anyOf = schema.array("anyOf"); 084 if (anyOf != null) { 085 boolean accepted = false; 086 for (Object branch : anyOf) if (valid(branch, instance, path)) { accepted = true; break; } 087 if (!accepted) error(errors, path, printable(instance) + " is not valid under any of the given schemas"); 088 } 089 090 List<Object> oneOf = schema.array("oneOf"); 091 if (oneOf != null) { 092 int accepted = 0; 093 for (Object branch : oneOf) if (valid(branch, instance, path)) accepted++; 094 if (accepted != 1) error(errors, path, printable(instance) + " is valid under " + accepted + " oneOf schemas"); 095 } 096 097 if (schema.contains("not") && valid(schema.get("not"), instance, path)) { 098 error(errors, path, printable(instance) + " must not validate against the prohibited schema"); 099 } 100 101 if (schema.contains("if")) { 102 if (valid(schema.get("if"), instance, path)) { 103 if (schema.contains("then")) evaluate(schema.get("then"), instance, path, errors); 104 } else if (schema.contains("else")) { 105 evaluate(schema.get("else"), instance, path, errors); 106 } 107 } 108 } 109 110 private void evaluateObject(SlpcObject schema, SlpcObject instance, String path, List<SlpcValidationError> errors) { 111 List<Object> required = schema.array("required"); 112 if (required != null) { 113 for (Object name : required) { 114 if (!instance.contains((String) name)) error(errors, path, "'" + name + "' is a required property"); 115 } 116 } 117 SlpcObject properties = schema.object("properties"); 118 Set<String> known = new HashSet<String>(); 119 if (properties != null) { 120 known.addAll(properties.names()); 121 for (String name : properties.names()) { 122 if (instance.contains(name)) evaluate(properties.get(name), instance.get(name), child(path, name), errors); 123 } 124 } 125 if (schema.contains("additionalProperties")) { 126 Object additional = schema.get("additionalProperties"); 127 List<String> unexpected = new ArrayList<String>(); 128 for (String name : instance.names()) { 129 if (known.contains(name)) continue; 130 if (Boolean.FALSE.equals(additional)) unexpected.add(name); 131 else if (!Boolean.TRUE.equals(additional)) evaluate(additional, instance.get(name), child(path, name), errors); 132 } 133 if (!unexpected.isEmpty()) error(errors, path, "Additional properties are not allowed: " + unexpected); 134 } 135 } 136 137 private void evaluateArray(SlpcObject schema, List<?> instance, String path, List<SlpcValidationError> errors) { 138 BigInteger minimum = schema.integer("minItems"); 139 BigInteger maximum = schema.integer("maxItems"); 140 if (minimum != null && instance.size() < minimum.intValue()) error(errors, path, instance + " is too short"); 141 if (maximum != null && instance.size() > maximum.intValue()) error(errors, path, instance + " is too long"); 142 if (Boolean.TRUE.equals(schema.get("uniqueItems"))) { 143 for (int left = 0; left < instance.size(); left++) { 144 for (int right = left + 1; right < instance.size(); right++) { 145 if (jsonEquals(instance.get(left), instance.get(right))) { 146 error(errors, path, "array has non-unique elements at " + left + " and " + right); 147 } 148 } 149 } 150 } 151 if (schema.contains("items")) { 152 Object itemSchema = schema.get("items"); 153 if (itemSchema instanceof List<?>) { 154 List<?> tuple = (List<?>) itemSchema; 155 for (int index = 0; index < instance.size() && index < tuple.size(); index++) { 156 evaluate(tuple.get(index), instance.get(index), path + "[" + index + "]", errors); 157 } 158 } else { 159 for (int index = 0; index < instance.size(); index++) { 160 evaluate(itemSchema, instance.get(index), path + "[" + index + "]", errors); 161 } 162 } 163 } 164 if (schema.contains("contains")) { 165 boolean found = false; 166 for (int index = 0; index < instance.size(); index++) { 167 if (valid(schema.get("contains"), instance.get(index), path + "[" + index + "]")) { 168 found = true; 169 break; 170 } 171 } 172 if (!found) error(errors, path, "array does not contain an item matching the required schema"); 173 } 174 } 175 176 private void evaluateString(SlpcObject schema, String instance, String path, List<SlpcValidationError> errors) { 177 BigInteger minimum = schema.integer("minLength"); 178 BigInteger maximum = schema.integer("maxLength"); 179 int length = instance.codePointCount(0, instance.length()); 180 if (minimum != null && length < minimum.intValue()) error(errors, path, "'" + instance + "' is too short"); 181 if (maximum != null && length > maximum.intValue()) error(errors, path, "'" + instance + "' is too long"); 182 String pattern = schema.string("pattern"); 183 if (pattern != null && !Pattern.compile(pattern).matcher(instance).find()) { 184 error(errors, path, "'" + instance + "' does not match '" + pattern + "'"); 185 } 186 } 187 188 private void evaluateInteger(SlpcObject schema, BigInteger instance, String path, List<SlpcValidationError> errors) { 189 BigInteger minimum = schema.integer("minimum"); 190 BigInteger maximum = schema.integer("maximum"); 191 if (minimum != null && instance.compareTo(minimum) < 0) error(errors, path, instance + " is less than the minimum of " + minimum); 192 if (maximum != null && instance.compareTo(maximum) > 0) error(errors, path, instance + " is greater than the maximum of " + maximum); 193 } 194 195 private boolean valid(Object schema, Object instance, String path) { 196 ArrayList<SlpcValidationError> trial = new ArrayList<SlpcValidationError>(); 197 evaluate(schema, instance, path, trial); 198 return trial.isEmpty(); 199 } 200 201 private Object resolve(String ref) { 202 if (!ref.startsWith("#/")) throw new IllegalArgumentException("only local JSON Schema references are supported: " + ref); 203 Object current = rootSchema; 204 String[] segments = ref.substring(2).split("/"); 205 for (String encoded : segments) { 206 String segment = encoded.replace("~1", "/").replace("~0", "~"); 207 if (!(current instanceof SlpcObject) || !((SlpcObject) current).contains(segment)) { 208 throw new IllegalArgumentException("unresolved JSON Schema reference: " + ref); 209 } 210 current = ((SlpcObject) current).get(segment); 211 } 212 return current; 213 } 214 215 private boolean matchesType(Object declared, Object instance) { 216 if (declared instanceof String) return matchesTypeName((String) declared, instance); 217 if (declared instanceof List<?>) { 218 for (Object type : (List<?>) declared) if (matchesTypeName((String) type, instance)) return true; 219 return false; 220 } 221 throw new IllegalArgumentException("invalid type keyword"); 222 } 223 224 private boolean matchesTypeName(String type, Object instance) { 225 if ("null".equals(type)) return instance == null; 226 if ("object".equals(type)) return instance instanceof SlpcObject; 227 if ("array".equals(type)) return instance instanceof List<?>; 228 if ("string".equals(type)) return instance instanceof String; 229 if ("boolean".equals(type)) return instance instanceof Boolean; 230 if ("integer".equals(type) || "number".equals(type)) return instance instanceof BigInteger; 231 throw new IllegalArgumentException("unsupported JSON Schema type: " + type); 232 } 233 234 private static boolean containsJson(List<Object> values, Object target) { 235 for (Object value : values) if (jsonEquals(value, target)) return true; 236 return false; 237 } 238 239 private static boolean jsonEquals(Object left, Object right) { 240 return left == null ? right == null : left.equals(right); 241 } 242 243 private static String printable(Object value) { 244 if (value == null) return "null"; 245 if (value instanceof String) return "'" + value + "'"; 246 return String.valueOf(value); 247 } 248 249 private static String child(String path, String member) { 250 return member.matches("[A-Za-z_][A-Za-z0-9_]*") ? path + "." + member : path + "['" + member + "']"; 251 } 252 253 private static void error(List<SlpcValidationError> errors, String path, String message) { 254 errors.add(new SlpcValidationError(SlpcValidationError.Stage.SCHEMA, path, message)); 255 } 256}