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