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 if (pojoSwap != null && pojoSwap.getClass().getAnnotation(Schema.class) != null) 143 jscm = getClassMeta(pojoSwap.getClass()).getExtendedMeta(JsonSchemaClassMeta.class); 144 if (jscm == null) 145 jscm = sType.getExtendedMeta(JsonSchemaClassMeta.class); 146 147 TypeCategory tc = null; 148 149 if (sType.isNumber()) { 150 tc = NUMBER; 151 if (sType.isDecimal()) { 152 type = "number"; 153 if (sType.isFloat()) { 154 format = "float"; 155 } else if (sType.isDouble()) { 156 format = "double"; 157 } 158 } else { 159 type = "integer"; 160 if (sType.isShort()) { 161 format = "int16"; 162 } else if (sType.isInteger()) { 163 format = "int32"; 164 } else if (sType.isLong()) { 165 format = "int64"; 166 } 167 } 168 } else if (sType.isBoolean()) { 169 tc = BOOLEAN; 170 type = "boolean"; 171 } else if (sType.isMap()) { 172 tc = MAP; 173 type = "object"; 174 } else if (sType.isBean()) { 175 tc = BEAN; 176 type = "object"; 177 } else if (sType.isCollection()) { 178 tc = COLLECTION; 179 type = "array"; 180 } else if (sType.isArray()) { 181 tc = ARRAY; 182 type = "array"; 183 } else if (sType.isEnum()) { 184 tc = ENUM; 185 type = "string"; 186 } else if (sType.isCharSequence() || sType.isChar()) { 187 tc = STRING; 188 type = "string"; 189 } else if (sType.isUri()) { 190 tc = STRING; 191 type = "string"; 192 format = "uri"; 193 } else { 194 tc = STRING; 195 type = "string"; 196 } 197 198 // Add info from @Schema on bean property. 199 if (jsbpm != null) { 200 out.appendAll(jsbpm.getSchema()); 201 } 202 203 out.appendAll(jscm.getSchema()); 204 205 out.appendIf(false, true, true, "type", type); 206 out.appendIf(false, true, true, "format", format); 207 208 if (aType != null) { 209 210 example = getExample(sType, tc, exampleAdded); 211 description = getDescription(sType, tc, descriptionAdded); 212 exampleAdded |= example != null; 213 descriptionAdded |= description != null; 214 215 if (tc == BEAN) { 216 ObjectMap properties = new ObjectMap(); 217 BeanMeta bm = getBeanMeta(sType.getInnerClass()); 218 if (pNames != null) 219 bm = new BeanMetaFiltered(bm, pNames); 220 for (Iterator<BeanPropertyMeta> i = bm.getPropertyMetas().iterator(); i.hasNext();) { 221 BeanPropertyMeta p = i.next(); 222 if (p.canRead()) 223 properties.put(p.getName(), getSchema(p.getClassMeta(), p.getName(), p.getProperties(), exampleAdded, descriptionAdded, p.getExtendedMeta(JsonSchemaBeanPropertyMeta.class))); 224 } 225 out.put("properties", properties); 226 227 } else if (tc == COLLECTION) { 228 ClassMeta et = sType.getElementType(); 229 if (sType.isCollection() && sType.getInfo().isChildOf(Set.class)) 230 out.put("uniqueItems", true); 231 out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null)); 232 233 } else if (tc == ARRAY) { 234 ClassMeta et = sType.getElementType(); 235 if (sType.isCollection() && sType.getInfo().isChildOf(Set.class)) 236 out.put("uniqueItems", true); 237 out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null)); 238 239 } else if (tc == ENUM) { 240 out.put("enum", getEnums(sType)); 241 242 } else if (tc == MAP) { 243 ObjectMap om = getSchema(sType.getValueType(), "additionalProperties", null, exampleAdded, descriptionAdded, null); 244 if (! om.isEmpty()) 245 out.put("additionalProperties", om); 246 247 } 248 } 249 250 out.appendAll(jscm.getSchema()); 251 252 out.appendIf(false, true, true, "description", description); 253 out.appendIf(false, true, true, "x-example", example); 254 255 if (ds != null) 256 out.appendAll(ds); 257 258 if (useDef) { 259 defs.put(getBeanDefId(sType), out); 260 out = new ObjectMap().append("$ref", getBeanDefUri(sType)); 261 } 262 263 pop(); 264 265 return out; 266 } 267 268 private List<String> getEnums(ClassMeta<?> cm) { 269 List<String> l = new ArrayList<>(); 270 for (Enum<?> e : getEnumConstants(cm.getInnerClass())) 271 l.add(cm.toString(e)); 272 return l; 273 } 274 275 private Object getExample(ClassMeta<?> sType, TypeCategory t, boolean exampleAdded) throws SerializeException { 276 boolean canAdd = isAllowNestedExamples() || ! exampleAdded; 277 if (canAdd && (getAddExamplesTo().contains(t) || getAddExamplesTo().contains(ANY))) { 278 Object example = sType.getExample(this); 279 if (example != null) { 280 try { 281 return JsonParser.DEFAULT.parse(toJson(example), Object.class); 282 } catch (ParseException e) { 283 throw new SerializeException(e); 284 } 285 } 286 } 287 return null; 288 } 289 290 private String toJson(Object o) throws SerializeException { 291 if (jsSession == null) 292 jsSession = ctx.getJsonSerializer().createSession(null); 293 return jsSession.serializeToString(o); 294 } 295 296 private Object getDescription(ClassMeta<?> sType, TypeCategory t, boolean descriptionAdded) { 297 boolean canAdd = isAllowNestedDescriptions() || ! descriptionAdded; 298 if (canAdd && (getAddDescriptionsTo().contains(t) || getAddDescriptionsTo().contains(ANY))) 299 return sType.toString(); 300 return null; 301 } 302 303 /** 304 * Returns the definition ID for the specified class. 305 * 306 * @param cm The class to get the definition ID of. 307 * @return The definition ID for the specified class. 308 */ 309 public String getBeanDefId(ClassMeta<?> cm) { 310 return getBeanDefMapper().getId(cm); 311 } 312 313 /** 314 * Returns the definition URI for the specified class. 315 * 316 * @param cm The class to get the definition URI of. 317 * @return The definition URI for the specified class. 318 */ 319 public java.net.URI getBeanDefUri(ClassMeta<?> cm) { 320 return getBeanDefMapper().getURI(cm); 321 } 322 323 /** 324 * Returns the definition URI for the specified class. 325 * 326 * @param id The definition ID to get the definition URI of. 327 * @return The definition URI for the specified class. 328 */ 329 public java.net.URI getBeanDefUri(String id) { 330 return getBeanDefMapper().getURI(id); 331 } 332 333 /** 334 * Returns the definitions that were gathered during this session. 335 * 336 * <p> 337 * This map is modifiable and affects the map in the session. 338 * 339 * @return 340 * The definitions that were gathered during this session, or <jk>null</jk> if {@link JsonSchemaGenerator#JSONSCHEMA_useBeanDefs} was not enabled. 341 */ 342 public Map<String,ObjectMap> getBeanDefs() { 343 return defs; 344 } 345 346 /** 347 * Adds a schema definition to this session. 348 * 349 * @param id The definition ID. 350 * @param def The definition schema. 351 * @return This object (for method chaining). 352 */ 353 public JsonSchemaGeneratorSession addBeanDef(String id, ObjectMap def) { 354 if (defs != null) 355 defs.put(id, def); 356 return this; 357 } 358 359 //----------------------------------------------------------------------------------------------------------------- 360 // Properties 361 //----------------------------------------------------------------------------------------------------------------- 362 363 /** 364 * Configuration property: Add descriptions to types. 365 * 366 * @see JsonSchemaGenerator#JSONSCHEMA_addDescriptionsTo 367 * @return 368 * Set of categories of types that descriptions should be automatically added to generated schemas. 369 */ 370 protected final Set<TypeCategory> getAddDescriptionsTo() { 371 return ctx.getAddDescriptionsTo(); 372 } 373 374 /** 375 * Configuration property: Add examples. 376 * 377 * @see JsonSchemaGenerator#JSONSCHEMA_addExamplesTo 378 * @return 379 * Set of categories of types that examples should be automatically added to generated schemas. 380 */ 381 protected final Set<TypeCategory> getAddExamplesTo() { 382 return ctx.getAddExamplesTo(); 383 } 384 385 /** 386 * Configuration property: Allow nested descriptions. 387 * 388 * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedDescriptions 389 * @return 390 * <jk>true</jk> if nested descriptions are allowed in schema definitions. 391 */ 392 protected final boolean isAllowNestedDescriptions() { 393 return ctx.isAllowNestedDescriptions(); 394 } 395 396 /** 397 * Configuration property: Allow nested examples. 398 * 399 * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedExamples 400 * @return 401 * <jk>true</jk> if nested examples are allowed in schema definitions. 402 */ 403 protected final boolean isAllowNestedExamples() { 404 return ctx.isAllowNestedExamples(); 405 } 406 407 /** 408 * Configuration property: Bean schema definition mapper. 409 * 410 * @see JsonSchemaGenerator#JSONSCHEMA_beanDefMapper 411 * @return 412 * Interface to use for converting Bean classes to definition IDs and URIs. 413 */ 414 protected final BeanDefMapper getBeanDefMapper() { 415 return ctx.getBeanDefMapper(); 416 } 417 418 /** 419 * Configuration property: Default schemas. 420 * 421 * @see JsonSchemaGenerator#JSONSCHEMA_defaultSchemas 422 * @return 423 * Custom schema information for particular class types. 424 */ 425 protected final Map<String,ObjectMap> getDefaultSchemas() { 426 return ctx.getDefaultSchemas(); 427 } 428 429 /** 430 * Configuration property: Ignore types from schema definitions. 431 * 432 * @see JsonSchemaGenerator#JSONSCHEMA_ignoreTypes 433 * @return 434 * Custom schema information for particular class types. 435 */ 436 protected final Set<Pattern> getIgnoreTypes() { 437 return ctx.getIgnoreTypes(); 438 } 439 440 /** 441 * Configuration property: Use bean definitions. 442 * 443 * @see JsonSchemaGenerator#JSONSCHEMA_useBeanDefs 444 * @return 445 * <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags. 446 */ 447 protected final boolean isUseBeanDefs() { 448 return ctx.isUseBeanDefs(); 449 } 450 451 //----------------------------------------------------------------------------------------------------------------- 452 // Utility methods 453 //----------------------------------------------------------------------------------------------------------------- 454 455 private ClassMeta<?> toClassMeta(Object o) { 456 if (o instanceof Type) 457 return getClassMeta((Type)o); 458 return getClassMetaForObject(o); 459 } 460 461 //----------------------------------------------------------------------------------------------------------------- 462 // Other methods 463 //----------------------------------------------------------------------------------------------------------------- 464 465 @Override /* Session */ 466 public ObjectMap toMap() { 467 return super.toMap() 468 .append("JsonSchemaGeneratorSession", new DefaultFilteringObjectMap() 469 ); 470 } 471}