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