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.ObjectUtils.*;
016import static org.apache.juneau.jsonschema.TypeCategory.*;
017
018import java.lang.reflect.*;
019import java.util.*;
020import java.util.regex.*;
021
022import org.apache.juneau.*;
023import org.apache.juneau.collections.*;
024import org.apache.juneau.json.*;
025import org.apache.juneau.jsonschema.annotation.*;
026import org.apache.juneau.parser.ParseException;
027import org.apache.juneau.serializer.*;
028import org.apache.juneau.transform.*;
029
030/**
031 * Session object that lives for the duration of a single use of {@link JsonSchemaSerializer}.
032 *
033 * <p>
034 * This class is NOT thread safe.
035 * It is typically discarded after one-time use although it can be reused within the same thread.
036 */
037public class JsonSchemaGeneratorSession extends BeanTraverseSession {
038
039   private final JsonSchemaGenerator ctx;
040   private final Map<String,OMap> defs;
041   private JsonSerializerSession jsSession;
042
043   /**
044    * Create a new session using properties specified in the context.
045    *
046    * @param ctx
047    *    The context creating this session object.
048    *    The context contains all the configuration settings for this object.
049    * @param args
050    *    Runtime arguments.
051    *    These specify session-level information such as locale and URI context.
052    *    It also include session-level properties that override the properties defined on the bean and
053    *    serializer contexts.
054    */
055   protected JsonSchemaGeneratorSession(JsonSchemaGenerator ctx, BeanSessionArgs args) {
056      super(ctx, args);
057      this.ctx = ctx;
058      if (isUseBeanDefs())
059         defs = new TreeMap<>();
060      else
061         defs = null;
062   }
063
064   /**
065    * Returns the JSON-schema for the specified object.
066    *
067    * @param o
068    *    The object.
069    *    <br>Can either be a POJO or a <c>Class</c>/<c>Type</c>.
070    * @return The schema for the type.
071    * @throws BeanRecursionException Bean recursion occurred.
072    * @throws SerializeException Error occurred.
073    */
074   public OMap getSchema(Object o) throws BeanRecursionException, SerializeException {
075      return getSchema(toClassMeta(o), "root", null, false, false, null);
076   }
077
078   /**
079    * Returns the JSON-schema for the specified type.
080    *
081    * @param type The object type.
082    * @return The schema for the type.
083    * @throws BeanRecursionException Bean recursion occurred.
084    * @throws SerializeException Error occurred.
085    */
086   public OMap getSchema(Type type) throws BeanRecursionException, SerializeException {
087      return getSchema(getClassMeta(type), "root", null, false, false, null);
088   }
089
090   /**
091    * Returns the JSON-schema for the specified type.
092    *
093    * @param cm The object type.
094    * @return The schema for the type.
095    * @throws BeanRecursionException Bean recursion occurred.
096    * @throws SerializeException Error occurred.
097    */
098   public OMap getSchema(ClassMeta<?> cm) throws BeanRecursionException, SerializeException {
099      return getSchema(cm, "root", null, false, false, null);
100   }
101
102   @SuppressWarnings({ "unchecked", "rawtypes" })
103   private OMap getSchema(ClassMeta<?> eType, String attrName, String[] pNames, boolean exampleAdded, boolean descriptionAdded, JsonSchemaBeanPropertyMeta jsbpm) throws BeanRecursionException, SerializeException {
104
105      if (ctx.isIgnoredType(eType))
106         return null;
107
108      OMap out = new OMap();
109
110      if (eType == null)
111         eType = object();
112
113      ClassMeta<?> aType;        // The actual type (will be null if recursion occurs)
114      ClassMeta<?> sType;        // The serialized type
115      PojoSwap pojoSwap = eType.getSwap(this);
116
117      aType = push(attrName, eType, null);
118
119      sType = eType.getSerializedClassMeta(this);
120
121      String type = null, format = null;
122      Object example = null, description = null;
123
124      boolean useDef = isUseBeanDefs() && sType.isBean() && pNames == null;
125
126      if (useDef) {
127         exampleAdded = false;
128         descriptionAdded = false;
129      }
130
131      if (useDef && defs.containsKey(getBeanDefId(sType))) {
132         pop();
133         return new OMap().a("$ref", getBeanDefUri(sType));
134      }
135
136      OMap ds = getDefaultSchemas().get(sType.getInnerClass().getName());
137      if (ds != null && ds.containsKey("type")) {
138         pop();
139         return out.aa(ds);
140      }
141
142      JsonSchemaClassMeta jscm = null;
143      ClassMeta pojoSwapCM = pojoSwap == null ? null : getClassMeta(pojoSwap.getClass());
144      if (pojoSwapCM != null && pojoSwapCM.hasAnnotation(Schema.class))
145         jscm = getJsonSchemaClassMeta(pojoSwapCM);
146      if (jscm == null)
147         jscm = getJsonSchemaClassMeta(sType);
148
149      TypeCategory tc = null;
150
151      if (sType.isNumber()) {
152         tc = NUMBER;
153         if (sType.isDecimal()) {
154            type = "number";
155            if (sType.isFloat()) {
156               format = "float";
157            } else if (sType.isDouble()) {
158               format = "double";
159            }
160         } else {
161            type = "integer";
162            if (sType.isShort()) {
163               format = "int16";
164            } else if (sType.isInteger()) {
165               format = "int32";
166            } else if (sType.isLong()) {
167               format = "int64";
168            }
169         }
170      } else if (sType.isBoolean()) {
171         tc = BOOLEAN;
172         type = "boolean";
173      } else if (sType.isMap()) {
174         tc = MAP;
175         type = "object";
176      } else if (sType.isBean()) {
177         tc = BEAN;
178         type = "object";
179      } else if (sType.isCollection()) {
180         tc = COLLECTION;
181         type = "array";
182      } else if (sType.isArray()) {
183         tc = ARRAY;
184         type = "array";
185      } else if (sType.isEnum()) {
186         tc = ENUM;
187         type = "string";
188      } else if (sType.isCharSequence() || sType.isChar()) {
189         tc = STRING;
190         type = "string";
191      } else if (sType.isUri()) {
192         tc = STRING;
193         type = "string";
194         format = "uri";
195      } else {
196         tc = STRING;
197         type = "string";
198      }
199
200      // Add info from @Schema on bean property.
201      if (jsbpm != null) {
202         out.appendAll(jsbpm.getSchema());
203      }
204
205      out.appendAll(jscm.getSchema());
206
207      out.appendIf(false, true, true, "type", type);
208      out.appendIf(false, true, true, "format", format);
209
210      if (aType != null) {
211
212         example = getExample(sType, tc, exampleAdded);
213         description = getDescription(sType, tc, descriptionAdded);
214         exampleAdded |= example != null;
215         descriptionAdded |= description != null;
216
217         if (tc == BEAN) {
218            OMap properties = new OMap();
219            BeanMeta bm = getBeanMeta(sType.getInnerClass());
220            if (pNames != null)
221               bm = new BeanMetaFiltered(bm, pNames);
222            for (Iterator<BeanPropertyMeta> i = bm.getPropertyMetas().iterator(); i.hasNext();) {
223               BeanPropertyMeta p = i.next();
224               if (p.canRead())
225                  properties.put(p.getName(), getSchema(p.getClassMeta(), p.getName(), p.getProperties(), exampleAdded, descriptionAdded, getJsonSchemaBeanPropertyMeta(p)));
226            }
227            out.put("properties", properties);
228
229         } else if (tc == COLLECTION) {
230            ClassMeta et = sType.getElementType();
231            if (sType.isCollection() && sType.getInfo().isChildOf(Set.class))
232               out.put("uniqueItems", true);
233            out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null));
234
235         } else if (tc == ARRAY) {
236            ClassMeta et = sType.getElementType();
237            if (sType.isCollection() && sType.getInfo().isChildOf(Set.class))
238               out.put("uniqueItems", true);
239            out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null));
240
241         } else if (tc == ENUM) {
242            out.put("enum", getEnums(sType));
243
244         } else if (tc == MAP) {
245            OMap om = getSchema(sType.getValueType(), "additionalProperties", null, exampleAdded, descriptionAdded, null);
246            if (! om.isEmpty())
247               out.put("additionalProperties", om);
248
249         }
250      }
251
252      out.appendAll(jscm.getSchema());
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 = OMap.of("$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 SerializeException {
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            try {
283               return JsonParser.DEFAULT.parse(toJson(example), Object.class);
284            } catch (ParseException e) {
285               throw new SerializeException(e);
286            }
287         }
288      }
289      return null;
290   }
291
292   private String toJson(Object o) throws SerializeException {
293      if (jsSession == null)
294         jsSession = ctx.getJsonSerializer().createSession(null);
295      return jsSession.serializeToString(o);
296   }
297
298   private Object getDescription(ClassMeta<?> sType, TypeCategory t, boolean descriptionAdded) {
299      boolean canAdd = isAllowNestedDescriptions() || ! descriptionAdded;
300      if (canAdd && (getAddDescriptionsTo().contains(t) || getAddDescriptionsTo().contains(ANY)))
301         return sType.toString();
302      return null;
303   }
304
305   /**
306    * Returns the definition ID for the specified class.
307    *
308    * @param cm The class to get the definition ID of.
309    * @return The definition ID for the specified class.
310    */
311   public String getBeanDefId(ClassMeta<?> cm) {
312      return getBeanDefMapper().getId(cm);
313   }
314
315   /**
316    * Returns the definition URI for the specified class.
317    *
318    * @param cm The class to get the definition URI of.
319    * @return The definition URI for the specified class.
320    */
321   public java.net.URI getBeanDefUri(ClassMeta<?> cm) {
322      return getBeanDefMapper().getURI(cm);
323   }
324
325   /**
326    * Returns the definition URI for the specified class.
327    *
328    * @param id The definition ID to get the definition URI of.
329    * @return The definition URI for the specified class.
330    */
331   public java.net.URI getBeanDefUri(String id) {
332      return getBeanDefMapper().getURI(id);
333   }
334
335   /**
336    * Returns the definitions that were gathered during this session.
337    *
338    * <p>
339    * This map is modifiable and affects the map in the session.
340    *
341    * @return
342    *    The definitions that were gathered during this session, or <jk>null</jk> if {@link JsonSchemaGenerator#JSONSCHEMA_useBeanDefs} was not enabled.
343    */
344   public Map<String,OMap> getBeanDefs() {
345      return defs;
346   }
347
348   /**
349    * Adds a schema definition to this session.
350    *
351    * @param id The definition ID.
352    * @param def The definition schema.
353    * @return This object (for method chaining).
354    */
355   public JsonSchemaGeneratorSession addBeanDef(String id, OMap def) {
356      if (defs != null)
357         defs.put(id, def);
358      return this;
359   }
360
361   //-----------------------------------------------------------------------------------------------------------------
362   // Properties
363   //-----------------------------------------------------------------------------------------------------------------
364
365   /**
366    * Configuration property:  Add descriptions to types.
367    *
368    * @see JsonSchemaGenerator#JSONSCHEMA_addDescriptionsTo
369    * @return
370    *    Set of categories of types that descriptions should be automatically added to generated schemas.
371    */
372   protected final Set<TypeCategory> getAddDescriptionsTo() {
373      return ctx.getAddDescriptionsTo();
374   }
375
376   /**
377    * Configuration property:  Add examples.
378    *
379    * @see JsonSchemaGenerator#JSONSCHEMA_addExamplesTo
380    * @return
381    *    Set of categories of types that examples should be automatically added to generated schemas.
382    */
383   protected final Set<TypeCategory> getAddExamplesTo() {
384      return ctx.getAddExamplesTo();
385   }
386
387   /**
388    * Configuration property:  Allow nested descriptions.
389    *
390    * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedDescriptions
391    * @return
392    *    <jk>true</jk> if nested descriptions are allowed in schema definitions.
393    */
394   protected final boolean isAllowNestedDescriptions() {
395      return ctx.isAllowNestedDescriptions();
396   }
397
398   /**
399    * Configuration property:  Allow nested examples.
400    *
401    * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedExamples
402    * @return
403    *    <jk>true</jk> if nested examples are allowed in schema definitions.
404    */
405   protected final boolean isAllowNestedExamples() {
406      return ctx.isAllowNestedExamples();
407   }
408
409   /**
410    * Configuration property:  Bean schema definition mapper.
411    *
412    * @see JsonSchemaGenerator#JSONSCHEMA_beanDefMapper
413    * @return
414    *    Interface to use for converting Bean classes to definition IDs and URIs.
415    */
416   protected final BeanDefMapper getBeanDefMapper() {
417      return ctx.getBeanDefMapper();
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,OMap> getDefaultSchemas() {
428      return ctx.getDefaultSchemas();
429   }
430
431   /**
432    * Configuration property:  Ignore types from schema definitions.
433    *
434    * @see JsonSchemaGenerator#JSONSCHEMA_ignoreTypes
435    * @return
436    *    Custom schema information for particular class types.
437    */
438   protected final Set<Pattern> getIgnoreTypes() {
439      return ctx.getIgnoreTypes();
440   }
441
442   /**
443    * Configuration property:  Use bean definitions.
444    *
445    * @see JsonSchemaGenerator#JSONSCHEMA_useBeanDefs
446    * @return
447    *    <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags.
448    */
449   protected final boolean isUseBeanDefs() {
450      return ctx.isUseBeanDefs();
451   }
452
453   //-----------------------------------------------------------------------------------------------------------------
454   // Extended metadata
455   //-----------------------------------------------------------------------------------------------------------------
456
457   /**
458    * Returns the language-specific metadata on the specified class.
459    *
460    * @param cm The class to return the metadata on.
461    * @return The metadata.
462    */
463   public JsonSchemaClassMeta getJsonSchemaClassMeta(ClassMeta<?> cm) {
464      return ctx.getJsonSchemaClassMeta(cm);
465   }
466
467   /**
468    * Returns the language-specific metadata on the specified bean property.
469    *
470    * @param bpm The bean property to return the metadata on.
471    * @return The metadata.
472    */
473   public JsonSchemaBeanPropertyMeta getJsonSchemaBeanPropertyMeta(BeanPropertyMeta bpm) {
474      return ctx.getJsonSchemaBeanPropertyMeta(bpm);
475   }
476
477   //-----------------------------------------------------------------------------------------------------------------
478   // Utility methods
479   //-----------------------------------------------------------------------------------------------------------------
480
481   private ClassMeta<?> toClassMeta(Object o) {
482      if (o instanceof Type)
483         return getClassMeta((Type)o);
484      return getClassMetaForObject(o);
485   }
486
487   //-----------------------------------------------------------------------------------------------------------------
488   // Other methods
489   //-----------------------------------------------------------------------------------------------------------------
490
491   @Override /* Session */
492   public OMap toMap() {
493      return super.toMap()
494         .a("JsonSchemaGeneratorSession", new DefaultFilteringOMap()
495         );
496   }
497}