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}