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}