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.CollectionUtils.*;
016import static org.apache.juneau.jsonschema.TypeCategory.*;
017
018import java.lang.reflect.*;
019import java.util.*;
020import java.util.function.*;
021import java.util.regex.*;
022
023import org.apache.juneau.*;
024import org.apache.juneau.annotation.*;
025import org.apache.juneau.collections.*;
026import org.apache.juneau.common.internal.*;
027import org.apache.juneau.internal.*;
028import org.apache.juneau.json.*;
029import org.apache.juneau.parser.ParseException;
030import org.apache.juneau.serializer.*;
031import org.apache.juneau.swap.*;
032
033/**
034 * Session object that lives for the duration of a single use of {@link JsonSchemaSerializer}.
035 *
036 * <h5 class='section'>Notes:</h5><ul>
037 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
038 * </ul>
039 *
040 * <h5 class='section'>See Also:</h5><ul>
041 *    <li class='link'><a class="doclink" href="../../../../index.html#jm.JsonSchemaDetails">JSON-Schema Support</a>
042 * </ul>
043 */
044public class JsonSchemaGeneratorSession extends BeanTraverseSession {
045
046   //-----------------------------------------------------------------------------------------------------------------
047   // Static
048   //-----------------------------------------------------------------------------------------------------------------
049
050   /**
051    * Creates a new builder for this object.
052    *
053    * @param ctx The context creating this session.
054    * @return A new builder.
055    */
056   public static Builder create(JsonSchemaGenerator ctx) {
057      return new Builder(ctx);
058   }
059
060   //-----------------------------------------------------------------------------------------------------------------
061   // Builder
062   //-----------------------------------------------------------------------------------------------------------------
063
064   /**
065    * Builder class.
066    */
067   @FluentSetters
068   public static class Builder extends BeanTraverseSession.Builder {
069
070      JsonSchemaGenerator ctx;
071
072      /**
073       * Constructor
074       *
075       * @param ctx The context creating this session.
076       */
077      protected Builder(JsonSchemaGenerator ctx) {
078         super(ctx);
079         this.ctx = ctx;
080      }
081
082      @Override
083      public JsonSchemaGeneratorSession build() {
084         return new JsonSchemaGeneratorSession(this);
085      }
086
087      // <FluentSetters>
088
089      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
090      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
091         super.apply(type, apply);
092         return this;
093      }
094
095      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
096      public Builder debug(Boolean value) {
097         super.debug(value);
098         return this;
099      }
100
101      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
102      public Builder properties(Map<String,Object> value) {
103         super.properties(value);
104         return this;
105      }
106
107      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
108      public Builder property(String key, Object value) {
109         super.property(key, value);
110         return this;
111      }
112
113      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
114      public Builder unmodifiable() {
115         super.unmodifiable();
116         return this;
117      }
118
119      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
120      public Builder locale(Locale value) {
121         super.locale(value);
122         return this;
123      }
124
125      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
126      public Builder localeDefault(Locale value) {
127         super.localeDefault(value);
128         return this;
129      }
130
131      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
132      public Builder mediaType(MediaType value) {
133         super.mediaType(value);
134         return this;
135      }
136
137      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
138      public Builder mediaTypeDefault(MediaType value) {
139         super.mediaTypeDefault(value);
140         return this;
141      }
142
143      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
144      public Builder timeZone(TimeZone value) {
145         super.timeZone(value);
146         return this;
147      }
148
149      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
150      public Builder timeZoneDefault(TimeZone value) {
151         super.timeZoneDefault(value);
152         return this;
153      }
154
155      // </FluentSetters>
156   }
157
158   //-----------------------------------------------------------------------------------------------------------------
159   // Instance
160   //-----------------------------------------------------------------------------------------------------------------
161
162   private final JsonSchemaGenerator ctx;
163   private final Map<String,JsonMap> defs;
164   private JsonSerializerSession jsSession;
165   private JsonParserSession jpSession;
166
167   /**
168    * Constructor.
169    *
170    * @param builder The builder for this object.
171    */
172   protected JsonSchemaGeneratorSession(Builder builder) {
173      super(builder);
174      ctx = builder.ctx;
175      defs = isUseBeanDefs() ? new TreeMap<>() : null;
176   }
177
178   /**
179    * Returns the JSON-schema for the specified object.
180    *
181    * @param o
182    *    The object.
183    *    <br>Can either be a POJO or a <c>Class</c>/<c>Type</c>.
184    * @return The schema for the type.
185    * @throws BeanRecursionException Bean recursion occurred.
186    * @throws SerializeException Error occurred.
187    */
188   public JsonMap getSchema(Object o) throws BeanRecursionException, SerializeException {
189      return getSchema(toClassMeta(o), "root", null, false, false, null);
190   }
191
192   /**
193    * Returns the JSON-schema for the specified type.
194    *
195    * @param type The object type.
196    * @return The schema for the type.
197    * @throws BeanRecursionException Bean recursion occurred.
198    * @throws SerializeException Error occurred.
199    */
200   public JsonMap getSchema(Type type) throws BeanRecursionException, SerializeException {
201      return getSchema(getClassMeta(type), "root", null, false, false, null);
202   }
203
204   /**
205    * Returns the JSON-schema for the specified type.
206    *
207    * @param cm The object type.
208    * @return The schema for the type.
209    * @throws BeanRecursionException Bean recursion occurred.
210    * @throws SerializeException Error occurred.
211    */
212   public JsonMap getSchema(ClassMeta<?> cm) throws BeanRecursionException, SerializeException {
213      return getSchema(cm, "root", null, false, false, null);
214   }
215
216   @SuppressWarnings({ "unchecked", "rawtypes" })
217   private JsonMap getSchema(ClassMeta<?> eType, String attrName, String[] pNames, boolean exampleAdded, boolean descriptionAdded, JsonSchemaBeanPropertyMeta jsbpm) throws BeanRecursionException, SerializeException {
218
219      if (ctx.isIgnoredType(eType))
220         return null;
221
222      JsonMap out = new JsonMap();
223
224      if (eType == null)
225         eType = object();
226
227      ClassMeta<?> aType;        // The actual type (will be null if recursion occurs)
228      ClassMeta<?> sType;        // The serialized type
229      ObjectSwap objectSwap = eType.getSwap(this);
230
231      aType = push(attrName, eType, null);
232
233      sType = eType.getSerializedClassMeta(this);
234
235      String type = null, format = null;
236      Object example = null, description = null;
237
238      boolean useDef = isUseBeanDefs() && sType.isBean() && pNames == null;
239
240      if (useDef) {
241         exampleAdded = false;
242         descriptionAdded = false;
243      }
244
245      if (useDef && defs.containsKey(getBeanDefId(sType))) {
246         pop();
247         return new JsonMap().append("$ref", getBeanDefUri(sType));
248      }
249
250      JsonSchemaClassMeta jscm = null;
251      ClassMeta objectSwapCM = objectSwap == null ? null : getClassMeta(objectSwap.getClass());
252      if (objectSwapCM != null && objectSwapCM.hasAnnotation(Schema.class))
253         jscm = getJsonSchemaClassMeta(objectSwapCM);
254      if (jscm == null)
255         jscm = getJsonSchemaClassMeta(sType);
256
257      TypeCategory tc = null;
258
259      if (sType.isNumber()) {
260         tc = NUMBER;
261         if (sType.isDecimal()) {
262            type = "number";
263            if (sType.isFloat()) {
264               format = "float";
265            } else if (sType.isDouble()) {
266               format = "double";
267            }
268         } else {
269            type = "integer";
270            if (sType.isShort()) {
271               format = "int16";
272            } else if (sType.isInteger()) {
273               format = "int32";
274            } else if (sType.isLong()) {
275               format = "int64";
276            }
277         }
278      } else if (sType.isBoolean()) {
279         tc = BOOLEAN;
280         type = "boolean";
281      } else if (sType.isMap()) {
282         tc = MAP;
283         type = "object";
284      } else if (sType.isBean()) {
285         tc = BEAN;
286         type = "object";
287      } else if (sType.isCollection()) {
288         tc = COLLECTION;
289         type = "array";
290      } else if (sType.isArray()) {
291         tc = ARRAY;
292         type = "array";
293      } else if (sType.isEnum()) {
294         tc = ENUM;
295         type = "string";
296      } else if (sType.isCharSequence() || sType.isChar()) {
297         tc = STRING;
298         type = "string";
299      } else if (sType.isUri()) {
300         tc = STRING;
301         type = "string";
302         format = "uri";
303      } else {
304         tc = STRING;
305         type = "string";
306      }
307
308      // Add info from @Schema on bean property.
309      if (jsbpm != null) {
310         out.append(jsbpm.getSchema());
311      }
312
313      out.append(jscm.getSchema());
314
315      Predicate<String> ne = StringUtils::isNotEmpty;
316      out.appendIfAbsentIf(ne, "type", type);
317      out.appendIfAbsentIf(ne, "format", format);
318
319      if (aType != null) {
320
321         example = getExample(sType, tc, exampleAdded);
322         description = getDescription(sType, tc, descriptionAdded);
323         exampleAdded |= example != null;
324         descriptionAdded |= description != null;
325
326         if (tc == BEAN) {
327            JsonMap properties = new JsonMap();
328            BeanMeta bm = getBeanMeta(sType.getInnerClass());
329            if (pNames != null)
330               bm = new BeanMetaFiltered(bm, pNames);
331            for (Iterator<BeanPropertyMeta> i = bm.getPropertyMetas().iterator(); i.hasNext();) {
332               BeanPropertyMeta p = i.next();
333               if (p.canRead())
334                  properties.put(p.getName(), getSchema(p.getClassMeta(), p.getName(), p.getProperties(), exampleAdded, descriptionAdded, getJsonSchemaBeanPropertyMeta(p)));
335            }
336            out.put("properties", properties);
337
338         } else if (tc == COLLECTION) {
339            ClassMeta et = sType.getElementType();
340            if (sType.isCollection() && sType.getInfo().isChildOf(Set.class))
341               out.put("uniqueItems", true);
342            out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null));
343
344         } else if (tc == ARRAY) {
345            ClassMeta et = sType.getElementType();
346            if (sType.isCollection() && sType.getInfo().isChildOf(Set.class))
347               out.put("uniqueItems", true);
348            out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null));
349
350         } else if (tc == ENUM) {
351            out.put("enum", getEnums(sType));
352
353         } else if (tc == MAP) {
354            JsonMap om = getSchema(sType.getValueType(), "additionalProperties", null, exampleAdded, descriptionAdded, null);
355            if (! om.isEmpty())
356               out.put("additionalProperties", om);
357
358         }
359      }
360
361      out.append(jscm.getSchema());
362
363      Predicate<Object> neo = ObjectUtils::isNotEmpty;
364      out.appendIfAbsentIf(neo, "description", description);
365      out.appendIfAbsentIf(neo, "example", example);
366
367      if (useDef) {
368         defs.put(getBeanDefId(sType), out);
369         out = JsonMap.of("$ref", getBeanDefUri(sType));
370      }
371
372      pop();
373
374      return out;
375   }
376
377   @SuppressWarnings("unchecked")
378   private List<String> getEnums(ClassMeta<?> cm) {
379      List<String> l = list();
380      for (Enum<?> e : ((Class<Enum<?>>)cm.getInnerClass()).getEnumConstants())
381         l.add(cm.toString(e));
382      return l;
383   }
384
385   private Object getExample(ClassMeta<?> sType, TypeCategory t, boolean exampleAdded) throws SerializeException {
386      boolean canAdd = isAllowNestedExamples() || ! exampleAdded;
387      if (canAdd && (getAddExamplesTo().contains(t) || getAddExamplesTo().contains(ANY))) {
388         Object example = sType.getExample(this, jpSession());
389         if (example != null) {
390            try {
391               return JsonParser.DEFAULT.parse(toJson(example), Object.class);
392            } catch (ParseException e) {
393               throw new SerializeException(e);
394            }
395         }
396      }
397      return null;
398   }
399
400   private String toJson(Object o) throws SerializeException {
401      if (jsSession == null)
402         jsSession = ctx.getJsonSerializer().getSession();
403      return jsSession.serializeToString(o);
404   }
405
406   private JsonParserSession jpSession() {
407      if (jpSession == null)
408         jpSession = ctx.getJsonParser().getSession();
409      return jpSession;
410   }
411
412   private Object getDescription(ClassMeta<?> sType, TypeCategory t, boolean descriptionAdded) {
413      boolean canAdd = isAllowNestedDescriptions() || ! descriptionAdded;
414      if (canAdd && (getAddDescriptionsTo().contains(t) || getAddDescriptionsTo().contains(ANY)))
415         return sType.toString();
416      return null;
417   }
418
419   /**
420    * Returns the definition ID for the specified class.
421    *
422    * @param cm The class to get the definition ID of.
423    * @return The definition ID for the specified class.
424    */
425   public String getBeanDefId(ClassMeta<?> cm) {
426      return getBeanDefMapper().getId(cm);
427   }
428
429   /**
430    * Returns the definition URI for the specified class.
431    *
432    * @param cm The class to get the definition URI of.
433    * @return The definition URI for the specified class.
434    */
435   public java.net.URI getBeanDefUri(ClassMeta<?> cm) {
436      return getBeanDefMapper().getURI(cm);
437   }
438
439   /**
440    * Returns the definition URI for the specified class.
441    *
442    * @param id The definition ID to get the definition URI of.
443    * @return The definition URI for the specified class.
444    */
445   public java.net.URI getBeanDefUri(String id) {
446      return getBeanDefMapper().getURI(id);
447   }
448
449   /**
450    * Returns the definitions that were gathered during this session.
451    *
452    * <p>
453    * This map is modifiable and affects the map in the session.
454    *
455    * @return
456    *    The definitions that were gathered during this session, or <jk>null</jk> if {@link JsonSchemaGenerator.Builder#useBeanDefs()} was not enabled.
457    */
458   public Map<String,JsonMap> getBeanDefs() {
459      return defs;
460   }
461
462   /**
463    * Adds a schema definition to this session.
464    *
465    * @param id The definition ID.
466    * @param def The definition schema.
467    * @return This object.
468    */
469   public JsonSchemaGeneratorSession addBeanDef(String id, JsonMap def) {
470      if (defs != null)
471         defs.put(id, def);
472      return this;
473   }
474
475   //-----------------------------------------------------------------------------------------------------------------
476   // Properties
477   //-----------------------------------------------------------------------------------------------------------------
478
479   /**
480    * Add descriptions to types.
481    *
482    * @see JsonSchemaGenerator.Builder#addDescriptionsTo(TypeCategory...)
483    * @return
484    *    Set of categories of types that descriptions should be automatically added to generated schemas.
485    */
486   protected final Set<TypeCategory> getAddDescriptionsTo() {
487      return ctx.getAddDescriptionsTo();
488   }
489
490   /**
491    * Add examples.
492    *
493    * @see JsonSchemaGenerator.Builder#addExamplesTo(TypeCategory...)
494    * @return
495    *    Set of categories of types that examples should be automatically added to generated schemas.
496    */
497   protected final Set<TypeCategory> getAddExamplesTo() {
498      return ctx.getAddExamplesTo();
499   }
500
501   /**
502    * Allow nested descriptions.
503    *
504    * @see JsonSchemaGenerator.Builder#allowNestedDescriptions()
505    * @return
506    *    <jk>true</jk> if nested descriptions are allowed in schema definitions.
507    */
508   protected final boolean isAllowNestedDescriptions() {
509      return ctx.isAllowNestedDescriptions();
510   }
511
512   /**
513    * Allow nested examples.
514    *
515    * @see JsonSchemaGenerator.Builder#allowNestedExamples()
516    * @return
517    *    <jk>true</jk> if nested examples are allowed in schema definitions.
518    */
519   protected final boolean isAllowNestedExamples() {
520      return ctx.isAllowNestedExamples();
521   }
522
523   /**
524    * Bean schema definition mapper.
525    *
526    * @see JsonSchemaGenerator.Builder#beanDefMapper(Class)
527    * @return
528    *    Interface to use for converting Bean classes to definition IDs and URIs.
529    */
530   protected final BeanDefMapper getBeanDefMapper() {
531      return ctx.getBeanDefMapper();
532   }
533
534   /**
535    * Ignore types from schema definitions.
536    *
537    * @see JsonSchemaGenerator.Builder#ignoreTypes(String...)
538    * @return
539    *    Custom schema information for particular class types.
540    */
541   protected final List<Pattern> getIgnoreTypes() {
542      return ctx.getIgnoreTypes();
543   }
544
545   /**
546    * Use bean definitions.
547    *
548    * @see JsonSchemaGenerator.Builder#useBeanDefs()
549    * @return
550    *    <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags.
551    */
552   protected final boolean isUseBeanDefs() {
553      return ctx.isUseBeanDefs();
554   }
555
556   //-----------------------------------------------------------------------------------------------------------------
557   // Extended metadata
558   //-----------------------------------------------------------------------------------------------------------------
559
560   /**
561    * Returns the language-specific metadata on the specified class.
562    *
563    * @param cm The class to return the metadata on.
564    * @return The metadata.
565    */
566   public JsonSchemaClassMeta getJsonSchemaClassMeta(ClassMeta<?> cm) {
567      return ctx.getJsonSchemaClassMeta(cm);
568   }
569
570   /**
571    * Returns the language-specific metadata on the specified bean property.
572    *
573    * @param bpm The bean property to return the metadata on.
574    * @return The metadata.
575    */
576   public JsonSchemaBeanPropertyMeta getJsonSchemaBeanPropertyMeta(BeanPropertyMeta bpm) {
577      return ctx.getJsonSchemaBeanPropertyMeta(bpm);
578   }
579
580   //-----------------------------------------------------------------------------------------------------------------
581   // Utility methods
582   //-----------------------------------------------------------------------------------------------------------------
583
584   private ClassMeta<?> toClassMeta(Object o) {
585      if (o instanceof Type)
586         return getClassMeta((Type)o);
587      return getClassMetaForObject(o);
588   }
589}