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.ObjectUtils.*; 016import static org.apache.juneau.jsonschema.TypeCategory.*; 017 018import java.lang.reflect.*; 019import java.util.*; 020import java.util.regex.*; 021 022import org.apache.juneau.*; 023import org.apache.juneau.collections.*; 024import org.apache.juneau.json.*; 025import org.apache.juneau.jsonschema.annotation.*; 026import org.apache.juneau.parser.ParseException; 027import org.apache.juneau.serializer.*; 028import org.apache.juneau.transform.*; 029 030/** 031 * Session object that lives for the duration of a single use of {@link JsonSchemaSerializer}. 032 * 033 * <p> 034 * This class is NOT thread safe. 035 * It is typically discarded after one-time use although it can be reused within the same thread. 036 */ 037public class JsonSchemaGeneratorSession extends BeanTraverseSession { 038 039 private final JsonSchemaGenerator ctx; 040 private final Map<String,OMap> defs; 041 private JsonSerializerSession jsSession; 042 043 /** 044 * Create a new session using properties specified in the context. 045 * 046 * @param ctx 047 * The context creating this session object. 048 * The context contains all the configuration settings for this object. 049 * @param args 050 * Runtime arguments. 051 * These specify session-level information such as locale and URI context. 052 * It also include session-level properties that override the properties defined on the bean and 053 * serializer contexts. 054 */ 055 protected JsonSchemaGeneratorSession(JsonSchemaGenerator ctx, BeanSessionArgs args) { 056 super(ctx, args); 057 this.ctx = ctx; 058 if (isUseBeanDefs()) 059 defs = new TreeMap<>(); 060 else 061 defs = null; 062 } 063 064 /** 065 * Returns the JSON-schema for the specified object. 066 * 067 * @param o 068 * The object. 069 * <br>Can either be a POJO or a <c>Class</c>/<c>Type</c>. 070 * @return The schema for the type. 071 * @throws BeanRecursionException Bean recursion occurred. 072 * @throws SerializeException Error occurred. 073 */ 074 public OMap getSchema(Object o) throws BeanRecursionException, SerializeException { 075 return getSchema(toClassMeta(o), "root", null, false, false, null); 076 } 077 078 /** 079 * Returns the JSON-schema for the specified type. 080 * 081 * @param type The object type. 082 * @return The schema for the type. 083 * @throws BeanRecursionException Bean recursion occurred. 084 * @throws SerializeException Error occurred. 085 */ 086 public OMap getSchema(Type type) throws BeanRecursionException, SerializeException { 087 return getSchema(getClassMeta(type), "root", null, false, false, null); 088 } 089 090 /** 091 * Returns the JSON-schema for the specified type. 092 * 093 * @param cm The object type. 094 * @return The schema for the type. 095 * @throws BeanRecursionException Bean recursion occurred. 096 * @throws SerializeException Error occurred. 097 */ 098 public OMap getSchema(ClassMeta<?> cm) throws BeanRecursionException, SerializeException { 099 return getSchema(cm, "root", null, false, false, null); 100 } 101 102 @SuppressWarnings({ "unchecked", "rawtypes" }) 103 private OMap getSchema(ClassMeta<?> eType, String attrName, String[] pNames, boolean exampleAdded, boolean descriptionAdded, JsonSchemaBeanPropertyMeta jsbpm) throws BeanRecursionException, SerializeException { 104 105 if (ctx.isIgnoredType(eType)) 106 return null; 107 108 OMap out = new OMap(); 109 110 if (eType == null) 111 eType = object(); 112 113 ClassMeta<?> aType; // The actual type (will be null if recursion occurs) 114 ClassMeta<?> sType; // The serialized type 115 PojoSwap pojoSwap = eType.getSwap(this); 116 117 aType = push(attrName, eType, null); 118 119 sType = eType.getSerializedClassMeta(this); 120 121 String type = null, format = null; 122 Object example = null, description = null; 123 124 boolean useDef = isUseBeanDefs() && sType.isBean() && pNames == null; 125 126 if (useDef) { 127 exampleAdded = false; 128 descriptionAdded = false; 129 } 130 131 if (useDef && defs.containsKey(getBeanDefId(sType))) { 132 pop(); 133 return new OMap().a("$ref", getBeanDefUri(sType)); 134 } 135 136 OMap ds = getDefaultSchemas().get(sType.getInnerClass().getName()); 137 if (ds != null && ds.containsKey("type")) { 138 pop(); 139 return out.aa(ds); 140 } 141 142 JsonSchemaClassMeta jscm = null; 143 ClassMeta pojoSwapCM = pojoSwap == null ? null : getClassMeta(pojoSwap.getClass()); 144 if (pojoSwapCM != null && pojoSwapCM.hasAnnotation(Schema.class)) 145 jscm = getJsonSchemaClassMeta(pojoSwapCM); 146 if (jscm == null) 147 jscm = getJsonSchemaClassMeta(sType); 148 149 TypeCategory tc = null; 150 151 if (sType.isNumber()) { 152 tc = NUMBER; 153 if (sType.isDecimal()) { 154 type = "number"; 155 if (sType.isFloat()) { 156 format = "float"; 157 } else if (sType.isDouble()) { 158 format = "double"; 159 } 160 } else { 161 type = "integer"; 162 if (sType.isShort()) { 163 format = "int16"; 164 } else if (sType.isInteger()) { 165 format = "int32"; 166 } else if (sType.isLong()) { 167 format = "int64"; 168 } 169 } 170 } else if (sType.isBoolean()) { 171 tc = BOOLEAN; 172 type = "boolean"; 173 } else if (sType.isMap()) { 174 tc = MAP; 175 type = "object"; 176 } else if (sType.isBean()) { 177 tc = BEAN; 178 type = "object"; 179 } else if (sType.isCollection()) { 180 tc = COLLECTION; 181 type = "array"; 182 } else if (sType.isArray()) { 183 tc = ARRAY; 184 type = "array"; 185 } else if (sType.isEnum()) { 186 tc = ENUM; 187 type = "string"; 188 } else if (sType.isCharSequence() || sType.isChar()) { 189 tc = STRING; 190 type = "string"; 191 } else if (sType.isUri()) { 192 tc = STRING; 193 type = "string"; 194 format = "uri"; 195 } else { 196 tc = STRING; 197 type = "string"; 198 } 199 200 // Add info from @Schema on bean property. 201 if (jsbpm != null) { 202 out.appendAll(jsbpm.getSchema()); 203 } 204 205 out.appendAll(jscm.getSchema()); 206 207 out.appendIf(false, true, true, "type", type); 208 out.appendIf(false, true, true, "format", format); 209 210 if (aType != null) { 211 212 example = getExample(sType, tc, exampleAdded); 213 description = getDescription(sType, tc, descriptionAdded); 214 exampleAdded |= example != null; 215 descriptionAdded |= description != null; 216 217 if (tc == BEAN) { 218 OMap properties = new OMap(); 219 BeanMeta bm = getBeanMeta(sType.getInnerClass()); 220 if (pNames != null) 221 bm = new BeanMetaFiltered(bm, pNames); 222 for (Iterator<BeanPropertyMeta> i = bm.getPropertyMetas().iterator(); i.hasNext();) { 223 BeanPropertyMeta p = i.next(); 224 if (p.canRead()) 225 properties.put(p.getName(), getSchema(p.getClassMeta(), p.getName(), p.getProperties(), exampleAdded, descriptionAdded, getJsonSchemaBeanPropertyMeta(p))); 226 } 227 out.put("properties", properties); 228 229 } else if (tc == COLLECTION) { 230 ClassMeta et = sType.getElementType(); 231 if (sType.isCollection() && sType.getInfo().isChildOf(Set.class)) 232 out.put("uniqueItems", true); 233 out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null)); 234 235 } else if (tc == ARRAY) { 236 ClassMeta et = sType.getElementType(); 237 if (sType.isCollection() && sType.getInfo().isChildOf(Set.class)) 238 out.put("uniqueItems", true); 239 out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null)); 240 241 } else if (tc == ENUM) { 242 out.put("enum", getEnums(sType)); 243 244 } else if (tc == MAP) { 245 OMap om = getSchema(sType.getValueType(), "additionalProperties", null, exampleAdded, descriptionAdded, null); 246 if (! om.isEmpty()) 247 out.put("additionalProperties", om); 248 249 } 250 } 251 252 out.appendAll(jscm.getSchema()); 253 254 out.appendIf(false, true, true, "description", description); 255 out.appendIf(false, true, true, "x-example", example); 256 257 if (ds != null) 258 out.appendAll(ds); 259 260 if (useDef) { 261 defs.put(getBeanDefId(sType), out); 262 out = OMap.of("$ref", getBeanDefUri(sType)); 263 } 264 265 pop(); 266 267 return out; 268 } 269 270 private List<String> getEnums(ClassMeta<?> cm) { 271 List<String> l = new ArrayList<>(); 272 for (Enum<?> e : getEnumConstants(cm.getInnerClass())) 273 l.add(cm.toString(e)); 274 return l; 275 } 276 277 private Object getExample(ClassMeta<?> sType, TypeCategory t, boolean exampleAdded) throws SerializeException { 278 boolean canAdd = isAllowNestedExamples() || ! exampleAdded; 279 if (canAdd && (getAddExamplesTo().contains(t) || getAddExamplesTo().contains(ANY))) { 280 Object example = sType.getExample(this); 281 if (example != null) { 282 try { 283 return JsonParser.DEFAULT.parse(toJson(example), Object.class); 284 } catch (ParseException e) { 285 throw new SerializeException(e); 286 } 287 } 288 } 289 return null; 290 } 291 292 private String toJson(Object o) throws SerializeException { 293 if (jsSession == null) 294 jsSession = ctx.getJsonSerializer().createSession(null); 295 return jsSession.serializeToString(o); 296 } 297 298 private Object getDescription(ClassMeta<?> sType, TypeCategory t, boolean descriptionAdded) { 299 boolean canAdd = isAllowNestedDescriptions() || ! descriptionAdded; 300 if (canAdd && (getAddDescriptionsTo().contains(t) || getAddDescriptionsTo().contains(ANY))) 301 return sType.toString(); 302 return null; 303 } 304 305 /** 306 * Returns the definition ID for the specified class. 307 * 308 * @param cm The class to get the definition ID of. 309 * @return The definition ID for the specified class. 310 */ 311 public String getBeanDefId(ClassMeta<?> cm) { 312 return getBeanDefMapper().getId(cm); 313 } 314 315 /** 316 * Returns the definition URI for the specified class. 317 * 318 * @param cm The class to get the definition URI of. 319 * @return The definition URI for the specified class. 320 */ 321 public java.net.URI getBeanDefUri(ClassMeta<?> cm) { 322 return getBeanDefMapper().getURI(cm); 323 } 324 325 /** 326 * Returns the definition URI for the specified class. 327 * 328 * @param id The definition ID to get the definition URI of. 329 * @return The definition URI for the specified class. 330 */ 331 public java.net.URI getBeanDefUri(String id) { 332 return getBeanDefMapper().getURI(id); 333 } 334 335 /** 336 * Returns the definitions that were gathered during this session. 337 * 338 * <p> 339 * This map is modifiable and affects the map in the session. 340 * 341 * @return 342 * The definitions that were gathered during this session, or <jk>null</jk> if {@link JsonSchemaGenerator#JSONSCHEMA_useBeanDefs} was not enabled. 343 */ 344 public Map<String,OMap> getBeanDefs() { 345 return defs; 346 } 347 348 /** 349 * Adds a schema definition to this session. 350 * 351 * @param id The definition ID. 352 * @param def The definition schema. 353 * @return This object (for method chaining). 354 */ 355 public JsonSchemaGeneratorSession addBeanDef(String id, OMap def) { 356 if (defs != null) 357 defs.put(id, def); 358 return this; 359 } 360 361 //----------------------------------------------------------------------------------------------------------------- 362 // Properties 363 //----------------------------------------------------------------------------------------------------------------- 364 365 /** 366 * Configuration property: Add descriptions to types. 367 * 368 * @see JsonSchemaGenerator#JSONSCHEMA_addDescriptionsTo 369 * @return 370 * Set of categories of types that descriptions should be automatically added to generated schemas. 371 */ 372 protected final Set<TypeCategory> getAddDescriptionsTo() { 373 return ctx.getAddDescriptionsTo(); 374 } 375 376 /** 377 * Configuration property: Add examples. 378 * 379 * @see JsonSchemaGenerator#JSONSCHEMA_addExamplesTo 380 * @return 381 * Set of categories of types that examples should be automatically added to generated schemas. 382 */ 383 protected final Set<TypeCategory> getAddExamplesTo() { 384 return ctx.getAddExamplesTo(); 385 } 386 387 /** 388 * Configuration property: Allow nested descriptions. 389 * 390 * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedDescriptions 391 * @return 392 * <jk>true</jk> if nested descriptions are allowed in schema definitions. 393 */ 394 protected final boolean isAllowNestedDescriptions() { 395 return ctx.isAllowNestedDescriptions(); 396 } 397 398 /** 399 * Configuration property: Allow nested examples. 400 * 401 * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedExamples 402 * @return 403 * <jk>true</jk> if nested examples are allowed in schema definitions. 404 */ 405 protected final boolean isAllowNestedExamples() { 406 return ctx.isAllowNestedExamples(); 407 } 408 409 /** 410 * Configuration property: Bean schema definition mapper. 411 * 412 * @see JsonSchemaGenerator#JSONSCHEMA_beanDefMapper 413 * @return 414 * Interface to use for converting Bean classes to definition IDs and URIs. 415 */ 416 protected final BeanDefMapper getBeanDefMapper() { 417 return ctx.getBeanDefMapper(); 418 } 419 420 /** 421 * Configuration property: Default schemas. 422 * 423 * @see JsonSchemaGenerator#JSONSCHEMA_defaultSchemas 424 * @return 425 * Custom schema information for particular class types. 426 */ 427 protected final Map<String,OMap> getDefaultSchemas() { 428 return ctx.getDefaultSchemas(); 429 } 430 431 /** 432 * Configuration property: Ignore types from schema definitions. 433 * 434 * @see JsonSchemaGenerator#JSONSCHEMA_ignoreTypes 435 * @return 436 * Custom schema information for particular class types. 437 */ 438 protected final Set<Pattern> getIgnoreTypes() { 439 return ctx.getIgnoreTypes(); 440 } 441 442 /** 443 * Configuration property: Use bean definitions. 444 * 445 * @see JsonSchemaGenerator#JSONSCHEMA_useBeanDefs 446 * @return 447 * <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags. 448 */ 449 protected final boolean isUseBeanDefs() { 450 return ctx.isUseBeanDefs(); 451 } 452 453 //----------------------------------------------------------------------------------------------------------------- 454 // Extended metadata 455 //----------------------------------------------------------------------------------------------------------------- 456 457 /** 458 * Returns the language-specific metadata on the specified class. 459 * 460 * @param cm The class to return the metadata on. 461 * @return The metadata. 462 */ 463 public JsonSchemaClassMeta getJsonSchemaClassMeta(ClassMeta<?> cm) { 464 return ctx.getJsonSchemaClassMeta(cm); 465 } 466 467 /** 468 * Returns the language-specific metadata on the specified bean property. 469 * 470 * @param bpm The bean property to return the metadata on. 471 * @return The metadata. 472 */ 473 public JsonSchemaBeanPropertyMeta getJsonSchemaBeanPropertyMeta(BeanPropertyMeta bpm) { 474 return ctx.getJsonSchemaBeanPropertyMeta(bpm); 475 } 476 477 //----------------------------------------------------------------------------------------------------------------- 478 // Utility methods 479 //----------------------------------------------------------------------------------------------------------------- 480 481 private ClassMeta<?> toClassMeta(Object o) { 482 if (o instanceof Type) 483 return getClassMeta((Type)o); 484 return getClassMetaForObject(o); 485 } 486 487 //----------------------------------------------------------------------------------------------------------------- 488 // Other methods 489 //----------------------------------------------------------------------------------------------------------------- 490 491 @Override /* Session */ 492 public OMap toMap() { 493 return super.toMap() 494 .a("JsonSchemaGeneratorSession", new DefaultFilteringOMap() 495 ); 496 } 497}