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