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 if (isUseBeanDefs()) 057 defs = new TreeMap<>(); 058 else 059 defs = null; 060 } 061 062 /** 063 * Returns the JSON-schema for the specified object. 064 * 065 * @param o 066 * The object. 067 * <br>Can either be a POJO or a <code>Class</code>/<code>Type</code>. 068 * @return The schema for the type. 069 * @throws Exception 070 */ 071 public ObjectMap getSchema(Object o) throws Exception { 072 return getSchema(toClassMeta(o), "root", null, false, false, null); 073 } 074 075 /** 076 * Returns the JSON-schema for the specified type. 077 * 078 * @param type The object type. 079 * @return The schema for the type. 080 * @throws Exception 081 */ 082 public ObjectMap getSchema(Type type) throws Exception { 083 return getSchema(getClassMeta(type), "root", null, false, false, null); 084 } 085 086 /** 087 * Returns the JSON-schema for the specified type. 088 * 089 * @param cm The object type. 090 * @return The schema for the type. 091 * @throws Exception 092 */ 093 public ObjectMap getSchema(ClassMeta<?> cm) throws Exception { 094 return getSchema(cm, "root", null, false, false, null); 095 } 096 097 @SuppressWarnings({ "unchecked", "rawtypes" }) 098 private ObjectMap getSchema(ClassMeta<?> eType, String attrName, String[] pNames, boolean exampleAdded, boolean descriptionAdded, JsonSchemaBeanPropertyMeta jsbpm) throws Exception { 099 100 if (ctx.isIgnoredType(eType)) 101 return null; 102 103 ObjectMap out = new ObjectMap(); 104 105 if (eType == null) 106 eType = object(); 107 108 ClassMeta<?> aType; // The actual type (will be null if recursion occurs) 109 ClassMeta<?> sType; // The serialized type 110 PojoSwap pojoSwap = eType.getPojoSwap(this); 111 112 aType = push(attrName, eType, null); 113 114 sType = eType.getSerializedClassMeta(this); 115 116 String type = null, format = null; 117 Object example = null, description = null; 118 119 boolean useDef = isUseBeanDefs() && sType.isBean() && pNames == null; 120 121 if (useDef) { 122 exampleAdded = false; 123 descriptionAdded = false; 124 } 125 126 if (useDef && defs.containsKey(getBeanDefId(sType))) { 127 pop(); 128 return new ObjectMap().append("$ref", getBeanDefUri(sType)); 129 } 130 131 ObjectMap ds = getDefaultSchemas().get(sType.getInnerClass().getName()); 132 if (ds != null && ds.containsKey("type")) { 133 pop(); 134 return out.appendAll(ds); 135 } 136 137 JsonSchemaClassMeta jscm = null; 138 if (pojoSwap != null && pojoSwap.getClass().getAnnotation(Schema.class) != null) 139 jscm = getClassMeta(pojoSwap.getClass()).getExtendedMeta(JsonSchemaClassMeta.class); 140 if (jscm == null) 141 jscm = sType.getExtendedMeta(JsonSchemaClassMeta.class); 142 143 TypeCategory tc = null; 144 145 if (sType.isNumber()) { 146 tc = NUMBER; 147 if (sType.isDecimal()) { 148 type = "number"; 149 if (sType.isFloat()) { 150 format = "float"; 151 } else if (sType.isDouble()) { 152 format = "double"; 153 } 154 } else { 155 type = "integer"; 156 if (sType.isShort()) { 157 format = "int16"; 158 } else if (sType.isInteger()) { 159 format = "int32"; 160 } else if (sType.isLong()) { 161 format = "int64"; 162 } 163 } 164 } else if (sType.isBoolean()) { 165 tc = BOOLEAN; 166 type = "boolean"; 167 } else if (sType.isMap()) { 168 tc = MAP; 169 type = "object"; 170 } else if (sType.isBean()) { 171 tc = BEAN; 172 type = "object"; 173 } else if (sType.isCollection()) { 174 tc = COLLECTION; 175 type = "array"; 176 } else if (sType.isArray()) { 177 tc = ARRAY; 178 type = "array"; 179 } else if (sType.isEnum()) { 180 tc = ENUM; 181 type = "string"; 182 } else if (sType.isCharSequence() || sType.isChar()) { 183 tc = STRING; 184 type = "string"; 185 } else if (sType.isUri()) { 186 tc = STRING; 187 type = "string"; 188 format = "uri"; 189 } else { 190 tc = STRING; 191 type = "string"; 192 } 193 194 // Add info from @Schema on bean property. 195 if (jsbpm != null) { 196 out.appendAll(jsbpm.getSchema()); 197 } 198 199 out.appendAll(jscm.getSchema()); 200 201 out.appendIf(false, true, true, "type", type); 202 out.appendIf(false, true, true, "format", format); 203 204 if (aType != null) { 205 206 example = getExample(sType, tc, exampleAdded); 207 description = getDescription(sType, tc, descriptionAdded); 208 exampleAdded |= example != null; 209 descriptionAdded |= description != null; 210 211 if (tc == BEAN) { 212 ObjectMap properties = new ObjectMap(); 213 BeanMeta bm = getBeanMeta(sType.getInnerClass()); 214 if (pNames != null) 215 bm = new BeanMetaFiltered(bm, pNames); 216 for (Iterator<BeanPropertyMeta> i = bm.getPropertyMetas().iterator(); i.hasNext();) { 217 BeanPropertyMeta p = i.next(); 218 if (p.canRead()) 219 properties.put(p.getName(), getSchema(p.getClassMeta(), p.getName(), p.getProperties(), exampleAdded, descriptionAdded, p.getExtendedMeta(JsonSchemaBeanPropertyMeta.class))); 220 } 221 out.put("properties", properties); 222 223 } else if (tc == COLLECTION) { 224 ClassMeta et = sType.getElementType(); 225 if (sType.isCollection() && isParentClass(Set.class, sType.getInnerClass())) 226 out.put("uniqueItems", true); 227 out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null)); 228 229 } else if (tc == ARRAY) { 230 ClassMeta et = sType.getElementType(); 231 if (sType.isCollection() && isParentClass(Set.class, sType.getInnerClass())) 232 out.put("uniqueItems", true); 233 out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null)); 234 235 } else if (tc == ENUM) { 236 out.put("enum", getEnums(sType)); 237 238 } else if (tc == MAP) { 239 ObjectMap om = getSchema(sType.getValueType(), "additionalProperties", null, exampleAdded, descriptionAdded, null); 240 if (! om.isEmpty()) 241 out.put("additionalProperties", om); 242 243 } 244 } 245 246 out.appendAll(jscm.getSchema()); 247 248 out.appendIf(false, true, true, "description", description); 249 out.appendIf(false, true, true, "x-example", example); 250 251 if (ds != null) 252 out.appendAll(ds); 253 254 if (useDef) { 255 defs.put(getBeanDefId(sType), out); 256 out = new ObjectMap().append("$ref", getBeanDefUri(sType)); 257 } 258 259 pop(); 260 261 return out; 262 } 263 264 private List<String> getEnums(ClassMeta<?> cm) { 265 List<String> l = new ArrayList<>(); 266 for (Enum<?> e : getEnumConstants(cm.getInnerClass())) 267 l.add(cm.toString(e)); 268 return l; 269 } 270 271 private Object getExample(ClassMeta<?> sType, TypeCategory t, boolean exampleAdded) throws Exception { 272 boolean canAdd = isAllowNestedExamples() || ! exampleAdded; 273 if (canAdd && (getAddExamplesTo().contains(t) || getAddExamplesTo().contains(ANY))) { 274 Object example = sType.getExample(this); 275 if (example != null) 276 return JsonParser.DEFAULT.parse(toJson(example), Object.class); 277 } 278 return null; 279 } 280 281 private String toJson(Object o) throws SerializeException { 282 if (jsSession == null) 283 jsSession = ctx.getJsonSerializer().createSession(null); 284 return jsSession.serializeToString(o); 285 } 286 287 private Object getDescription(ClassMeta<?> sType, TypeCategory t, boolean descriptionAdded) { 288 boolean canAdd = isAllowNestedDescriptions() || ! descriptionAdded; 289 if (canAdd && (getAddDescriptionsTo().contains(t) || getAddDescriptionsTo().contains(ANY))) 290 return sType.toString(); 291 return null; 292 } 293 294 /** 295 * Returns the definition ID for the specified class. 296 * 297 * @param cm The class to get the definition ID of. 298 * @return The definition ID for the specified class. 299 */ 300 public String getBeanDefId(ClassMeta<?> cm) { 301 return getBeanDefMapper().getId(cm); 302 } 303 304 /** 305 * Returns the definition URI for the specified class. 306 * 307 * @param cm The class to get the definition URI of. 308 * @return The definition URI for the specified class. 309 */ 310 public java.net.URI getBeanDefUri(ClassMeta<?> cm) { 311 return getBeanDefMapper().getURI(cm); 312 } 313 314 /** 315 * Returns the definition URI for the specified class. 316 * 317 * @param id The definition ID to get the definition URI of. 318 * @return The definition URI for the specified class. 319 */ 320 public java.net.URI getBeanDefUri(String id) { 321 return getBeanDefMapper().getURI(id); 322 } 323 324 /** 325 * Returns the definitions that were gathered during this session. 326 * 327 * <p> 328 * This map is modifiable and affects the map in the session. 329 * 330 * @return 331 * The definitions that were gathered during this session, or <jk>null</jk> if {@link JsonSchemaGenerator#JSONSCHEMA_useBeanDefs} was not enabled. 332 */ 333 public Map<String,ObjectMap> getBeanDefs() { 334 return defs; 335 } 336 337 /** 338 * Adds a schema definition to this session. 339 * 340 * @param id The definition ID. 341 * @param def The definition schema. 342 * @return This object (for method chaining). 343 */ 344 public JsonSchemaGeneratorSession addBeanDef(String id, ObjectMap def) { 345 if (defs != null) 346 defs.put(id, def); 347 return this; 348 } 349 350 //----------------------------------------------------------------------------------------------------------------- 351 // Properties 352 //----------------------------------------------------------------------------------------------------------------- 353 354 /** 355 * Configuration property: Use bean definitions. 356 * 357 * @see JsonSchemaGenerator#JSONSCHEMA_useBeanDefs 358 * @return 359 * <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags. 360 */ 361 protected final boolean isUseBeanDefs() { 362 return ctx.isUseBeanDefs(); 363 } 364 365 /** 366 * Configuration property: Allow nested examples. 367 * 368 * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedExamples 369 * @return 370 * <jk>true</jk> if nested examples are allowed in schema definitions. 371 */ 372 protected final boolean isAllowNestedExamples() { 373 return ctx.isAllowNestedExamples(); 374 } 375 376 /** 377 * Configuration property: Allow nested descriptions. 378 * 379 * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedDescriptions 380 * @return 381 * <jk>true</jk> if nested descriptions are allowed in schema definitions. 382 */ 383 protected final boolean isAllowNestedDescriptions() { 384 return ctx.isAllowNestedDescriptions(); 385 } 386 387 /** 388 * Configuration property: Bean schema definition mapper. 389 * 390 * @see JsonSchemaGenerator#JSONSCHEMA_beanDefMapper 391 * @return 392 * Interface to use for converting Bean classes to definition IDs and URIs. 393 */ 394 protected final BeanDefMapper getBeanDefMapper() { 395 return ctx.getBeanDefMapper(); 396 } 397 398 /** 399 * Configuration property: Add examples. 400 * 401 * @see JsonSchemaGenerator#JSONSCHEMA_addExamplesTo 402 * @return 403 * Set of categories of types that examples should be automatically added to generated schemas. 404 */ 405 protected final Set<TypeCategory> getAddExamplesTo() { 406 return ctx.getAddExamplesTo(); 407 } 408 409 /** 410 * Configuration property: Add descriptions to types. 411 * 412 * @see JsonSchemaGenerator#JSONSCHEMA_addDescriptionsTo 413 * @return 414 * Set of categories of types that descriptions should be automatically added to generated schemas. 415 */ 416 protected final Set<TypeCategory> getAddDescriptionsTo() { 417 return ctx.getAddDescriptionsTo(); 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,ObjectMap> getDefaultSchemas() { 428 return ctx.getDefaultSchemas(); 429 } 430 431 //----------------------------------------------------------------------------------------------------------------- 432 // Utility methods 433 //----------------------------------------------------------------------------------------------------------------- 434 435 private ClassMeta<?> toClassMeta(Object o) { 436 if (o instanceof Type) 437 return getClassMeta((Type)o); 438 return getClassMetaForObject(o); 439 } 440}