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      ClassMeta pojoSwapCM = pojoSwap == null ? null : getClassMeta(pojoSwap.getClass());
143      if (pojoSwapCM != null && pojoSwapCM.getAnnotation(Schema.class) != null)
144         jscm = getJsonSchemaClassMeta(pojoSwapCM);
145      if (jscm == null)
146         jscm = getJsonSchemaClassMeta(sType);
147
148      TypeCategory tc = null;
149
150      if (sType.isNumber()) {
151         tc = NUMBER;
152         if (sType.isDecimal()) {
153            type = "number";
154            if (sType.isFloat()) {
155               format = "float";
156            } else if (sType.isDouble()) {
157               format = "double";
158            }
159         } else {
160            type = "integer";
161            if (sType.isShort()) {
162               format = "int16";
163            } else if (sType.isInteger()) {
164               format = "int32";
165            } else if (sType.isLong()) {
166               format = "int64";
167            }
168         }
169      } else if (sType.isBoolean()) {
170         tc = BOOLEAN;
171         type = "boolean";
172      } else if (sType.isMap()) {
173         tc = MAP;
174         type = "object";
175      } else if (sType.isBean()) {
176         tc = BEAN;
177         type = "object";
178      } else if (sType.isCollection()) {
179         tc = COLLECTION;
180         type = "array";
181      } else if (sType.isArray()) {
182         tc = ARRAY;
183         type = "array";
184      } else if (sType.isEnum()) {
185         tc = ENUM;
186         type = "string";
187      } else if (sType.isCharSequence() || sType.isChar()) {
188         tc = STRING;
189         type = "string";
190      } else if (sType.isUri()) {
191         tc = STRING;
192         type = "string";
193         format = "uri";
194      } else {
195         tc = STRING;
196         type = "string";
197      }
198
199      // Add info from @Schema on bean property.
200      if (jsbpm != null) {
201         out.appendAll(jsbpm.getSchema());
202      }
203
204      out.appendAll(jscm.getSchema());
205
206      out.appendIf(false, true, true, "type", type);
207      out.appendIf(false, true, true, "format", format);
208
209      if (aType != null) {
210
211         example = getExample(sType, tc, exampleAdded);
212         description = getDescription(sType, tc, descriptionAdded);
213         exampleAdded |= example != null;
214         descriptionAdded |= description != null;
215
216         if (tc == BEAN) {
217            ObjectMap properties = new ObjectMap();
218            BeanMeta bm = getBeanMeta(sType.getInnerClass());
219            if (pNames != null)
220               bm = new BeanMetaFiltered(bm, pNames);
221            for (Iterator<BeanPropertyMeta> i = bm.getPropertyMetas().iterator(); i.hasNext();) {
222               BeanPropertyMeta p = i.next();
223               if (p.canRead())
224                  properties.put(p.getName(), getSchema(p.getClassMeta(), p.getName(), p.getProperties(), exampleAdded, descriptionAdded, getJsonSchemaBeanPropertyMeta(p)));
225            }
226            out.put("properties", properties);
227
228         } else if (tc == COLLECTION) {
229            ClassMeta et = sType.getElementType();
230            if (sType.isCollection() && sType.getInfo().isChildOf(Set.class))
231               out.put("uniqueItems", true);
232            out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null));
233
234         } else if (tc == ARRAY) {
235            ClassMeta et = sType.getElementType();
236            if (sType.isCollection() && sType.getInfo().isChildOf(Set.class))
237               out.put("uniqueItems", true);
238            out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null));
239
240         } else if (tc == ENUM) {
241            out.put("enum", getEnums(sType));
242
243         } else if (tc == MAP) {
244            ObjectMap om = getSchema(sType.getValueType(), "additionalProperties", null, exampleAdded, descriptionAdded, null);
245            if (! om.isEmpty())
246               out.put("additionalProperties", om);
247
248         }
249      }
250
251      out.appendAll(jscm.getSchema());
252
253      out.appendIf(false, true, true, "description", description);
254      out.appendIf(false, true, true, "x-example", example);
255
256      if (ds != null)
257         out.appendAll(ds);
258
259      if (useDef) {
260         defs.put(getBeanDefId(sType), out);
261         out = new ObjectMap().append("$ref", getBeanDefUri(sType));
262      }
263
264      pop();
265
266      return out;
267   }
268
269   private List<String> getEnums(ClassMeta<?> cm) {
270      List<String> l = new ArrayList<>();
271      for (Enum<?> e : getEnumConstants(cm.getInnerClass()))
272         l.add(cm.toString(e));
273      return l;
274   }
275
276   private Object getExample(ClassMeta<?> sType, TypeCategory t, boolean exampleAdded) throws SerializeException {
277      boolean canAdd = isAllowNestedExamples() || ! exampleAdded;
278      if (canAdd && (getAddExamplesTo().contains(t) || getAddExamplesTo().contains(ANY))) {
279         Object example = sType.getExample(this);
280         if (example != null) {
281            try {
282               return JsonParser.DEFAULT.parse(toJson(example), Object.class);
283            } catch (ParseException e) {
284               throw new SerializeException(e);
285            }
286         }
287      }
288      return null;
289   }
290
291   private String toJson(Object o) throws SerializeException {
292      if (jsSession == null)
293         jsSession = ctx.getJsonSerializer().createSession(null);
294      return jsSession.serializeToString(o);
295   }
296
297   private Object getDescription(ClassMeta<?> sType, TypeCategory t, boolean descriptionAdded) {
298      boolean canAdd = isAllowNestedDescriptions() || ! descriptionAdded;
299      if (canAdd && (getAddDescriptionsTo().contains(t) || getAddDescriptionsTo().contains(ANY)))
300         return sType.toString();
301      return null;
302   }
303
304   /**
305    * Returns the definition ID for the specified class.
306    *
307    * @param cm The class to get the definition ID of.
308    * @return The definition ID for the specified class.
309    */
310   public String getBeanDefId(ClassMeta<?> cm) {
311      return getBeanDefMapper().getId(cm);
312   }
313
314   /**
315    * Returns the definition URI for the specified class.
316    *
317    * @param cm The class to get the definition URI of.
318    * @return The definition URI for the specified class.
319    */
320   public java.net.URI getBeanDefUri(ClassMeta<?> cm) {
321      return getBeanDefMapper().getURI(cm);
322   }
323
324   /**
325    * Returns the definition URI for the specified class.
326    *
327    * @param id The definition ID to get the definition URI of.
328    * @return The definition URI for the specified class.
329    */
330   public java.net.URI getBeanDefUri(String id) {
331      return getBeanDefMapper().getURI(id);
332   }
333
334   /**
335    * Returns the definitions that were gathered during this session.
336    *
337    * <p>
338    * This map is modifiable and affects the map in the session.
339    *
340    * @return
341    *    The definitions that were gathered during this session, or <jk>null</jk> if {@link JsonSchemaGenerator#JSONSCHEMA_useBeanDefs} was not enabled.
342    */
343   public Map<String,ObjectMap> getBeanDefs() {
344      return defs;
345   }
346
347   /**
348    * Adds a schema definition to this session.
349    *
350    * @param id The definition ID.
351    * @param def The definition schema.
352    * @return This object (for method chaining).
353    */
354   public JsonSchemaGeneratorSession addBeanDef(String id, ObjectMap def) {
355      if (defs != null)
356         defs.put(id, def);
357      return this;
358   }
359
360   //-----------------------------------------------------------------------------------------------------------------
361   // Properties
362   //-----------------------------------------------------------------------------------------------------------------
363
364   /**
365    * Configuration property:  Add descriptions to types.
366    *
367    * @see JsonSchemaGenerator#JSONSCHEMA_addDescriptionsTo
368    * @return
369    *    Set of categories of types that descriptions should be automatically added to generated schemas.
370    */
371   protected final Set<TypeCategory> getAddDescriptionsTo() {
372      return ctx.getAddDescriptionsTo();
373   }
374
375   /**
376    * Configuration property:  Add examples.
377    *
378    * @see JsonSchemaGenerator#JSONSCHEMA_addExamplesTo
379    * @return
380    *    Set of categories of types that examples should be automatically added to generated schemas.
381    */
382   protected final Set<TypeCategory> getAddExamplesTo() {
383      return ctx.getAddExamplesTo();
384   }
385
386   /**
387    * Configuration property:  Allow nested descriptions.
388    *
389    * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedDescriptions
390    * @return
391    *    <jk>true</jk> if nested descriptions are allowed in schema definitions.
392    */
393   protected final boolean isAllowNestedDescriptions() {
394      return ctx.isAllowNestedDescriptions();
395   }
396
397   /**
398    * Configuration property:  Allow nested examples.
399    *
400    * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedExamples
401    * @return
402    *    <jk>true</jk> if nested examples are allowed in schema definitions.
403    */
404   protected final boolean isAllowNestedExamples() {
405      return ctx.isAllowNestedExamples();
406   }
407
408   /**
409    * Configuration property:  Bean schema definition mapper.
410    *
411    * @see JsonSchemaGenerator#JSONSCHEMA_beanDefMapper
412    * @return
413    *    Interface to use for converting Bean classes to definition IDs and URIs.
414    */
415   protected final BeanDefMapper getBeanDefMapper() {
416      return ctx.getBeanDefMapper();
417   }
418
419   /**
420    * Configuration property:  Default schemas.
421    *
422    * @see JsonSchemaGenerator#JSONSCHEMA_defaultSchemas
423    * @return
424    *    Custom schema information for particular class types.
425    */
426   protected final Map<String,ObjectMap> getDefaultSchemas() {
427      return ctx.getDefaultSchemas();
428   }
429
430   /**
431    * Configuration property:  Ignore types from schema definitions.
432    *
433    * @see JsonSchemaGenerator#JSONSCHEMA_ignoreTypes
434    * @return
435    *    Custom schema information for particular class types.
436    */
437   protected final Set<Pattern> getIgnoreTypes() {
438      return ctx.getIgnoreTypes();
439   }
440
441   /**
442    * Configuration property:  Use bean definitions.
443    *
444    * @see JsonSchemaGenerator#JSONSCHEMA_useBeanDefs
445    * @return
446    *    <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags.
447    */
448   protected final boolean isUseBeanDefs() {
449      return ctx.isUseBeanDefs();
450   }
451
452   //-----------------------------------------------------------------------------------------------------------------
453   // Extended metadata
454   //-----------------------------------------------------------------------------------------------------------------
455
456   /**
457    * Returns the language-specific metadata on the specified class.
458    *
459    * @param cm The class to return the metadata on.
460    * @return The metadata.
461    */
462   public JsonSchemaClassMeta getJsonSchemaClassMeta(ClassMeta<?> cm) {
463      return ctx.getJsonSchemaClassMeta(cm);
464   }
465
466   /**
467    * Returns the language-specific metadata on the specified bean property.
468    *
469    * @param bpm The bean property to return the metadata on.
470    * @return The metadata.
471    */
472   public JsonSchemaBeanPropertyMeta getJsonSchemaBeanPropertyMeta(BeanPropertyMeta bpm) {
473      return ctx.getJsonSchemaBeanPropertyMeta(bpm);
474   }
475
476   //-----------------------------------------------------------------------------------------------------------------
477   // Utility methods
478   //-----------------------------------------------------------------------------------------------------------------
479
480   private ClassMeta<?> toClassMeta(Object o) {
481      if (o instanceof Type)
482         return getClassMeta((Type)o);
483      return getClassMetaForObject(o);
484   }
485
486   //-----------------------------------------------------------------------------------------------------------------
487   // Other methods
488   //-----------------------------------------------------------------------------------------------------------------
489
490   @Override /* Session */
491   public ObjectMap toMap() {
492      return super.toMap()
493         .append("JsonSchemaGeneratorSession", new DefaultFilteringObjectMap()
494         );
495   }
496}