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.marshall.jsonschema;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022import static org.apache.juneau.commons.utils.SystemUtils.*;
023import static org.apache.juneau.commons.utils.ThrowableUtils.*;
024
025import java.util.*;
026import java.util.concurrent.*;
027import java.util.regex.*;
028
029import org.apache.juneau.commons.bean.*;
030import org.apache.juneau.commons.collections.*;
031import org.apache.juneau.marshall.*;
032import org.apache.juneau.marshall.collections.*;
033import org.apache.juneau.marshall.json.*;
034import org.apache.juneau.marshall.json5.*;
035
036/**
037 * Generates JSON-schema metadata about POJOs.
038 *
039 * <h5 class='section'>Notes:</h5><ul>
040 *    <li class='note'>This class is thread safe and reusable.
041 * </ul>
042 *
043 * <p>
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 */
048@SuppressWarnings({
049   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
050   "java:S1452" // Builder<?> wildcard return intentional; callers chain via fluent API without needing the concrete type
051})
052public class JsonSchemaGenerator extends MarshallingTraverseContext implements JsonSchemaMetaProvider {
053
054   /**
055    * Builder class.
056    */
057   public static class Builder extends MarshallingTraverseContext.Builder<Builder> {
058
059      private static final Cache<HashKey,JsonSchemaGenerator> CACHE = Cache.of(HashKey.class, JsonSchemaGenerator.class).build();
060
061      protected final JsonParser.Builder<?> jsonParserBuilder;
062      protected final JsonSerializer.Builder<?> jsonSerializerBuilder;
063
064      private boolean allowNestedDescriptions;
065      private boolean allowNestedExamples;
066      private boolean useBeanDefs;
067      private Class<? extends MarshallingDefMapper> beanDefMapper;
068      private SortedSet<TypeCategory> addDescriptionsTo;
069      private SortedSet<TypeCategory> addExamplesTo;
070      private SortedSet<String> ignoreTypes;
071
072      /**
073       * Constructor, default settings.
074       */
075      protected Builder() {
076         MarshallingContext.Builder bc = marshallingContext();
077         jsonSerializerBuilder = JsonSerializer.create().marshallingContext(bc);
078         jsonParserBuilder = Json5Parser.create().marshallingContext(bc);
079         registerBuilders(jsonSerializerBuilder, jsonParserBuilder);
080         addDescriptionsTo = null;
081         addExamplesTo = null;
082         allowNestedDescriptions = env("JsonSchemaGenerator.allowNestedDescriptions", false);
083         allowNestedExamples = env("JsonSchemaGenerator.allowNestedExamples", false);
084         useBeanDefs = env("JsonSchemaGenerator.useBeanDefs", false);
085         beanDefMapper = BasicBeanDefMapper.class;
086         ignoreTypes = null;
087      }
088
089      /**
090       * Copy constructor.
091       *
092       * @param copyFrom The builder to copy from.
093       *    <br>Cannot be <jk>null</jk>.
094       */
095      protected Builder(Builder copyFrom) {
096         super(reqnn("copyFrom", copyFrom));
097         MarshallingContext.Builder bc = marshallingContext();
098         jsonSerializerBuilder = copyFrom.jsonSerializerBuilder.copy().marshallingContext(bc);
099         jsonParserBuilder = copyFrom.jsonParserBuilder.copy().marshallingContext(bc);
100         registerBuilders(jsonSerializerBuilder, jsonParserBuilder);
101         addDescriptionsTo = copyFrom.addDescriptionsTo == null ? null : new TreeSet<>(copyFrom.addDescriptionsTo);
102         addExamplesTo = copyFrom.addExamplesTo == null ? null : new TreeSet<>(copyFrom.addExamplesTo);
103         allowNestedDescriptions = copyFrom.allowNestedDescriptions;
104         allowNestedExamples = copyFrom.allowNestedExamples;
105         beanDefMapper = copyFrom.beanDefMapper;
106         ignoreTypes = copyFrom.ignoreTypes == null ? null : new TreeSet<>(copyFrom.ignoreTypes);
107         useBeanDefs = copyFrom.useBeanDefs;
108      }
109
110      /**
111       * Copy constructor.
112       *
113       * @param copyFrom The bean to copy from.
114       *    <br>Cannot be <jk>null</jk>.
115       */
116      protected Builder(JsonSchemaGenerator copyFrom) {
117         super(reqnn("copyFrom", copyFrom));
118         MarshallingContext.Builder bc = marshallingContext();
119         jsonSerializerBuilder = copyFrom.jsonSerializer.copy().marshallingContext(bc);
120         jsonParserBuilder = copyFrom.jsonParser.copy().marshallingContext(bc);
121         registerBuilders(jsonSerializerBuilder, jsonParserBuilder);
122         addDescriptionsTo = copyFrom.addDescriptionsTo.isEmpty() ? null : new TreeSet<>(copyFrom.addDescriptionsTo);
123         addExamplesTo = copyFrom.addExamplesTo.isEmpty() ? null : new TreeSet<>(copyFrom.addExamplesTo);
124         allowNestedDescriptions = copyFrom.allowNestedDescriptions;
125         allowNestedExamples = copyFrom.allowNestedExamples;
126         beanDefMapper = copyFrom.beanDefMapper;
127         ignoreTypes = copyFrom.ignoreTypes.isEmpty() ? null : new TreeSet<>(copyFrom.ignoreTypes);
128         useBeanDefs = copyFrom.useBeanDefs;
129      }
130
131      /**
132       * Add descriptions.
133       *
134       * <p>
135       * Identifies which categories of types that descriptions should be automatically added to generated schemas.
136       * The description is the result of calling {@link ClassMeta#getName()}.
137       * The format is a comma-delimited list of any of the following values:
138       *
139       * <ul class='javatree'>
140       *    <li class='jf'>{@link TypeCategory#BEAN BEAN}
141       *    <li class='jf'>{@link TypeCategory#COLLECTION COLLECTION}
142       *    <li class='jf'>{@link TypeCategory#ARRAY ARRAY}
143       *    <li class='jf'>{@link TypeCategory#MAP MAP}
144       *    <li class='jf'>{@link TypeCategory#STRING STRING}
145       *    <li class='jf'>{@link TypeCategory#NUMBER NUMBER}
146       *    <li class='jf'>{@link TypeCategory#BOOLEAN BOOLEAN}
147       *    <li class='jf'>{@link TypeCategory#ANY ANY}
148       *    <li class='jf'>{@link TypeCategory#OTHER OTHER}
149       * </ul>
150       *
151       * @param values
152       *    The values to add to this setting.
153       *    <br>The default is an empty string.
154       *    <br>Cannot contain <jk>null</jk> values.
155       * @return This object.
156       */
157      public Builder addDescriptionsTo(TypeCategory...values) {
158         reqnns("values", values);
159         addDescriptionsTo = addAll(addDescriptionsTo, values);
160         return this;
161      }
162
163      /**
164       * Add examples.
165       *
166       * <p>
167       * Identifies which categories of types that examples should be automatically added to generated schemas.
168       * <p>
169       * The examples come from calling {@link ClassMeta#getExample(MarshallingSession,JsonParserSession)} which in turn gets examples
170       * from the following:
171       * <ul class='javatree'>
172       *    <li class='ja'>{@link Example}
173       *    <li class='ja'>{@link Marshalled#example() Marshalled(example)}
174       * </ul>
175       *
176       * <p>
177       * The format is a comma-delimited list of any of the following values:
178       *
179       * <ul class='javatree'>
180       *    <li class='jf'>{@link TypeCategory#BEAN BEAN}
181       *    <li class='jf'>{@link TypeCategory#COLLECTION COLLECTION}
182       *    <li class='jf'>{@link TypeCategory#ARRAY ARRAY}
183       *    <li class='jf'>{@link TypeCategory#MAP MAP}
184       *    <li class='jf'>{@link TypeCategory#STRING STRING}
185       *    <li class='jf'>{@link TypeCategory#NUMBER NUMBER}
186       *    <li class='jf'>{@link TypeCategory#BOOLEAN BOOLEAN}
187       *    <li class='jf'>{@link TypeCategory#ANY ANY}
188       *    <li class='jf'>{@link TypeCategory#OTHER OTHER}
189       * </ul>
190       *
191       * @param values
192       *    The values to add to this setting.
193       *    <br>The default is an empty string.
194       *    <br>Cannot contain <jk>null</jk> values.
195       * @return This object.
196       */
197      public Builder addExamplesTo(TypeCategory...values) {
198         reqnns("values", values);
199         addExamplesTo = addAll(addExamplesTo, values);
200         return this;
201      }
202
203      /**
204       * Allow nested descriptions.
205       *
206       * <p>
207       * Identifies whether nested descriptions are allowed in schema definitions.
208       *
209       * @return This object.
210       */
211      public Builder allowNestedDescriptions() {
212         return allowNestedDescriptions(true);
213      }
214
215      /**
216       * Same as {@link #allowNestedDescriptions()} but allows you to explicitly specify the value.
217       *
218       * @param value The value for this setting.
219       * @return This object.
220       */
221      public Builder allowNestedDescriptions(boolean value) {
222         allowNestedDescriptions = value;
223         return this;
224      }
225
226      /**
227       * Allow nested examples.
228       *
229       * <p>
230       * Identifies whether nested examples are allowed in schema definitions.
231       *
232       * @return This object.
233       */
234      public Builder allowNestedExamples() {
235         return allowNestedExamples(true);
236      }
237
238      /**
239       * Same as {@link #allowNestedExamples()} but allows you to explicitly specify the value.
240       *
241       * @param value The value for this setting.
242       * @return This object.
243       */
244      public Builder allowNestedExamples(boolean value) {
245         allowNestedExamples = value;
246         return this;
247      }
248
249      /**
250       * Schema definition mapper.
251       *
252       * <p>
253       * Interface to use for converting Bean classes to definition IDs and URIs.
254       * <p>
255       * Used primarily for defining common definition sections for beans in Swagger JSON.
256       * <p>
257       * This setting is ignored if {@link JsonSchemaGenerator.Builder#useBeanDefs()} is not enabled.
258       *
259       * @param value
260       *    The new value for this setting.
261       *    <br>The default is {@link BasicBeanDefMapper}.
262       *    <br>Cannot be <jk>null</jk>.
263       * @return This object.
264       */
265      public Builder beanDefMapper(Class<? extends MarshallingDefMapper> value) {
266         beanDefMapper = reqnn("value", value);
267         return this;
268      }
269
270      @Override /* Overridden from Context.Builder<?> */
271      public JsonSchemaGenerator build() {
272         return cache(CACHE).build(JsonSchemaGenerator.class);
273      }
274
275      @Override /* Overridden from Context.Builder<?> */
276      public Builder copy() {
277         return new Builder(this);
278      }
279
280      /**
281       * Gives access to the inner JSON parser builder if you want to modify the parser settings.
282       *
283       * @return The JSON serializer builder.
284       */
285      public JsonParser.Builder<?> getJsonParserBuilder() { return jsonParserBuilder; }
286
287      /**
288       * Gives access to the inner JSON serializer builder if you want to modify the serializer settings.
289       *
290       * @return The JSON serializer builder.
291       */
292      public JsonSerializer.Builder<?> getJsonSerializerBuilder() { return jsonSerializerBuilder; }
293
294      @Override /* Overridden from Context.Builder<?> */
295      public HashKey hashKey() {
296         // @formatter:off
297         return HashKey.of(
298            super.hashKey(),
299            jsonSerializerBuilder.hashKey(),
300            jsonParserBuilder.hashKey(),
301            addDescriptionsTo,
302            addExamplesTo,
303            allowNestedDescriptions,
304            allowNestedExamples,
305            useBeanDefs,
306            beanDefMapper,
307            ignoreTypes
308         );
309         // @formatter:on
310      }
311
312      /**
313       * Ignore types from schema definitions.
314       *
315       * <h5 class='section'>Description:</h5>
316       * <p>
317       * Defines class name patterns that should be ignored when generating schema definitions in the generated
318       * Swagger documentation.
319       *
320       * <h5 class='section'>Example:</h5>
321       * <p class='bjava'>
322       *    <jc>// Don't generate schema for any prototype packages or the class named 'Swagger'.</jc>
323       *    <ja>@JsonSchemaConfig</ja>(
324       *       ignoreTypes=<js>"Swagger,*.proto.*"</js>
325       *    )
326       *    <jk>public class</jk> MyResource {...}
327       * </p>
328       *
329       * @param values
330       *    The values to add.
331       *    <br>Cannot contain <jk>null</jk> values.
332       * @return This object.
333       */
334      public Builder ignoreTypes(String...values) {
335         reqnns("values", values);
336         ignoreTypes = addAll(ignoreTypes, values);
337         return this;
338      }
339
340      /**
341       * Use bean definitions.
342       *
343       * <p>
344       * When enabled, schemas on beans will be serialized as the following:
345       * <p class='bjson'>
346       *    {
347       *       type: <js>'object'</js>,
348       *       <js>'$ref'</js>: <js>'#/definitions/TypeId'</js>
349       *    }
350       * </p>
351       *
352       * <p>
353       * The definitions can then be retrieved from the session using {@link JsonSchemaGeneratorSession#getBeanDefs()}.
354       * <p>
355       * Definitions can also be added programmatically using {@link JsonSchemaGeneratorSession#addBeanDef(String, JsonMap)}.
356       *
357       * @return This object.
358       */
359      public Builder useBeanDefs() {
360         return useBeanDefs(true);
361      }
362
363      /**
364       * Same as {@link #useBeanDefs()} but allows you to explicitly specify the value.
365       *
366       * @param value The value for this setting.
367       * @return This object.
368       */
369      public Builder useBeanDefs(boolean value) {
370         useBeanDefs = value;
371         return this;
372      }
373
374
375   }
376
377   /** Default serializer, all default settings.*/
378   public static final JsonSchemaGenerator DEFAULT = new JsonSchemaGenerator(create());
379
380   /**
381    * Creates a new builder for this object.
382    *
383    * @return A new builder.
384    */
385   public static Builder create() {
386      return new Builder();
387   }
388
389   protected final boolean allowNestedDescriptions;
390   protected final boolean allowNestedExamples;
391   protected final boolean useBeanDefs;
392   protected final Class<? extends MarshallingDefMapper> beanDefMapper;
393   protected final JsonParser jsonParser;
394   protected final JsonSerializer jsonSerializer;
395   protected final Set<TypeCategory> addDescriptionsTo;
396   protected final Set<TypeCategory> addExamplesTo;
397   protected final Set<String> ignoreTypes;
398   private final MarshallingDefMapper beanDefMapperBean;
399   private final Map<BeanPropertyMeta,JsonSchemaBeanPropertyMeta> jsonSchemaBeanPropertyMetas = new ConcurrentHashMap<>();
400   private final Map<ClassMeta<?>,JsonSchemaClassMeta> jsonSchemaClassMetas = new ConcurrentHashMap<>();
401   private final List<Pattern> ignoreTypePatterns;
402
403   /**
404    * Constructor.
405    *
406    * @param builder The builder for this object.  Cannot be <jk>null</jk>.
407    */
408   public JsonSchemaGenerator(Builder builder) {
409      super(builder.detectRecursions().ignoreRecursions());
410
411      addDescriptionsTo = builder.addDescriptionsTo == null ? Collections.emptySet() : new TreeSet<>(builder.addDescriptionsTo);
412      addExamplesTo = builder.addExamplesTo == null ? Collections.emptySet() : new TreeSet<>(builder.addExamplesTo);
413      allowNestedDescriptions = builder.allowNestedDescriptions;
414      allowNestedExamples = builder.allowNestedExamples;
415      beanDefMapper = builder.beanDefMapper;
416      ignoreTypes = builder.ignoreTypes == null ? Collections.emptySet() : new TreeSet<>(builder.ignoreTypes);
417      useBeanDefs = builder.useBeanDefs;
418
419      Set<Pattern> ignoreTypePatterns2 = set();
420      ignoreTypes.forEach(y -> split(y, x -> ignoreTypePatterns2.add(Pattern.compile(x.replace(".", "\\.").replace("*", ".*")))));
421      this.ignoreTypePatterns = u(new ArrayList<>(ignoreTypePatterns2));
422
423      try {
424         beanDefMapperBean = beanDefMapper.getDeclaredConstructor().newInstance();
425      } catch (Exception e) {
426         throw toRex(e);
427      }
428
429      jsonSerializer = builder.jsonSerializerBuilder.build();
430      jsonParser = builder.jsonParserBuilder.marshallingContext(getMarshallingContext()).build();
431   }
432
433   @Override /* Overridden from Context */
434   public Builder copy() {
435      return new Builder(this);
436   }
437
438   @Override /* Overridden from Context */
439   public JsonSchemaGeneratorSession.Builder createSession() {
440      return JsonSchemaGeneratorSession.create(this);
441   }
442
443   /**
444    * Ignore types from schema definitions.
445    *
446    * @see Builder#ignoreTypes(String...)
447    * @return
448    *    Custom schema information for particular class types.
449    *    <br>Never <jk>null</jk>.
450    *    <br>List is unmodifiable.
451    */
452   public List<Pattern> getIgnoreTypes() { return ignoreTypePatterns; }
453
454   @Override
455   public JsonSchemaBeanPropertyMeta getJsonSchemaBeanPropertyMeta(BeanPropertyMeta bpm) {
456      return jsonSchemaBeanPropertyMetas.computeIfAbsent(bpm, k -> new JsonSchemaBeanPropertyMeta(k, this));
457   }
458
459   @Override
460   public JsonSchemaClassMeta getJsonSchemaClassMeta(ClassMeta<?> cm) {
461      return jsonSchemaClassMetas.computeIfAbsent(cm, k -> new JsonSchemaClassMeta(k, this));
462   }
463
464   @Override /* Overridden from Context */
465   public JsonSchemaGeneratorSession getSession() { return createSession().build(); }
466
467   /**
468    * Returns <jk>true</jk> if the specified type is ignored.
469    *
470    * <p>
471    * The type is ignored if it's specified in the {@link Builder#ignoreTypes(String...)} setting.
472    * <br>Ignored types return <jk>null</jk> on the call to {@link JsonSchemaGeneratorSession#getSchema(ClassMeta)}.
473    *
474    * @param cm The type to check.  Must not be <jk>null</jk>.
475    * @return <jk>true</jk> if the specified type is ignored.
476    */
477   public boolean isIgnoredType(ClassMeta<?> cm) {
478      for (var p : ignoreTypePatterns)
479         if (p.matcher(cm.getNameSimple()).matches() || p.matcher(cm.getName()).matches())
480            return true;
481      return false;
482   }
483
484   /**
485    * Add descriptions to types.
486    *
487    * @see Builder#addDescriptionsTo(TypeCategory...)
488    * @return
489    *    Set of categories of types that descriptions should be automatically added to generated schemas.
490    */
491   protected final Set<TypeCategory> getAddDescriptionsTo() { return addDescriptionsTo; }
492
493   /**
494    * Add examples.
495    *
496    * @see Builder#addExamplesTo(TypeCategory...)
497    * @return
498    *    Set of categories of types that examples should be automatically added to generated schemas.
499    */
500   protected final Set<TypeCategory> getAddExamplesTo() { return addExamplesTo; }
501
502   /**
503    * Bean schema definition mapper.
504    *
505    * @see Builder#beanDefMapper(Class)
506    * @return
507    *    Interface to use for converting Bean classes to definition IDs and URIs.
508    */
509   protected final MarshallingDefMapper getBeanDefMapper() { return beanDefMapperBean; }
510
511   /**
512    * Allow nested descriptions.
513    *
514    * @see Builder#allowNestedDescriptions()
515    * @return
516    *    <jk>true</jk> if nested descriptions are allowed in schema definitions.
517    */
518   protected final boolean isAllowNestedDescriptions() { return allowNestedDescriptions; }
519
520   /**
521    * Allow nested examples.
522    *
523    * @see Builder#allowNestedExamples()
524    * @return
525    *    <jk>true</jk> if nested examples are allowed in schema definitions.
526    */
527   protected final boolean isAllowNestedExamples() { return allowNestedExamples; }
528
529   /**
530    * Use bean definitions.
531    *
532    * @see Builder#useBeanDefs()
533    * @return
534    *    <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags.
535    */
536   protected final boolean isUseBeanDefs() { return useBeanDefs; }
537
538   @Override /* Overridden from MarshallingTraverseContext */
539   protected FluentMap<String,Object> properties() {
540      return super.properties()
541         .a("addDescriptionsTo", addDescriptionsTo)
542         .a("addExamplesTo", addExamplesTo)
543         .a("allowNestedDescriptions", allowNestedDescriptions)
544         .a("allowNestedExamples", allowNestedExamples)
545         .a("beanDefMapper", beanDefMapper)
546         .a("ignoreTypes", ignoreTypes)
547         .a("useBeanDefs", useBeanDefs);
548   }
549
550   JsonParser getJsonParser() { return jsonParser; }
551
552   JsonSerializer getJsonSerializer() { return jsonSerializer; }
553}