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}