001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.jsonschema; 018 019import static org.apache.juneau.commons.utils.AssertionUtils.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.Utils.*; 022import static org.apache.juneau.jsonschema.TypeCategory.*; 023 024import java.lang.reflect.*; 025import java.util.*; 026import java.util.function.*; 027import java.util.regex.*; 028 029import org.apache.juneau.*; 030import org.apache.juneau.annotation.*; 031import org.apache.juneau.collections.*; 032import org.apache.juneau.commons.utils.*; 033import org.apache.juneau.json.*; 034import org.apache.juneau.parser.*; 035import org.apache.juneau.serializer.*; 036 037/** 038 * Session object that lives for the duration of a single use of {@link JsonSchemaSerializer}. 039 * 040 * <h5 class='section'>Notes:</h5><ul> 041 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 042 * </ul> 043 * 044 * <h5 class='section'>See Also:</h5><ul> 045 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JsonSchemaDetails">JSON-Schema Support</a> 046 * </ul> 047 */ 048public class JsonSchemaGeneratorSession extends BeanTraverseSession { 049 /** 050 * Builder class. 051 */ 052 public static class Builder extends BeanTraverseSession.Builder { 053 054 private JsonSchemaGenerator ctx; 055 056 /** 057 * Constructor 058 * 059 * @param ctx The context creating this session. 060 * <br>Cannot be <jk>null</jk>. 061 */ 062 protected Builder(JsonSchemaGenerator ctx) { 063 super(assertArgNotNull("ctx", ctx)); 064 this.ctx = ctx; 065 } 066 067 @Override /* Overridden from Builder */ 068 public <T> Builder apply(Class<T> type, Consumer<T> apply) { 069 super.apply(type, apply); 070 return this; 071 } 072 073 @Override 074 public JsonSchemaGeneratorSession build() { 075 return new JsonSchemaGeneratorSession(this); 076 } 077 078 @Override /* Overridden from Builder */ 079 public Builder debug(Boolean value) { 080 super.debug(value); 081 return this; 082 } 083 084 @Override /* Overridden from Builder */ 085 public Builder locale(Locale value) { 086 super.locale(value); 087 return this; 088 } 089 090 @Override /* Overridden from Builder */ 091 public Builder mediaType(MediaType value) { 092 super.mediaType(value); 093 return this; 094 } 095 096 @Override /* Overridden from Builder */ 097 public Builder mediaTypeDefault(MediaType value) { 098 super.mediaTypeDefault(value); 099 return this; 100 } 101 102 @Override /* Overridden from Builder */ 103 public Builder properties(Map<String,Object> value) { 104 super.properties(value); 105 return this; 106 } 107 108 @Override /* Overridden from Builder */ 109 public Builder property(String key, Object value) { 110 super.property(key, value); 111 return this; 112 } 113 114 @Override /* Overridden from Builder */ 115 public Builder timeZone(TimeZone value) { 116 super.timeZone(value); 117 return this; 118 } 119 120 @Override /* Overridden from Builder */ 121 public Builder timeZoneDefault(TimeZone value) { 122 super.timeZoneDefault(value); 123 return this; 124 } 125 126 @Override /* Overridden from Builder */ 127 public Builder unmodifiable() { 128 super.unmodifiable(); 129 return this; 130 } 131 } 132 133 /** 134 * Creates a new builder for this object. 135 * 136 * @param ctx The context creating this session. 137 * <br>Cannot be <jk>null</jk>. 138 * @return A new builder. 139 */ 140 public static Builder create(JsonSchemaGenerator ctx) { 141 return new Builder(assertArgNotNull("ctx", ctx)); 142 } 143 144 private final JsonSchemaGenerator ctx; 145 private final Map<String,JsonMap> defs; 146 private JsonParserSession jpSession; 147 private JsonSerializerSession jsSession; 148 149 /** 150 * Constructor. 151 * 152 * @param builder The builder for this object. 153 */ 154 protected JsonSchemaGeneratorSession(Builder builder) { 155 super(builder); 156 ctx = builder.ctx; 157 defs = isUseBeanDefs() ? new TreeMap<>() : null; 158 } 159 160 /** 161 * Adds a schema definition to this session. 162 * 163 * @param id The definition ID. 164 * <br>Cannot be <jk>null</jk>. 165 * @param def The definition schema. 166 * <br>Cannot be <jk>null</jk>. 167 * @return This object. 168 */ 169 public JsonSchemaGeneratorSession addBeanDef(String id, JsonMap def) { 170 if (nn(defs)) 171 defs.put(assertArgNotNull("id", id), assertArgNotNull("def", def)); 172 return this; 173 } 174 175 /** 176 * Returns the definition ID for the specified class. 177 * 178 * @param cm The class to get the definition ID of. 179 * @return The definition ID for the specified class. 180 */ 181 public String getBeanDefId(ClassMeta<?> cm) { 182 return getBeanDefMapper().getId(cm); 183 } 184 185 /** 186 * Returns the definitions that were gathered during this session. 187 * 188 * <p> 189 * This map is modifiable and affects the map in the session. 190 * 191 * @return 192 * The definitions that were gathered during this session, or <jk>null</jk> if {@link JsonSchemaGenerator.Builder#useBeanDefs()} was not enabled. 193 */ 194 public Map<String,JsonMap> getBeanDefs() { return defs; } 195 196 /** 197 * Returns the definition URI for the specified class. 198 * 199 * @param cm The class to get the definition URI of. 200 * @return The definition URI for the specified class. 201 */ 202 public java.net.URI getBeanDefUri(ClassMeta<?> cm) { 203 return getBeanDefMapper().getURI(cm); 204 } 205 206 /** 207 * Returns the definition URI for the specified class. 208 * 209 * @param id The definition ID to get the definition URI of. 210 * @return The definition URI for the specified class. 211 */ 212 public java.net.URI getBeanDefUri(String id) { 213 return getBeanDefMapper().getURI(id); 214 } 215 216 /** 217 * Returns the language-specific metadata on the specified bean property. 218 * 219 * @param bpm The bean property to return the metadata on. 220 * @return The metadata. 221 */ 222 public JsonSchemaBeanPropertyMeta getJsonSchemaBeanPropertyMeta(BeanPropertyMeta bpm) { 223 return ctx.getJsonSchemaBeanPropertyMeta(bpm); 224 } 225 226 /** 227 * Returns the language-specific metadata on the specified class. 228 * 229 * @param cm The class to return the metadata on. 230 * @return The metadata. 231 */ 232 public JsonSchemaClassMeta getJsonSchemaClassMeta(ClassMeta<?> cm) { 233 return ctx.getJsonSchemaClassMeta(cm); 234 } 235 236 /** 237 * Returns the JSON-schema for the specified type. 238 * 239 * @param cm The object type. 240 * @return The schema for the type. 241 * @throws BeanRecursionException Bean recursion occurred. 242 * @throws SerializeException Error occurred. 243 */ 244 public JsonMap getSchema(ClassMeta<?> cm) throws BeanRecursionException, SerializeException { 245 return getSchema(cm, "root", null, false, false, null); 246 } 247 248 /** 249 * Returns the JSON-schema for the specified object. 250 * 251 * @param o 252 * The object. 253 * <br>Can either be a POJO or a <c>Class</c>/<c>Type</c>. 254 * @return The schema for the type. 255 * @throws BeanRecursionException Bean recursion occurred. 256 * @throws SerializeException Error occurred. 257 */ 258 public JsonMap getSchema(Object o) throws BeanRecursionException, SerializeException { 259 return getSchema(toClassMeta(o), "root", null, false, false, null); 260 } 261 262 /** 263 * Returns the JSON-schema for the specified type. 264 * 265 * @param type The object type. 266 * @return The schema for the type. 267 * @throws BeanRecursionException Bean recursion occurred. 268 * @throws SerializeException Error occurred. 269 */ 270 public JsonMap getSchema(Type type) throws BeanRecursionException, SerializeException { 271 return getSchema(getClassMeta(type), "root", null, false, false, null); 272 } 273 274 private Object getDescription(ClassMeta<?> sType, TypeCategory t, boolean descriptionAdded) { 275 var canAdd = isAllowNestedDescriptions() || ! descriptionAdded; 276 if (canAdd && (getAddDescriptionsTo().contains(t) || getAddDescriptionsTo().contains(ANY))) 277 return sType.toString(); 278 return null; 279 } 280 281 @SuppressWarnings("unchecked") 282 private static List<String> getEnums(ClassMeta<?> cm) { 283 List<String> l = list(); 284 for (var e : ((Class<Enum<?>>)cm.inner()).getEnumConstants()) 285 l.add(cm.toString(e)); 286 return l; 287 } 288 289 private Object getExample(ClassMeta<?> sType, TypeCategory t, boolean exampleAdded) throws SerializeException { 290 var canAdd = isAllowNestedExamples() || ! exampleAdded; 291 if (canAdd && (getAddExamplesTo().contains(t) || getAddExamplesTo().contains(ANY))) { 292 var example = sType.getExample(this, jpSession()); 293 if (nn(example)) { 294 try { 295 return JsonParser.DEFAULT.parse(toJson(example), Object.class); 296 } catch (ParseException e) { 297 throw new SerializeException(e); 298 } 299 } 300 } 301 return null; 302 } 303 304 @SuppressWarnings({ "unchecked", "rawtypes" }) 305 private JsonMap getSchema(ClassMeta<?> eType, String attrName, List<String> pNames, boolean exampleAdded, boolean descriptionAdded, JsonSchemaBeanPropertyMeta jsbpm) 306 throws BeanRecursionException, SerializeException { 307 308 if (ctx.isIgnoredType(eType)) 309 return null; 310 311 var out = new JsonMap(); 312 313 if (eType == null) 314 eType = object(); 315 316 var aType = (ClassMeta<?>)null; // The actual type (will be null if recursion occurs) 317 var sType = (ClassMeta<?>)null; // The serialized type 318 var objectSwap = eType.getSwap(this); 319 320 aType = push(attrName, eType, null); 321 322 sType = eType.getSerializedClassMeta(this); 323 324 var type = (String)null; 325 var format = (String)null; 326 var example = (Object)null; 327 var description = (Object)null; 328 329 boolean useDef = isUseBeanDefs() && sType.isBean() && pNames == null; 330 331 if (useDef) { 332 exampleAdded = false; 333 descriptionAdded = false; 334 } 335 336 if (useDef && defs.containsKey(getBeanDefId(sType))) { 337 pop(); 338 return new JsonMap().append("$ref", getBeanDefUri(sType)); 339 } 340 341 var jscm = (JsonSchemaClassMeta)null; 342 var objectSwapCM = objectSwap == null ? null : getClassMeta(objectSwap.getClass()); 343 if (nn(objectSwapCM) && getAnnotationProvider().has(Schema.class, objectSwapCM)) 344 jscm = getJsonSchemaClassMeta(objectSwapCM); 345 if (jscm == null) 346 jscm = getJsonSchemaClassMeta(sType); 347 348 var tc = (TypeCategory)null; 349 350 if (sType.isNumber()) { 351 tc = NUMBER; 352 if (sType.isDecimal()) { 353 type = "number"; 354 if (sType.isFloat()) { 355 format = "float"; 356 } else if (sType.isDouble()) { 357 format = "double"; 358 } 359 } else { 360 type = "integer"; 361 if (sType.isShort()) { 362 format = "int16"; 363 } else if (sType.isInteger()) { 364 format = "int32"; 365 } else if (sType.isLong()) { 366 format = "int64"; 367 } 368 } 369 } else if (sType.isBoolean()) { 370 tc = BOOLEAN; 371 type = "boolean"; 372 } else if (sType.isMap()) { 373 tc = MAP; 374 type = "object"; 375 } else if (sType.isBean()) { 376 tc = BEAN; 377 type = "object"; 378 } else if (sType.isCollection()) { 379 tc = COLLECTION; 380 type = "array"; 381 } else if (sType.isArray()) { 382 tc = ARRAY; 383 type = "array"; 384 } else if (sType.isEnum()) { 385 tc = ENUM; 386 type = "string"; 387 } else if (sType.isCharSequence() || sType.isChar()) { 388 tc = STRING; 389 type = "string"; 390 } else if (sType.isUri()) { 391 tc = STRING; 392 type = "string"; 393 format = "uri"; 394 } else { 395 tc = STRING; 396 type = "string"; 397 } 398 399 // Add info from @Schema on bean property. 400 if (nn(jsbpm)) { 401 out.append(jsbpm.getSchema()); 402 } 403 404 out.append(jscm.getSchema()); 405 406 Predicate<String> ne = Utils::ne; 407 out.appendIfAbsentIf(ne, "type", type); 408 out.appendIfAbsentIf(ne, "format", format); 409 410 if (nn(aType)) { 411 412 example = getExample(sType, tc, exampleAdded); 413 description = getDescription(sType, tc, descriptionAdded); 414 exampleAdded |= nn(example); 415 descriptionAdded |= nn(description); 416 417 if (tc == BEAN) { 418 var properties = new JsonMap(); 419 BeanMeta bm = getBeanMeta(sType.inner()); 420 if (nn(pNames)) 421 bm = new BeanMetaFiltered(bm, pNames); 422 for (Iterator<BeanPropertyMeta> i = bm.getProperties().values().iterator(); i.hasNext();) { 423 BeanPropertyMeta p = i.next(); 424 if (p.canRead()) { 425 var pProps = p.getProperties(); 426 properties.put(p.getName(), getSchema(p.getClassMeta(), p.getName(), pProps, exampleAdded, descriptionAdded, getJsonSchemaBeanPropertyMeta(p))); 427 } 428 } 429 out.put("properties", properties); 430 431 } else if (tc == COLLECTION) { 432 ClassMeta et = sType.getElementType(); 433 if (sType.isCollection() && sType.isChildOf(Set.class)) 434 out.put("uniqueItems", true); 435 out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null)); 436 437 } else if (tc == ARRAY) { 438 ClassMeta et = sType.getElementType(); 439 if (sType.isCollection() && sType.isChildOf(Set.class)) 440 out.put("uniqueItems", true); 441 out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null)); 442 443 } else if (tc == ENUM) { 444 out.put("enum", getEnums(sType)); 445 446 } else if (tc == MAP) { 447 var om = getSchema(sType.getValueType(), "additionalProperties", null, exampleAdded, descriptionAdded, null); 448 if (! om.isEmpty()) 449 out.put("additionalProperties", om); 450 451 } 452 } 453 454 out.append(jscm.getSchema()); 455 456 Predicate<Object> neo = Utils::ne; 457 out.appendIfAbsentIf(neo, "description", description); 458 out.appendIfAbsentIf(neo, "example", example); 459 460 if (useDef) { 461 defs.put(getBeanDefId(sType), out); 462 out = JsonMap.of("$ref", getBeanDefUri(sType)); 463 } 464 465 pop(); 466 467 return out; 468 } 469 470 private JsonParserSession jpSession() { 471 if (jpSession == null) 472 jpSession = ctx.getJsonParser().getSession(); 473 return jpSession; 474 } 475 476 private ClassMeta<?> toClassMeta(Object o) { 477 if (o instanceof Type o2) 478 return getClassMeta(o2); 479 return getClassMetaForObject(o); 480 } 481 482 private String toJson(Object o) throws SerializeException { 483 if (jsSession == null) 484 jsSession = ctx.getJsonSerializer().getSession(); 485 return jsSession.serializeToString(o); 486 } 487 488 /** 489 * Add descriptions to types. 490 * 491 * @see JsonSchemaGenerator.Builder#addDescriptionsTo(TypeCategory...) 492 * @return 493 * Set of categories of types that descriptions should be automatically added to generated schemas. 494 */ 495 protected final Set<TypeCategory> getAddDescriptionsTo() { return ctx.getAddDescriptionsTo(); } 496 497 /** 498 * Add examples. 499 * 500 * @see JsonSchemaGenerator.Builder#addExamplesTo(TypeCategory...) 501 * @return 502 * Set of categories of types that examples should be automatically added to generated schemas. 503 */ 504 protected final Set<TypeCategory> getAddExamplesTo() { return ctx.getAddExamplesTo(); } 505 506 /** 507 * Bean schema definition mapper. 508 * 509 * @see JsonSchemaGenerator.Builder#beanDefMapper(Class) 510 * @return 511 * Interface to use for converting Bean classes to definition IDs and URIs. 512 */ 513 protected final BeanDefMapper getBeanDefMapper() { return ctx.getBeanDefMapper(); } 514 515 /** 516 * Ignore types from schema definitions. 517 * 518 * @see JsonSchemaGenerator.Builder#ignoreTypes(String...) 519 * @return 520 * Custom schema information for particular class types. 521 */ 522 protected final List<Pattern> getIgnoreTypes() { return ctx.getIgnoreTypes(); } 523 524 /** 525 * Allow nested descriptions. 526 * 527 * @see JsonSchemaGenerator.Builder#allowNestedDescriptions() 528 * @return 529 * <jk>true</jk> if nested descriptions are allowed in schema definitions. 530 */ 531 protected final boolean isAllowNestedDescriptions() { return ctx.isAllowNestedDescriptions(); } 532 533 /** 534 * Allow nested examples. 535 * 536 * @see JsonSchemaGenerator.Builder#allowNestedExamples() 537 * @return 538 * <jk>true</jk> if nested examples are allowed in schema definitions. 539 */ 540 protected final boolean isAllowNestedExamples() { return ctx.isAllowNestedExamples(); } 541 542 /** 543 * Use bean definitions. 544 * 545 * @see JsonSchemaGenerator.Builder#useBeanDefs() 546 * @return 547 * <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags. 548 */ 549 protected final boolean isUseBeanDefs() { return ctx.isUseBeanDefs(); } 550}