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.marshall.jsonschema; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022import static org.apache.juneau.commons.utils.SystemUtils.*; 023import static org.apache.juneau.commons.utils.ThrowableUtils.*; 024 025import java.util.*; 026import java.util.concurrent.*; 027import java.util.regex.*; 028 029import org.apache.juneau.commons.bean.*; 030import org.apache.juneau.commons.collections.*; 031import org.apache.juneau.marshall.*; 032import org.apache.juneau.marshall.collections.*; 033import org.apache.juneau.marshall.json.*; 034import org.apache.juneau.marshall.json5.*; 035 036/** 037 * Generates JSON-schema metadata about POJOs. 038 * 039 * <h5 class='section'>Notes:</h5><ul> 040 * <li class='note'>This class is thread safe and reusable. 041 * </ul> 042 * 043 * <p> 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 */ 048@SuppressWarnings({ 049 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 050 "java:S1452" // Builder<?> wildcard return intentional; callers chain via fluent API without needing the concrete type 051}) 052public class JsonSchemaGenerator extends MarshallingTraverseContext implements JsonSchemaMetaProvider { 053 054 /** 055 * Builder class. 056 */ 057 public static class Builder extends MarshallingTraverseContext.Builder<Builder> { 058 059 private static final Cache<HashKey,JsonSchemaGenerator> CACHE = Cache.of(HashKey.class, JsonSchemaGenerator.class).build(); 060 061 protected final JsonParser.Builder<?> jsonParserBuilder; 062 protected final JsonSerializer.Builder<?> jsonSerializerBuilder; 063 064 private boolean allowNestedDescriptions; 065 private boolean allowNestedExamples; 066 private boolean useBeanDefs; 067 private Class<? extends MarshallingDefMapper> beanDefMapper; 068 private SortedSet<TypeCategory> addDescriptionsTo; 069 private SortedSet<TypeCategory> addExamplesTo; 070 private SortedSet<String> ignoreTypes; 071 072 /** 073 * Constructor, default settings. 074 */ 075 protected Builder() { 076 MarshallingContext.Builder bc = marshallingContext(); 077 jsonSerializerBuilder = JsonSerializer.create().marshallingContext(bc); 078 jsonParserBuilder = Json5Parser.create().marshallingContext(bc); 079 registerBuilders(jsonSerializerBuilder, jsonParserBuilder); 080 addDescriptionsTo = null; 081 addExamplesTo = null; 082 allowNestedDescriptions = env("JsonSchemaGenerator.allowNestedDescriptions", false); 083 allowNestedExamples = env("JsonSchemaGenerator.allowNestedExamples", false); 084 useBeanDefs = env("JsonSchemaGenerator.useBeanDefs", false); 085 beanDefMapper = BasicBeanDefMapper.class; 086 ignoreTypes = null; 087 } 088 089 /** 090 * Copy constructor. 091 * 092 * @param copyFrom The builder to copy from. 093 * <br>Cannot be <jk>null</jk>. 094 */ 095 protected Builder(Builder copyFrom) { 096 super(reqnn("copyFrom", copyFrom)); 097 MarshallingContext.Builder bc = marshallingContext(); 098 jsonSerializerBuilder = copyFrom.jsonSerializerBuilder.copy().marshallingContext(bc); 099 jsonParserBuilder = copyFrom.jsonParserBuilder.copy().marshallingContext(bc); 100 registerBuilders(jsonSerializerBuilder, jsonParserBuilder); 101 addDescriptionsTo = copyFrom.addDescriptionsTo == null ? null : new TreeSet<>(copyFrom.addDescriptionsTo); 102 addExamplesTo = copyFrom.addExamplesTo == null ? null : new TreeSet<>(copyFrom.addExamplesTo); 103 allowNestedDescriptions = copyFrom.allowNestedDescriptions; 104 allowNestedExamples = copyFrom.allowNestedExamples; 105 beanDefMapper = copyFrom.beanDefMapper; 106 ignoreTypes = copyFrom.ignoreTypes == null ? null : new TreeSet<>(copyFrom.ignoreTypes); 107 useBeanDefs = copyFrom.useBeanDefs; 108 } 109 110 /** 111 * Copy constructor. 112 * 113 * @param copyFrom The bean to copy from. 114 * <br>Cannot be <jk>null</jk>. 115 */ 116 protected Builder(JsonSchemaGenerator copyFrom) { 117 super(reqnn("copyFrom", copyFrom)); 118 MarshallingContext.Builder bc = marshallingContext(); 119 jsonSerializerBuilder = copyFrom.jsonSerializer.copy().marshallingContext(bc); 120 jsonParserBuilder = copyFrom.jsonParser.copy().marshallingContext(bc); 121 registerBuilders(jsonSerializerBuilder, jsonParserBuilder); 122 addDescriptionsTo = copyFrom.addDescriptionsTo.isEmpty() ? null : new TreeSet<>(copyFrom.addDescriptionsTo); 123 addExamplesTo = copyFrom.addExamplesTo.isEmpty() ? null : new TreeSet<>(copyFrom.addExamplesTo); 124 allowNestedDescriptions = copyFrom.allowNestedDescriptions; 125 allowNestedExamples = copyFrom.allowNestedExamples; 126 beanDefMapper = copyFrom.beanDefMapper; 127 ignoreTypes = copyFrom.ignoreTypes.isEmpty() ? null : new TreeSet<>(copyFrom.ignoreTypes); 128 useBeanDefs = copyFrom.useBeanDefs; 129 } 130 131 /** 132 * Add descriptions. 133 * 134 * <p> 135 * Identifies which categories of types that descriptions should be automatically added to generated schemas. 136 * The description is the result of calling {@link ClassMeta#getName()}. 137 * The format is a comma-delimited list of any of the following values: 138 * 139 * <ul class='javatree'> 140 * <li class='jf'>{@link TypeCategory#BEAN BEAN} 141 * <li class='jf'>{@link TypeCategory#COLLECTION COLLECTION} 142 * <li class='jf'>{@link TypeCategory#ARRAY ARRAY} 143 * <li class='jf'>{@link TypeCategory#MAP MAP} 144 * <li class='jf'>{@link TypeCategory#STRING STRING} 145 * <li class='jf'>{@link TypeCategory#NUMBER NUMBER} 146 * <li class='jf'>{@link TypeCategory#BOOLEAN BOOLEAN} 147 * <li class='jf'>{@link TypeCategory#ANY ANY} 148 * <li class='jf'>{@link TypeCategory#OTHER OTHER} 149 * </ul> 150 * 151 * @param values 152 * The values to add to this setting. 153 * <br>The default is an empty string. 154 * <br>Cannot contain <jk>null</jk> values. 155 * @return This object. 156 */ 157 public Builder addDescriptionsTo(TypeCategory...values) { 158 reqnns("values", values); 159 addDescriptionsTo = addAll(addDescriptionsTo, values); 160 return this; 161 } 162 163 /** 164 * Add examples. 165 * 166 * <p> 167 * Identifies which categories of types that examples should be automatically added to generated schemas. 168 * <p> 169 * The examples come from calling {@link ClassMeta#getExample(MarshallingSession,JsonParserSession)} which in turn gets examples 170 * from the following: 171 * <ul class='javatree'> 172 * <li class='ja'>{@link Example} 173 * <li class='ja'>{@link Marshalled#example() Marshalled(example)} 174 * </ul> 175 * 176 * <p> 177 * The format is a comma-delimited list of any of the following values: 178 * 179 * <ul class='javatree'> 180 * <li class='jf'>{@link TypeCategory#BEAN BEAN} 181 * <li class='jf'>{@link TypeCategory#COLLECTION COLLECTION} 182 * <li class='jf'>{@link TypeCategory#ARRAY ARRAY} 183 * <li class='jf'>{@link TypeCategory#MAP MAP} 184 * <li class='jf'>{@link TypeCategory#STRING STRING} 185 * <li class='jf'>{@link TypeCategory#NUMBER NUMBER} 186 * <li class='jf'>{@link TypeCategory#BOOLEAN BOOLEAN} 187 * <li class='jf'>{@link TypeCategory#ANY ANY} 188 * <li class='jf'>{@link TypeCategory#OTHER OTHER} 189 * </ul> 190 * 191 * @param values 192 * The values to add to this setting. 193 * <br>The default is an empty string. 194 * <br>Cannot contain <jk>null</jk> values. 195 * @return This object. 196 */ 197 public Builder addExamplesTo(TypeCategory...values) { 198 reqnns("values", values); 199 addExamplesTo = addAll(addExamplesTo, values); 200 return this; 201 } 202 203 /** 204 * Allow nested descriptions. 205 * 206 * <p> 207 * Identifies whether nested descriptions are allowed in schema definitions. 208 * 209 * @return This object. 210 */ 211 public Builder allowNestedDescriptions() { 212 return allowNestedDescriptions(true); 213 } 214 215 /** 216 * Same as {@link #allowNestedDescriptions()} but allows you to explicitly specify the value. 217 * 218 * @param value The value for this setting. 219 * @return This object. 220 */ 221 public Builder allowNestedDescriptions(boolean value) { 222 allowNestedDescriptions = value; 223 return this; 224 } 225 226 /** 227 * Allow nested examples. 228 * 229 * <p> 230 * Identifies whether nested examples are allowed in schema definitions. 231 * 232 * @return This object. 233 */ 234 public Builder allowNestedExamples() { 235 return allowNestedExamples(true); 236 } 237 238 /** 239 * Same as {@link #allowNestedExamples()} but allows you to explicitly specify the value. 240 * 241 * @param value The value for this setting. 242 * @return This object. 243 */ 244 public Builder allowNestedExamples(boolean value) { 245 allowNestedExamples = value; 246 return this; 247 } 248 249 /** 250 * Schema definition mapper. 251 * 252 * <p> 253 * Interface to use for converting Bean classes to definition IDs and URIs. 254 * <p> 255 * Used primarily for defining common definition sections for beans in Swagger JSON. 256 * <p> 257 * This setting is ignored if {@link JsonSchemaGenerator.Builder#useBeanDefs()} is not enabled. 258 * 259 * @param value 260 * The new value for this setting. 261 * <br>The default is {@link BasicBeanDefMapper}. 262 * <br>Cannot be <jk>null</jk>. 263 * @return This object. 264 */ 265 public Builder beanDefMapper(Class<? extends MarshallingDefMapper> value) { 266 beanDefMapper = reqnn("value", value); 267 return this; 268 } 269 270 @Override /* Overridden from Context.Builder<?> */ 271 public JsonSchemaGenerator build() { 272 return cache(CACHE).build(JsonSchemaGenerator.class); 273 } 274 275 @Override /* Overridden from Context.Builder<?> */ 276 public Builder copy() { 277 return new Builder(this); 278 } 279 280 /** 281 * Gives access to the inner JSON parser builder if you want to modify the parser settings. 282 * 283 * @return The JSON serializer builder. 284 */ 285 public JsonParser.Builder<?> getJsonParserBuilder() { return jsonParserBuilder; } 286 287 /** 288 * Gives access to the inner JSON serializer builder if you want to modify the serializer settings. 289 * 290 * @return The JSON serializer builder. 291 */ 292 public JsonSerializer.Builder<?> getJsonSerializerBuilder() { return jsonSerializerBuilder; } 293 294 @Override /* Overridden from Context.Builder<?> */ 295 public HashKey hashKey() { 296 // @formatter:off 297 return HashKey.of( 298 super.hashKey(), 299 jsonSerializerBuilder.hashKey(), 300 jsonParserBuilder.hashKey(), 301 addDescriptionsTo, 302 addExamplesTo, 303 allowNestedDescriptions, 304 allowNestedExamples, 305 useBeanDefs, 306 beanDefMapper, 307 ignoreTypes 308 ); 309 // @formatter:on 310 } 311 312 /** 313 * Ignore types from schema definitions. 314 * 315 * <h5 class='section'>Description:</h5> 316 * <p> 317 * Defines class name patterns that should be ignored when generating schema definitions in the generated 318 * Swagger documentation. 319 * 320 * <h5 class='section'>Example:</h5> 321 * <p class='bjava'> 322 * <jc>// Don't generate schema for any prototype packages or the class named 'Swagger'.</jc> 323 * <ja>@JsonSchemaConfig</ja>( 324 * ignoreTypes=<js>"Swagger,*.proto.*"</js> 325 * ) 326 * <jk>public class</jk> MyResource {...} 327 * </p> 328 * 329 * @param values 330 * The values to add. 331 * <br>Cannot contain <jk>null</jk> values. 332 * @return This object. 333 */ 334 public Builder ignoreTypes(String...values) { 335 reqnns("values", values); 336 ignoreTypes = addAll(ignoreTypes, values); 337 return this; 338 } 339 340 /** 341 * Use bean definitions. 342 * 343 * <p> 344 * When enabled, schemas on beans will be serialized as the following: 345 * <p class='bjson'> 346 * { 347 * type: <js>'object'</js>, 348 * <js>'$ref'</js>: <js>'#/definitions/TypeId'</js> 349 * } 350 * </p> 351 * 352 * <p> 353 * The definitions can then be retrieved from the session using {@link JsonSchemaGeneratorSession#getBeanDefs()}. 354 * <p> 355 * Definitions can also be added programmatically using {@link JsonSchemaGeneratorSession#addBeanDef(String, JsonMap)}. 356 * 357 * @return This object. 358 */ 359 public Builder useBeanDefs() { 360 return useBeanDefs(true); 361 } 362 363 /** 364 * Same as {@link #useBeanDefs()} but allows you to explicitly specify the value. 365 * 366 * @param value The value for this setting. 367 * @return This object. 368 */ 369 public Builder useBeanDefs(boolean value) { 370 useBeanDefs = value; 371 return this; 372 } 373 374 375 } 376 377 /** Default serializer, all default settings.*/ 378 public static final JsonSchemaGenerator DEFAULT = new JsonSchemaGenerator(create()); 379 380 /** 381 * Creates a new builder for this object. 382 * 383 * @return A new builder. 384 */ 385 public static Builder create() { 386 return new Builder(); 387 } 388 389 protected final boolean allowNestedDescriptions; 390 protected final boolean allowNestedExamples; 391 protected final boolean useBeanDefs; 392 protected final Class<? extends MarshallingDefMapper> beanDefMapper; 393 protected final JsonParser jsonParser; 394 protected final JsonSerializer jsonSerializer; 395 protected final Set<TypeCategory> addDescriptionsTo; 396 protected final Set<TypeCategory> addExamplesTo; 397 protected final Set<String> ignoreTypes; 398 private final MarshallingDefMapper beanDefMapperBean; 399 private final Map<BeanPropertyMeta,JsonSchemaBeanPropertyMeta> jsonSchemaBeanPropertyMetas = new ConcurrentHashMap<>(); 400 private final Map<ClassMeta<?>,JsonSchemaClassMeta> jsonSchemaClassMetas = new ConcurrentHashMap<>(); 401 private final List<Pattern> ignoreTypePatterns; 402 403 /** 404 * Constructor. 405 * 406 * @param builder The builder for this object. Cannot be <jk>null</jk>. 407 */ 408 public JsonSchemaGenerator(Builder builder) { 409 super(builder.detectRecursions().ignoreRecursions()); 410 411 addDescriptionsTo = builder.addDescriptionsTo == null ? Collections.emptySet() : new TreeSet<>(builder.addDescriptionsTo); 412 addExamplesTo = builder.addExamplesTo == null ? Collections.emptySet() : new TreeSet<>(builder.addExamplesTo); 413 allowNestedDescriptions = builder.allowNestedDescriptions; 414 allowNestedExamples = builder.allowNestedExamples; 415 beanDefMapper = builder.beanDefMapper; 416 ignoreTypes = builder.ignoreTypes == null ? Collections.emptySet() : new TreeSet<>(builder.ignoreTypes); 417 useBeanDefs = builder.useBeanDefs; 418 419 Set<Pattern> ignoreTypePatterns2 = set(); 420 ignoreTypes.forEach(y -> split(y, x -> ignoreTypePatterns2.add(Pattern.compile(x.replace(".", "\\.").replace("*", ".*"))))); 421 this.ignoreTypePatterns = u(new ArrayList<>(ignoreTypePatterns2)); 422 423 try { 424 beanDefMapperBean = beanDefMapper.getDeclaredConstructor().newInstance(); 425 } catch (Exception e) { 426 throw toRex(e); 427 } 428 429 jsonSerializer = builder.jsonSerializerBuilder.build(); 430 jsonParser = builder.jsonParserBuilder.marshallingContext(getMarshallingContext()).build(); 431 } 432 433 @Override /* Overridden from Context */ 434 public Builder copy() { 435 return new Builder(this); 436 } 437 438 @Override /* Overridden from Context */ 439 public JsonSchemaGeneratorSession.Builder createSession() { 440 return JsonSchemaGeneratorSession.create(this); 441 } 442 443 /** 444 * Ignore types from schema definitions. 445 * 446 * @see Builder#ignoreTypes(String...) 447 * @return 448 * Custom schema information for particular class types. 449 * <br>Never <jk>null</jk>. 450 * <br>List is unmodifiable. 451 */ 452 public List<Pattern> getIgnoreTypes() { return ignoreTypePatterns; } 453 454 @Override 455 public JsonSchemaBeanPropertyMeta getJsonSchemaBeanPropertyMeta(BeanPropertyMeta bpm) { 456 return jsonSchemaBeanPropertyMetas.computeIfAbsent(bpm, k -> new JsonSchemaBeanPropertyMeta(k, this)); 457 } 458 459 @Override 460 public JsonSchemaClassMeta getJsonSchemaClassMeta(ClassMeta<?> cm) { 461 return jsonSchemaClassMetas.computeIfAbsent(cm, k -> new JsonSchemaClassMeta(k, this)); 462 } 463 464 @Override /* Overridden from Context */ 465 public JsonSchemaGeneratorSession getSession() { return createSession().build(); } 466 467 /** 468 * Returns <jk>true</jk> if the specified type is ignored. 469 * 470 * <p> 471 * The type is ignored if it's specified in the {@link Builder#ignoreTypes(String...)} setting. 472 * <br>Ignored types return <jk>null</jk> on the call to {@link JsonSchemaGeneratorSession#getSchema(ClassMeta)}. 473 * 474 * @param cm The type to check. Must not be <jk>null</jk>. 475 * @return <jk>true</jk> if the specified type is ignored. 476 */ 477 public boolean isIgnoredType(ClassMeta<?> cm) { 478 for (var p : ignoreTypePatterns) 479 if (p.matcher(cm.getNameSimple()).matches() || p.matcher(cm.getName()).matches()) 480 return true; 481 return false; 482 } 483 484 /** 485 * Add descriptions to types. 486 * 487 * @see Builder#addDescriptionsTo(TypeCategory...) 488 * @return 489 * Set of categories of types that descriptions should be automatically added to generated schemas. 490 */ 491 protected final Set<TypeCategory> getAddDescriptionsTo() { return addDescriptionsTo; } 492 493 /** 494 * Add examples. 495 * 496 * @see Builder#addExamplesTo(TypeCategory...) 497 * @return 498 * Set of categories of types that examples should be automatically added to generated schemas. 499 */ 500 protected final Set<TypeCategory> getAddExamplesTo() { return addExamplesTo; } 501 502 /** 503 * Bean schema definition mapper. 504 * 505 * @see Builder#beanDefMapper(Class) 506 * @return 507 * Interface to use for converting Bean classes to definition IDs and URIs. 508 */ 509 protected final MarshallingDefMapper getBeanDefMapper() { return beanDefMapperBean; } 510 511 /** 512 * Allow nested descriptions. 513 * 514 * @see Builder#allowNestedDescriptions() 515 * @return 516 * <jk>true</jk> if nested descriptions are allowed in schema definitions. 517 */ 518 protected final boolean isAllowNestedDescriptions() { return allowNestedDescriptions; } 519 520 /** 521 * Allow nested examples. 522 * 523 * @see Builder#allowNestedExamples() 524 * @return 525 * <jk>true</jk> if nested examples are allowed in schema definitions. 526 */ 527 protected final boolean isAllowNestedExamples() { return allowNestedExamples; } 528 529 /** 530 * Use bean definitions. 531 * 532 * @see Builder#useBeanDefs() 533 * @return 534 * <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags. 535 */ 536 protected final boolean isUseBeanDefs() { return useBeanDefs; } 537 538 @Override /* Overridden from MarshallingTraverseContext */ 539 protected FluentMap<String,Object> properties() { 540 return super.properties() 541 .a("addDescriptionsTo", addDescriptionsTo) 542 .a("addExamplesTo", addExamplesTo) 543 .a("allowNestedDescriptions", allowNestedDescriptions) 544 .a("allowNestedExamples", allowNestedExamples) 545 .a("beanDefMapper", beanDefMapper) 546 .a("ignoreTypes", ignoreTypes) 547 .a("useBeanDefs", useBeanDefs); 548 } 549 550 JsonParser getJsonParser() { return jsonParser; } 551 552 JsonSerializer getJsonSerializer() { return jsonSerializer; } 553}