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