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