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