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(Schema.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 @Schema on bean property. 192 if (jsbpm != null) { 193 out.appendAll(jsbpm.getSchema()); 194 } 195 196 out.appendAll(jscm.getSchema()); 197 198 out.appendIf(false, true, true, "type", type); 199 out.appendIf(false, true, true, "format", format); 200 201 if (aType != null) { 202 203 example = getExample(sType, tc, exampleAdded); 204 description = getDescription(sType, tc, descriptionAdded); 205 exampleAdded |= example != null; 206 descriptionAdded |= description != null; 207 208 if (tc == BEAN) { 209 ObjectMap properties = new ObjectMap(); 210 BeanMeta bm = getBeanMeta(sType.getInnerClass()); 211 if (pNames != null) 212 bm = new BeanMetaFiltered(bm, pNames); 213 for (Iterator<BeanPropertyMeta> i = bm.getPropertyMetas().iterator(); i.hasNext();) { 214 BeanPropertyMeta p = i.next(); 215 if (p.canRead()) 216 properties.put(p.getName(), getSchema(p.getClassMeta(), p.getName(), p.getProperties(), exampleAdded, descriptionAdded, p.getExtendedMeta(JsonSchemaBeanPropertyMeta.class))); 217 } 218 out.put("properties", properties); 219 220 } else if (tc == COLLECTION) { 221 ClassMeta et = sType.getElementType(); 222 if (sType.isCollection() && isParentClass(Set.class, sType.getInnerClass())) 223 out.put("uniqueItems", true); 224 out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null)); 225 226 } else if (tc == ARRAY) { 227 ClassMeta et = sType.getElementType(); 228 if (sType.isCollection() && isParentClass(Set.class, sType.getInnerClass())) 229 out.put("uniqueItems", true); 230 out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null)); 231 232 } else if (tc == ENUM) { 233 out.put("enum", getEnums(sType)); 234 235 } else if (tc == MAP) { 236 ObjectMap om = getSchema(sType.getValueType(), "additionalProperties", null, exampleAdded, descriptionAdded, null); 237 if (! om.isEmpty()) 238 out.put("additionalProperties", om); 239 240 } 241 } 242 243 out.appendAll(jscm.getSchema()); 244 245 out.appendIf(false, true, true, "description", description); 246 out.appendIf(false, true, true, "x-example", example); 247 248 if (ds != null) 249 out.appendAll(ds); 250 251 if (useDef) { 252 defs.put(getBeanDefId(sType), out); 253 out = new ObjectMap().append("$ref", getBeanDefUri(sType)); 254 } 255 256 pop(); 257 258 return out; 259 } 260 261 private List<String> getEnums(ClassMeta<?> cm) { 262 List<String> l = new ArrayList<>(); 263 for (Enum<?> e : getEnumConstants(cm.getInnerClass())) 264 l.add(cm.toString(e)); 265 return l; 266 } 267 268 private Object getExample(ClassMeta<?> sType, TypeCategory t, boolean exampleAdded) throws Exception { 269 boolean canAdd = isAllowNestedExamples() || ! exampleAdded; 270 if (canAdd && (getAddExamplesTo().contains(t) || getAddExamplesTo().contains(ANY))) { 271 Object example = sType.getExample(this); 272 if (example != null) 273 return JsonParser.DEFAULT.parse(toJson(example), Object.class); 274 } 275 return null; 276 } 277 278 private String toJson(Object o) throws SerializeException { 279 if (jsSession == null) 280 jsSession = ctx.getJsonSerializer().createSession(null); 281 return jsSession.serializeToString(o); 282 } 283 284 private Object getDescription(ClassMeta<?> sType, TypeCategory t, boolean descriptionAdded) { 285 boolean canAdd = isAllowNestedDescriptions() || ! descriptionAdded; 286 if (canAdd && (getAddDescriptionsTo().contains(t) || getAddDescriptionsTo().contains(ANY))) 287 return sType.toString(); 288 return null; 289 } 290 291 /** 292 * Returns the definition ID for the specified class. 293 * 294 * @param cm The class to get the definition ID of. 295 * @return The definition ID for the specified class. 296 */ 297 public String getBeanDefId(ClassMeta<?> cm) { 298 return getBeanDefMapper().getId(cm); 299 } 300 301 /** 302 * Returns the definition URI for the specified class. 303 * 304 * @param cm The class to get the definition URI of. 305 * @return The definition URI for the specified class. 306 */ 307 public java.net.URI getBeanDefUri(ClassMeta<?> cm) { 308 return getBeanDefMapper().getURI(cm); 309 } 310 311 /** 312 * Returns the definition URI for the specified class. 313 * 314 * @param id The definition ID to get the definition URI of. 315 * @return The definition URI for the specified class. 316 */ 317 public java.net.URI getBeanDefUri(String id) { 318 return getBeanDefMapper().getURI(id); 319 } 320 321 /** 322 * Returns the definitions that were gathered during this session. 323 * 324 * <p> 325 * This map is modifiable and affects the map in the session. 326 * 327 * @return 328 * The definitions that were gathered during this session, or <jk>null</jk> if {@link JsonSchemaGenerator#JSONSCHEMA_useBeanDefs} was not enabled. 329 */ 330 public Map<String,ObjectMap> getBeanDefs() { 331 return defs; 332 } 333 334 /** 335 * Adds a schema definition to this session. 336 * 337 * @param id The definition ID. 338 * @param def The definition schema. 339 * @return This object (for method chaining). 340 */ 341 public JsonSchemaGeneratorSession addBeanDef(String id, ObjectMap def) { 342 if (defs != null) 343 defs.put(id, def); 344 return this; 345 } 346 347 //----------------------------------------------------------------------------------------------------------------- 348 // Properties 349 //----------------------------------------------------------------------------------------------------------------- 350 351 /** 352 * Configuration property: Use bean definitions. 353 * 354 * @see JsonSchemaGenerator#JSONSCHEMA_useBeanDefs 355 * @return 356 * <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags. 357 */ 358 protected final boolean isUseBeanDefs() { 359 return ctx.isUseBeanDefs(); 360 } 361 362 /** 363 * Configuration property: Allow nested examples. 364 * 365 * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedExamples 366 * @return 367 * <jk>true</jk> if nested examples are allowed in schema definitions. 368 */ 369 protected final boolean isAllowNestedExamples() { 370 return ctx.isAllowNestedExamples(); 371 } 372 373 /** 374 * Configuration property: Allow nested descriptions. 375 * 376 * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedDescriptions 377 * @return 378 * <jk>true</jk> if nested descriptions are allowed in schema definitions. 379 */ 380 protected final boolean isAllowNestedDescriptions() { 381 return ctx.isAllowNestedDescriptions(); 382 } 383 384 /** 385 * Configuration property: Bean schema definition mapper. 386 * 387 * @see JsonSchemaGenerator#JSONSCHEMA_beanDefMapper 388 * @return 389 * Interface to use for converting Bean classes to definition IDs and URIs. 390 */ 391 protected final BeanDefMapper getBeanDefMapper() { 392 return ctx.getBeanDefMapper(); 393 } 394 395 /** 396 * Configuration property: Add examples. 397 * 398 * @see JsonSchemaGenerator#JSONSCHEMA_addExamplesTo 399 * @return 400 * Set of categories of types that examples should be automatically added to generated schemas. 401 */ 402 protected final Set<TypeCategory> getAddExamplesTo() { 403 return ctx.getAddExamplesTo(); 404 } 405 406 /** 407 * Configuration property: Add descriptions to types. 408 * 409 * @see JsonSchemaGenerator#JSONSCHEMA_addDescriptionsTo 410 * @return 411 * Set of categories of types that descriptions should be automatically added to generated schemas. 412 */ 413 protected final Set<TypeCategory> getAddDescriptionsTo() { 414 return ctx.getAddDescriptionsTo(); 415 } 416 417 /** 418 * Configuration property: Default schemas. 419 * 420 * @see JsonSchemaGenerator#JSONSCHEMA_defaultSchemas 421 * @return 422 * Custom schema information for particular class types. 423 */ 424 protected final Map<String,ObjectMap> getDefaultSchemas() { 425 return ctx.getDefaultSchemas(); 426 } 427 428 //----------------------------------------------------------------------------------------------------------------- 429 // Utility methods 430 //----------------------------------------------------------------------------------------------------------------- 431 432 private ClassMeta<?> toClassMeta(Object o) { 433 if (o instanceof Type) 434 return getClassMeta((Type)o); 435 return getClassMetaForObject(o); 436 } 437}