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.StringUtils.*; 016 017import java.util.*; 018import java.util.regex.*; 019 020import org.apache.juneau.*; 021import org.apache.juneau.annotation.*; 022import org.apache.juneau.json.*; 023 024/** 025 * Generates JSON-schema metadata about POJOs. 026 */ 027@ConfigurableContext 028public class JsonSchemaGenerator extends BeanTraverseContext { 029 030 //------------------------------------------------------------------------------------------------------------------- 031 // Configurable properties 032 //------------------------------------------------------------------------------------------------------------------- 033 034 static final String PREFIX = "JsonSchemaGenerator"; 035 036 /** 037 * Configuration property: Add descriptions to types. 038 * 039 * <h5 class='section'>Property:</h5> 040 * <ul> 041 * <li><b>Name:</b> <js>"JsonSchemaGenerator.addDescriptionsTo.s"</js> 042 * <li><b>Data type:</b> <c>String</c> 043 * <li><b>Default:</b> Empty string. 044 * <li><b>Session property:</b> <jk>false</jk> 045 * <li><b>Methods:</b> 046 * <ul> 047 * <li class='jm'>{@link JsonSchemaGeneratorBuilder#addDescriptionsTo(String)} 048 * </ul> 049 * </ul> 050 * 051 * <h5 class='section'>Description:</h5> 052 * <p> 053 * Identifies which categories of types that descriptions should be automatically added to generated schemas. 054 * <p> 055 * The description is the result of calling {@link ClassMeta#getFullName()}. 056 * <p> 057 * The format is a comma-delimited list of any of the following values: 058 * 059 * <ul class='javatree'> 060 * <li class='jf'>{@link TypeCategory#BEAN BEAN} 061 * <li class='jf'>{@link TypeCategory#COLLECTION COLLECTION} 062 * <li class='jf'>{@link TypeCategory#ARRAY ARRAY} 063 * <li class='jf'>{@link TypeCategory#MAP MAP} 064 * <li class='jf'>{@link TypeCategory#STRING STRING} 065 * <li class='jf'>{@link TypeCategory#NUMBER NUMBER} 066 * <li class='jf'>{@link TypeCategory#BOOLEAN BOOLEAN} 067 * <li class='jf'>{@link TypeCategory#ANY ANY} 068 * <li class='jf'>{@link TypeCategory#OTHER OTHER} 069 * </ul> 070 */ 071 public static final String JSONSCHEMA_addDescriptionsTo = PREFIX + ".addDescriptionsTo.s"; 072 073 /** 074 * Configuration property: Add examples. 075 * 076 * <h5 class='section'>Property:</h5> 077 * <ul> 078 * <li><b>Name:</b> <js>"JsonSchemaGenerator.addExamplesTo.s"</js> 079 * <li><b>Data type:</b> <c>String</c> 080 * <li><b>Default:</b> Empty string. 081 * <li><b>Session property:</b> <jk>false</jk> 082 * <li><b>Methods:</b> 083 * <ul> 084 * <li class='jm'>{@link JsonSchemaGeneratorBuilder#addExamplesTo(String)} 085 * </ul> 086 * </ul> 087 * 088 * <h5 class='section'>Description:</h5> 089 * <p> 090 * Identifies which categories of types that examples should be automatically added to generated schemas. 091 * <p> 092 * The examples come from calling {@link ClassMeta#getExample(BeanSession)} which in turn gets examples 093 * from the following: 094 * <ul class='javatree'> 095 * <li class='ja'>{@link Example} 096 * <li class='jf'>{@link BeanContext#BEAN_examples} 097 * </ul> 098 * 099 * <p> 100 * The format is a comma-delimited list of any of the following values: 101 * 102 * <ul class='javatree'> 103 * <li class='jf'>{@link TypeCategory#BEAN BEAN} 104 * <li class='jf'>{@link TypeCategory#COLLECTION COLLECTION} 105 * <li class='jf'>{@link TypeCategory#ARRAY ARRAY} 106 * <li class='jf'>{@link TypeCategory#MAP MAP} 107 * <li class='jf'>{@link TypeCategory#STRING STRING} 108 * <li class='jf'>{@link TypeCategory#NUMBER NUMBER} 109 * <li class='jf'>{@link TypeCategory#BOOLEAN BOOLEAN} 110 * <li class='jf'>{@link TypeCategory#ANY ANY} 111 * <li class='jf'>{@link TypeCategory#OTHER OTHER} 112 * </ul> 113 */ 114 public static final String JSONSCHEMA_addExamplesTo = PREFIX + ".addExamplesTo.s"; 115 116 /** 117 * Configuration property: Allow nested descriptions. 118 * 119 * <h5 class='section'>Property:</h5> 120 * <ul> 121 * <li><b>Name:</b> <js>"JsonSchemaGenerator.allowNestedDescriptions.b"</js> 122 * <li><b>Data type:</b> <c>Boolean</c> 123 * <li><b>Default:</b> <jk>false</jk> 124 * <li><b>Session property:</b> <jk>false</jk> 125 * <li><b>Methods:</b> 126 * <ul> 127 * <li class='jm'>{@link JsonSchemaGeneratorBuilder#allowNestedDescriptions()} 128 * </ul> 129 * </ul> 130 * 131 * <h5 class='section'>Description:</h5> 132 * <p> 133 * Identifies whether nested descriptions are allowed in schema definitions. 134 */ 135 public static final String JSONSCHEMA_allowNestedDescriptions = PREFIX + ".allowNestedDescriptions.b"; 136 137 /** 138 * Configuration property: Allow nested examples. 139 * 140 * <h5 class='section'>Property:</h5> 141 * <ul> 142 * <li><b>Name:</b> <js>"JsonSchemaGenerator.allowNestedExamples.b"</js> 143 * <li><b>Data type:</b> <c>Boolean</c> 144 * <li><b>Default:</b> <jk>false</jk> 145 * <li><b>Session property:</b> <jk>false</jk> 146 * <li><b>Methods:</b> 147 * <ul> 148 * <li class='jm'>{@link JsonSchemaGeneratorBuilder#allowNestedExamples()} 149 * </ul> 150 * </ul> 151 * 152 * <h5 class='section'>Description:</h5> 153 * <p> 154 * Identifies whether nested examples are allowed in schema definitions. 155 */ 156 public static final String JSONSCHEMA_allowNestedExamples = PREFIX + ".allowNestedExamples.b"; 157 158 /** 159 * Configuration property: Bean schema definition mapper. 160 * 161 * <h5 class='section'>Property:</h5> 162 * <ul> 163 * <li><b>Name:</b> <js>"JsonSchemaGenerator.beanDefMapper.o"</js> 164 * <li><b>Data type:</b> {@link BeanDefMapper} 165 * <li><b>Default:</b> {@link BasicBeanDefMapper} 166 * <li><b>Session property:</b> <jk>false</jk> 167 * <li><b>Methods:</b> 168 * <ul> 169 * <li class='jm'>{@link JsonSchemaGeneratorBuilder#beanDefMapper(Class)} 170 * <li class='jm'>{@link JsonSchemaGeneratorBuilder#beanDefMapper(BeanDefMapper)} 171 * </ul> 172 * </ul> 173 * 174 * <h5 class='section'>Description:</h5> 175 * <p> 176 * Interface to use for converting Bean classes to definition IDs and URIs. 177 * <p> 178 * Used primarily for defining common definition sections for beans in Swagger JSON. 179 * <p> 180 * This setting is ignored if {@link #JSONSCHEMA_useBeanDefs} is not enabled. 181 */ 182 public static final String JSONSCHEMA_beanDefMapper = PREFIX + ".beanDefMapper.o"; 183 184 /** 185 * Configuration property: Default schemas. 186 * 187 * <h5 class='section'>Property:</h5> 188 * <ul> 189 * <li><b>Name:</b> <js>"JsonSchemaGenerator.defaultSchema.smo"</js> 190 * <li><b>Data type:</b> <c>Map<String,ObjectMap></c> 191 * <li><b>Default:</b> Empty map. 192 * <li><b>Session property:</b> <jk>false</jk> 193 * <li><b>Methods:</b> 194 * <ul> 195 * <li class='jm'>{@link JsonSchemaGeneratorBuilder#defaultSchema(Class,ObjectMap)} 196 * </ul> 197 * </ul> 198 * 199 * <h5 class='section'>Description:</h5> 200 * <p> 201 * Allows you to override or provide custom schema information for particular class types. 202 * <p> 203 * Keys are full class names. 204 */ 205 public static final String JSONSCHEMA_defaultSchemas = PREFIX + ".defaultSchemas.smo"; 206 207 /** 208 * Configuration property: Ignore types from schema definitions. 209 * 210 * <h5 class='section'>Property:</h5> 211 * <ul> 212 * <li><b>Name:</b> <js>"JsonSchemaGenerator.ignoreTypes.s"</js> 213 * <li><b>Data type:</b> <c>String</c> (comma-delimited) 214 * <li><b>Default:</b> <jk>null</jk>. 215 * <li><b>Session property:</b> <jk>false</jk> 216 * </ul> 217 * 218 * <h5 class='section'>Description:</h5> 219 * <p> 220 * Defines class name patterns that should be ignored when generating schema definitions in the generated 221 * Swagger documentation. 222 * 223 * <h5 class='section'>Example:</h5> 224 * <p class='bcode w800'> 225 * <jc>// Don't generate schema for any prototype packages or the class named 'Swagger'. 226 * <ja>@RestResource</ja>( 227 * properties={ 228 * <ja>@Property</ja>(name=<jsf>JSONSCHEMA_ignoreTypes</jsf>, value=<js>"Swagger,*.proto.*"</js>) 229 * } 230 * <jk>public class</jk> MyResource {...} 231 * </p> 232 */ 233 public static final String JSONSCHEMA_ignoreTypes = PREFIX + ".ignoreTypes.s"; 234 235 /** 236 * Configuration property: Use bean definitions. 237 * 238 * <h5 class='section'>Property:</h5> 239 * <ul> 240 * <li><b>Name:</b> <js>"JsonSchemaGenerator.useBeanDefs.b"</js> 241 * <li><b>Data type:</b> <c>Boolean</c> 242 * <li><b>Default:</b> <jk>false</jk> 243 * <li><b>Methods:</b> 244 * <ul> 245 * <li class='jm'>{@link JsonSchemaGeneratorBuilder#useBeanDefs()} 246 * </ul> 247 * </ul> 248 * 249 * <h5 class='section'>Description:</h5> 250 * <p> 251 * When enabled, schemas on beans will be serialized as the following: 252 * <p class='bcode w800'> 253 * { 254 * type: <js>'object'</js>, 255 * <js>'$ref'</js>: <js>'#/definitions/TypeId'</js> 256 * } 257 * </p> 258 * 259 * <p> 260 * The definitions can then be retrieved from the session using {@link JsonSchemaGeneratorSession#getBeanDefs()}. 261 * <p> 262 * Definitions can also be added programmatically using {@link JsonSchemaGeneratorSession#addBeanDef(String, ObjectMap)}. 263 */ 264 public static final String JSONSCHEMA_useBeanDefs = PREFIX + ".useBeanDefs.b"; 265 266 267 //------------------------------------------------------------------------------------------------------------------- 268 // Predefined instances 269 //------------------------------------------------------------------------------------------------------------------- 270 271 /** Default serializer, all default settings.*/ 272 public static final JsonSchemaGenerator DEFAULT = new JsonSchemaGenerator(PropertyStore.DEFAULT); 273 274 275 //------------------------------------------------------------------------------------------------------------------- 276 // Instance 277 //------------------------------------------------------------------------------------------------------------------- 278 279 private final boolean useBeanDefs, allowNestedExamples, allowNestedDescriptions; 280 private final BeanDefMapper beanDefMapper; 281 private final Set<TypeCategory> addExamplesTo, addDescriptionsTo; 282 private final Map<String,ObjectMap> defaultSchemas; 283 private final JsonSerializer jsonSerializer; 284 private final Set<Pattern> ignoreTypes; 285 286 /** 287 * Constructor. 288 * 289 * @param ps Initialize with the specified config property store. 290 */ 291 public JsonSchemaGenerator(PropertyStore ps) { 292 super(ps.builder().set(BEANTRAVERSE_detectRecursions, true).set(BEANTRAVERSE_ignoreRecursions, true).build()); 293 294 useBeanDefs = getBooleanProperty(JSONSCHEMA_useBeanDefs, false); 295 allowNestedExamples = getBooleanProperty(JSONSCHEMA_allowNestedExamples, false); 296 allowNestedDescriptions = getBooleanProperty(JSONSCHEMA_allowNestedDescriptions, false); 297 beanDefMapper = getInstanceProperty(JSONSCHEMA_beanDefMapper, BeanDefMapper.class, BasicBeanDefMapper.class); 298 addExamplesTo = TypeCategory.parse(getStringProperty(JSONSCHEMA_addExamplesTo, null)); 299 addDescriptionsTo = TypeCategory.parse(getStringProperty(JSONSCHEMA_addDescriptionsTo, null)); 300 defaultSchemas = getMapProperty(JSONSCHEMA_defaultSchemas, ObjectMap.class); 301 302 Set<Pattern> ignoreTypes = new LinkedHashSet<>(); 303 for (String s : split(ps.getProperty(JSONSCHEMA_ignoreTypes, String.class, ""))) 304 ignoreTypes.add(Pattern.compile(s.replace(".", "\\.").replace("*", ".*"))); 305 this.ignoreTypes = ignoreTypes; 306 307 jsonSerializer = new JsonSerializer(ps); 308 } 309 310 @Override /* Context */ 311 public JsonSchemaGeneratorBuilder builder() { 312 return new JsonSchemaGeneratorBuilder(getPropertyStore()); 313 } 314 315 /** 316 * Instantiates a new clean-slate {@link JsonSerializerBuilder} object. 317 * 318 * <p> 319 * This is equivalent to simply calling <code><jk>new</jk> JsonSerializerBuilder()</code>. 320 * 321 * @return A new {@link JsonSerializerBuilder} object. 322 */ 323 public static JsonSchemaGeneratorBuilder create() { 324 return new JsonSchemaGeneratorBuilder(); 325 } 326 327 @Override /* Context */ 328 public JsonSchemaGeneratorSession createSession() { 329 return createSession(createDefaultSessionArgs()); 330 } 331 332 @Override 333 public JsonSchemaGeneratorSession createSession(BeanSessionArgs args) { 334 return new JsonSchemaGeneratorSession(this, args); 335 } 336 337 JsonSerializer getJsonSerializer() { 338 return jsonSerializer; 339 } 340 341 //----------------------------------------------------------------------------------------------------------------- 342 // Properties 343 //----------------------------------------------------------------------------------------------------------------- 344 345 /** 346 * Configuration property: Add descriptions to types. 347 * 348 * @see #JSONSCHEMA_addDescriptionsTo 349 * @return 350 * Set of categories of types that descriptions should be automatically added to generated schemas. 351 */ 352 protected final Set<TypeCategory> getAddDescriptionsTo() { 353 return addDescriptionsTo; 354 } 355 356 /** 357 * Configuration property: Add examples. 358 * 359 * @see #JSONSCHEMA_addExamplesTo 360 * @return 361 * Set of categories of types that examples should be automatically added to generated schemas. 362 */ 363 protected final Set<TypeCategory> getAddExamplesTo() { 364 return addExamplesTo; 365 } 366 367 /** 368 * Configuration property: Allow nested descriptions. 369 * 370 * @see #JSONSCHEMA_allowNestedDescriptions 371 * @return 372 * <jk>true</jk> if nested descriptions are allowed in schema definitions. 373 */ 374 protected final boolean isAllowNestedDescriptions() { 375 return allowNestedDescriptions; 376 } 377 378 /** 379 * Configuration property: Allow nested examples. 380 * 381 * @see #JSONSCHEMA_allowNestedExamples 382 * @return 383 * <jk>true</jk> if nested examples are allowed in schema definitions. 384 */ 385 protected final boolean isAllowNestedExamples() { 386 return allowNestedExamples; 387 } 388 389 /** 390 * Configuration property: Bean schema definition mapper. 391 * 392 * @see #JSONSCHEMA_beanDefMapper 393 * @return 394 * Interface to use for converting Bean classes to definition IDs and URIs. 395 */ 396 protected final BeanDefMapper getBeanDefMapper() { 397 return beanDefMapper; 398 } 399 400 /** 401 * Configuration property: Default schemas. 402 * 403 * @see #JSONSCHEMA_defaultSchemas 404 * @return 405 * Custom schema information for particular class types. 406 */ 407 protected final Map<String,ObjectMap> getDefaultSchemas() { 408 return defaultSchemas; 409 } 410 411 /** 412 * Configuration property: Ignore types from schema definitions. 413 * 414 * @see JsonSchemaGenerator#JSONSCHEMA_ignoreTypes 415 * @return 416 * Custom schema information for particular class types. 417 */ 418 public Set<Pattern> getIgnoreTypes() { 419 return ignoreTypes; 420 } 421 422 /** 423 * Configuration property: Use bean definitions. 424 * 425 * @see #JSONSCHEMA_useBeanDefs 426 * @return 427 * <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags. 428 */ 429 protected final boolean isUseBeanDefs() { 430 return useBeanDefs; 431 } 432 433 //----------------------------------------------------------------------------------------------------------------- 434 // Other methods 435 //----------------------------------------------------------------------------------------------------------------- 436 437 /** 438 * Returns <jk>true</jk> if the specified type is ignored. 439 * 440 * <p> 441 * The type is ignored if it's specified in the {@link #JSONSCHEMA_ignoreTypes} setting. 442 * <br>Ignored types return <jk>null</jk> on the call to {@link JsonSchemaGeneratorSession#getSchema(ClassMeta)}. 443 * 444 * @param cm The type to check. 445 * @return <jk>true</jk> if the specified type is ignored. 446 */ 447 public boolean isIgnoredType(ClassMeta<?> cm) { 448 for (Pattern p : ignoreTypes) 449 if (p.matcher(cm.getSimpleName()).matches() || p.matcher(cm.getName()).matches()) 450 return true; 451 return false; 452 } 453 454 //----------------------------------------------------------------------------------------------------------------- 455 // Other methods 456 //----------------------------------------------------------------------------------------------------------------- 457 458 @Override /* Context */ 459 public ObjectMap toMap() { 460 return super.toMap() 461 .append("JsonSchemaGenerator", new DefaultFilteringObjectMap() 462 .append("useBeanDefs", useBeanDefs) 463 .append("allowNestedExamples", allowNestedExamples) 464 .append("allowNestedDescriptions", allowNestedDescriptions) 465 .append("beanDefMapper", beanDefMapper) 466 .append("addExamplesTo", addExamplesTo) 467 .append("addDescriptionsTo", addDescriptionsTo) 468 .append("defaultSchemas", defaultSchemas) 469 .append("ignoreTypes", ignoreTypes) 470 ); 471 } 472}