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