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.StringUtils.*;
016
017import java.util.*;
018import java.util.regex.*;
019
020import org.apache.juneau.*;
021import org.apache.juneau.annotation.*;
022import org.apache.juneau.json.*;
023
024/**
025 * Generates JSON-schema metadata about POJOs.
026 */
027@ConfigurableContext
028public class JsonSchemaGenerator extends BeanTraverseContext {
029
030   //-------------------------------------------------------------------------------------------------------------------
031   // Configurable properties
032   //-------------------------------------------------------------------------------------------------------------------
033
034   static final String PREFIX = "JsonSchemaGenerator";
035
036   /**
037    * Configuration property:  Add descriptions to types.
038    *
039    * <h5 class='section'>Property:</h5>
040    * <ul>
041    *    <li><b>Name:</b>  <js>"JsonSchemaGenerator.addDescriptionsTo.s"</js>
042    *    <li><b>Data type:</b>  <c>String</c>
043    *    <li><b>Default:</b>  Empty string.
044    *    <li><b>Session property:</b>  <jk>false</jk>
045    *    <li><b>Methods:</b>
046    *       <ul>
047    *          <li class='jm'>{@link JsonSchemaGeneratorBuilder#addDescriptionsTo(String)}
048    *       </ul>
049    * </ul>
050    *
051    * <h5 class='section'>Description:</h5>
052    * <p>
053    * Identifies which categories of types that descriptions should be automatically added to generated schemas.
054    * <p>
055    * The description is the result of calling {@link ClassMeta#getFullName()}.
056    * <p>
057    * The format is a comma-delimited list of any of the following values:
058    *
059    * <ul class='javatree'>
060    *    <li class='jf'>{@link TypeCategory#BEAN BEAN}
061    *    <li class='jf'>{@link TypeCategory#COLLECTION COLLECTION}
062    *    <li class='jf'>{@link TypeCategory#ARRAY ARRAY}
063    *    <li class='jf'>{@link TypeCategory#MAP MAP}
064    *    <li class='jf'>{@link TypeCategory#STRING STRING}
065    *    <li class='jf'>{@link TypeCategory#NUMBER NUMBER}
066    *    <li class='jf'>{@link TypeCategory#BOOLEAN BOOLEAN}
067    *    <li class='jf'>{@link TypeCategory#ANY ANY}
068    *    <li class='jf'>{@link TypeCategory#OTHER OTHER}
069    * </ul>
070    */
071   public static final String JSONSCHEMA_addDescriptionsTo = PREFIX + ".addDescriptionsTo.s";
072
073   /**
074    * Configuration property:  Add examples.
075    *
076    * <h5 class='section'>Property:</h5>
077    * <ul>
078    *    <li><b>Name:</b>  <js>"JsonSchemaGenerator.addExamplesTo.s"</js>
079    *    <li><b>Data type:</b>  <c>String</c>
080    *    <li><b>Default:</b>  Empty string.
081    *    <li><b>Session property:</b>  <jk>false</jk>
082    *    <li><b>Methods:</b>
083    *       <ul>
084    *          <li class='jm'>{@link JsonSchemaGeneratorBuilder#addExamplesTo(String)}
085    *       </ul>
086    * </ul>
087    *
088    * <h5 class='section'>Description:</h5>
089    * <p>
090    * Identifies which categories of types that examples should be automatically added to generated schemas.
091    * <p>
092    * The examples come from calling {@link ClassMeta#getExample(BeanSession)} which in turn gets examples
093    * from the following:
094    * <ul class='javatree'>
095    *    <li class='ja'>{@link Example}
096    *    <li class='jf'>{@link BeanContext#BEAN_examples}
097    * </ul>
098    *
099    * <p>
100    * The format is a comma-delimited list of any of the following values:
101    *
102    * <ul class='javatree'>
103    *    <li class='jf'>{@link TypeCategory#BEAN BEAN}
104    *    <li class='jf'>{@link TypeCategory#COLLECTION COLLECTION}
105    *    <li class='jf'>{@link TypeCategory#ARRAY ARRAY}
106    *    <li class='jf'>{@link TypeCategory#MAP MAP}
107    *    <li class='jf'>{@link TypeCategory#STRING STRING}
108    *    <li class='jf'>{@link TypeCategory#NUMBER NUMBER}
109    *    <li class='jf'>{@link TypeCategory#BOOLEAN BOOLEAN}
110    *    <li class='jf'>{@link TypeCategory#ANY ANY}
111    *    <li class='jf'>{@link TypeCategory#OTHER OTHER}
112    * </ul>
113    */
114   public static final String JSONSCHEMA_addExamplesTo = PREFIX + ".addExamplesTo.s";
115
116   /**
117    * Configuration property:  Allow nested descriptions.
118    *
119    * <h5 class='section'>Property:</h5>
120    * <ul>
121    *    <li><b>Name:</b>  <js>"JsonSchemaGenerator.allowNestedDescriptions.b"</js>
122    *    <li><b>Data type:</b>  <c>Boolean</c>
123    *    <li><b>Default:</b>  <jk>false</jk>
124    *    <li><b>Session property:</b>  <jk>false</jk>
125    *    <li><b>Methods:</b>
126    *       <ul>
127    *          <li class='jm'>{@link JsonSchemaGeneratorBuilder#allowNestedDescriptions()}
128    *       </ul>
129    * </ul>
130    *
131    * <h5 class='section'>Description:</h5>
132    * <p>
133    * Identifies whether nested descriptions are allowed in schema definitions.
134    */
135   public static final String JSONSCHEMA_allowNestedDescriptions = PREFIX + ".allowNestedDescriptions.b";
136
137   /**
138    * Configuration property:  Allow nested examples.
139    *
140    * <h5 class='section'>Property:</h5>
141    * <ul>
142    *    <li><b>Name:</b>  <js>"JsonSchemaGenerator.allowNestedExamples.b"</js>
143    *    <li><b>Data type:</b>  <c>Boolean</c>
144    *    <li><b>Default:</b>  <jk>false</jk>
145    *    <li><b>Session property:</b>  <jk>false</jk>
146    *    <li><b>Methods:</b>
147    *       <ul>
148    *          <li class='jm'>{@link JsonSchemaGeneratorBuilder#allowNestedExamples()}
149    *       </ul>
150    * </ul>
151    *
152    * <h5 class='section'>Description:</h5>
153    * <p>
154    * Identifies whether nested examples are allowed in schema definitions.
155    */
156   public static final String JSONSCHEMA_allowNestedExamples = PREFIX + ".allowNestedExamples.b";
157
158   /**
159    * Configuration property:  Bean schema definition mapper.
160    *
161    * <h5 class='section'>Property:</h5>
162    * <ul>
163    *    <li><b>Name:</b>  <js>"JsonSchemaGenerator.beanDefMapper.o"</js>
164    *    <li><b>Data type:</b>  {@link BeanDefMapper}
165    *    <li><b>Default:</b>  {@link BasicBeanDefMapper}
166    *    <li><b>Session property:</b>  <jk>false</jk>
167    *    <li><b>Methods:</b>
168    *       <ul>
169    *          <li class='jm'>{@link JsonSchemaGeneratorBuilder#beanDefMapper(Class)}
170    *          <li class='jm'>{@link JsonSchemaGeneratorBuilder#beanDefMapper(BeanDefMapper)}
171    *       </ul>
172    * </ul>
173    *
174    * <h5 class='section'>Description:</h5>
175    * <p>
176    * Interface to use for converting Bean classes to definition IDs and URIs.
177    * <p>
178    * Used primarily for defining common definition sections for beans in Swagger JSON.
179    * <p>
180    * This setting is ignored if {@link #JSONSCHEMA_useBeanDefs} is not enabled.
181    */
182   public static final String JSONSCHEMA_beanDefMapper = PREFIX + ".beanDefMapper.o";
183
184   /**
185    * Configuration property:  Default schemas.
186    *
187    * <h5 class='section'>Property:</h5>
188    * <ul>
189    *    <li><b>Name:</b>  <js>"JsonSchemaGenerator.defaultSchema.smo"</js>
190    *    <li><b>Data type:</b>  <c>Map&lt;String,ObjectMap&gt;</c>
191    *    <li><b>Default:</b>  Empty map.
192    *    <li><b>Session property:</b>  <jk>false</jk>
193    *    <li><b>Methods:</b>
194    *       <ul>
195    *          <li class='jm'>{@link JsonSchemaGeneratorBuilder#defaultSchema(Class,ObjectMap)}
196    *       </ul>
197    * </ul>
198    *
199    * <h5 class='section'>Description:</h5>
200    * <p>
201    * Allows you to override or provide custom schema information for particular class types.
202    * <p>
203    * Keys are full class names.
204    */
205   public static final String JSONSCHEMA_defaultSchemas = PREFIX + ".defaultSchemas.smo";
206
207   /**
208    * Configuration property:  Ignore types from schema definitions.
209    *
210    * <h5 class='section'>Property:</h5>
211    * <ul>
212    *    <li><b>Name:</b>  <js>"JsonSchemaGenerator.ignoreTypes.s"</js>
213    *    <li><b>Data type:</b>  <c>String</c> (comma-delimited)
214    *    <li><b>Default:</b>  <jk>null</jk>.
215    *    <li><b>Session property:</b>  <jk>false</jk>
216    * </ul>
217    *
218    * <h5 class='section'>Description:</h5>
219    * <p>
220    * Defines class name patterns that should be ignored when generating schema definitions in the generated
221    * Swagger documentation.
222    *
223    * <h5 class='section'>Example:</h5>
224    * <p class='bcode w800'>
225    *    <jc>// Don't generate schema for any prototype packages or the class named 'Swagger'.
226    *    <ja>@RestResource</ja>(
227    *          properties={
228    *             <ja>@Property</ja>(name=<jsf>JSONSCHEMA_ignoreTypes</jsf>, value=<js>"Swagger,*.proto.*"</js>)
229    *          }
230    *    <jk>public class</jk> MyResource {...}
231    * </p>
232    */
233   public static final String JSONSCHEMA_ignoreTypes = PREFIX + ".ignoreTypes.s";
234
235   /**
236    * Configuration property:  Use bean definitions.
237    *
238    * <h5 class='section'>Property:</h5>
239    * <ul>
240    *    <li><b>Name:</b>  <js>"JsonSchemaGenerator.useBeanDefs.b"</js>
241    *    <li><b>Data type:</b>  <c>Boolean</c>
242    *    <li><b>Default:</b>  <jk>false</jk>
243    *    <li><b>Methods:</b>
244    *       <ul>
245    *          <li class='jm'>{@link JsonSchemaGeneratorBuilder#useBeanDefs()}
246    *       </ul>
247    * </ul>
248    *
249    * <h5 class='section'>Description:</h5>
250    * <p>
251    * When enabled, schemas on beans will be serialized as the following:
252    * <p class='bcode w800'>
253    *    {
254    *       type: <js>'object'</js>,
255    *       <js>'$ref'</js>: <js>'#/definitions/TypeId'</js>
256    *    }
257    * </p>
258    *
259    * <p>
260    * The definitions can then be retrieved from the session using {@link JsonSchemaGeneratorSession#getBeanDefs()}.
261    * <p>
262    * Definitions can also be added programmatically using {@link JsonSchemaGeneratorSession#addBeanDef(String, ObjectMap)}.
263    */
264   public static final String JSONSCHEMA_useBeanDefs = PREFIX + ".useBeanDefs.b";
265
266
267   //-------------------------------------------------------------------------------------------------------------------
268   // Predefined instances
269   //-------------------------------------------------------------------------------------------------------------------
270
271   /** Default serializer, all default settings.*/
272   public static final JsonSchemaGenerator DEFAULT = new JsonSchemaGenerator(PropertyStore.DEFAULT);
273
274
275   //-------------------------------------------------------------------------------------------------------------------
276   // Instance
277   //-------------------------------------------------------------------------------------------------------------------
278
279   private final boolean useBeanDefs, allowNestedExamples, allowNestedDescriptions;
280   private final BeanDefMapper beanDefMapper;
281   private final Set<TypeCategory> addExamplesTo, addDescriptionsTo;
282   private final Map<String,ObjectMap> defaultSchemas;
283   private final JsonSerializer jsonSerializer;
284   private final Set<Pattern> ignoreTypes;
285
286   /**
287    * Constructor.
288    *
289    * @param ps Initialize with the specified config property store.
290    */
291   public JsonSchemaGenerator(PropertyStore ps) {
292      super(ps.builder().set(BEANTRAVERSE_detectRecursions, true).set(BEANTRAVERSE_ignoreRecursions, true).build());
293
294      useBeanDefs = getBooleanProperty(JSONSCHEMA_useBeanDefs, false);
295      allowNestedExamples = getBooleanProperty(JSONSCHEMA_allowNestedExamples, false);
296      allowNestedDescriptions = getBooleanProperty(JSONSCHEMA_allowNestedDescriptions, false);
297      beanDefMapper = getInstanceProperty(JSONSCHEMA_beanDefMapper, BeanDefMapper.class, BasicBeanDefMapper.class);
298      addExamplesTo = TypeCategory.parse(getStringProperty(JSONSCHEMA_addExamplesTo, null));
299      addDescriptionsTo = TypeCategory.parse(getStringProperty(JSONSCHEMA_addDescriptionsTo, null));
300      defaultSchemas = getMapProperty(JSONSCHEMA_defaultSchemas, ObjectMap.class);
301
302      Set<Pattern> ignoreTypes = new LinkedHashSet<>();
303      for (String s : split(ps.getProperty(JSONSCHEMA_ignoreTypes, String.class, "")))
304         ignoreTypes.add(Pattern.compile(s.replace(".", "\\.").replace("*", ".*")));
305      this.ignoreTypes = ignoreTypes;
306
307      jsonSerializer = new JsonSerializer(ps);
308   }
309
310   @Override /* Context */
311   public JsonSchemaGeneratorBuilder builder() {
312      return new JsonSchemaGeneratorBuilder(getPropertyStore());
313   }
314
315   /**
316    * Instantiates a new clean-slate {@link JsonSerializerBuilder} object.
317    *
318    * <p>
319    * This is equivalent to simply calling <code><jk>new</jk> JsonSerializerBuilder()</code>.
320    *
321    * @return A new {@link JsonSerializerBuilder} object.
322    */
323   public static JsonSchemaGeneratorBuilder create() {
324      return new JsonSchemaGeneratorBuilder();
325   }
326
327   @Override /* Context */
328   public JsonSchemaGeneratorSession createSession() {
329      return createSession(createDefaultSessionArgs());
330   }
331
332   @Override
333   public JsonSchemaGeneratorSession createSession(BeanSessionArgs args) {
334      return new JsonSchemaGeneratorSession(this, args);
335   }
336
337   JsonSerializer getJsonSerializer() {
338      return jsonSerializer;
339   }
340
341   //-----------------------------------------------------------------------------------------------------------------
342   // Properties
343   //-----------------------------------------------------------------------------------------------------------------
344
345   /**
346    * Configuration property:  Add descriptions to types.
347    *
348    * @see #JSONSCHEMA_addDescriptionsTo
349    * @return
350    *    Set of categories of types that descriptions should be automatically added to generated schemas.
351    */
352   protected final Set<TypeCategory> getAddDescriptionsTo() {
353      return addDescriptionsTo;
354   }
355
356   /**
357    * Configuration property:  Add examples.
358    *
359    * @see #JSONSCHEMA_addExamplesTo
360    * @return
361    *    Set of categories of types that examples should be automatically added to generated schemas.
362    */
363   protected final Set<TypeCategory> getAddExamplesTo() {
364      return addExamplesTo;
365   }
366
367   /**
368    * Configuration property:  Allow nested descriptions.
369    *
370    * @see #JSONSCHEMA_allowNestedDescriptions
371    * @return
372    *    <jk>true</jk> if nested descriptions are allowed in schema definitions.
373    */
374   protected final boolean isAllowNestedDescriptions() {
375      return allowNestedDescriptions;
376   }
377
378   /**
379    * Configuration property:  Allow nested examples.
380    *
381    * @see #JSONSCHEMA_allowNestedExamples
382    * @return
383    *    <jk>true</jk> if nested examples are allowed in schema definitions.
384    */
385   protected final boolean isAllowNestedExamples() {
386      return allowNestedExamples;
387   }
388
389   /**
390    * Configuration property:  Bean schema definition mapper.
391    *
392    * @see #JSONSCHEMA_beanDefMapper
393    * @return
394    *    Interface to use for converting Bean classes to definition IDs and URIs.
395    */
396   protected final BeanDefMapper getBeanDefMapper() {
397      return beanDefMapper;
398   }
399
400   /**
401    * Configuration property:  Default schemas.
402    *
403    * @see #JSONSCHEMA_defaultSchemas
404    * @return
405    *    Custom schema information for particular class types.
406    */
407   protected final Map<String,ObjectMap> getDefaultSchemas() {
408      return defaultSchemas;
409   }
410
411   /**
412    * Configuration property:  Ignore types from schema definitions.
413    *
414    * @see JsonSchemaGenerator#JSONSCHEMA_ignoreTypes
415    * @return
416    *    Custom schema information for particular class types.
417    */
418   public Set<Pattern> getIgnoreTypes() {
419      return ignoreTypes;
420   }
421
422   /**
423    * Configuration property:  Use bean definitions.
424    *
425    * @see #JSONSCHEMA_useBeanDefs
426    * @return
427    *    <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags.
428    */
429   protected final boolean isUseBeanDefs() {
430      return useBeanDefs;
431   }
432
433   //-----------------------------------------------------------------------------------------------------------------
434   // Other methods
435   //-----------------------------------------------------------------------------------------------------------------
436
437   /**
438    * Returns <jk>true</jk> if the specified type is ignored.
439    *
440    * <p>
441    * The type is ignored if it's specified in the {@link #JSONSCHEMA_ignoreTypes} setting.
442    * <br>Ignored types return <jk>null</jk> on the call to {@link JsonSchemaGeneratorSession#getSchema(ClassMeta)}.
443    *
444    * @param cm The type to check.
445    * @return <jk>true</jk> if the specified type is ignored.
446    */
447   public boolean isIgnoredType(ClassMeta<?> cm) {
448      for (Pattern p : ignoreTypes)
449         if (p.matcher(cm.getSimpleName()).matches() || p.matcher(cm.getName()).matches())
450            return true;
451      return false;
452   }
453
454   //-----------------------------------------------------------------------------------------------------------------
455   // Other methods
456   //-----------------------------------------------------------------------------------------------------------------
457
458   @Override /* Context */
459   public ObjectMap toMap() {
460      return super.toMap()
461         .append("JsonSchemaGenerator", new DefaultFilteringObjectMap()
462            .append("useBeanDefs", useBeanDefs)
463            .append("allowNestedExamples", allowNestedExamples)
464            .append("allowNestedDescriptions", allowNestedDescriptions)
465            .append("beanDefMapper", beanDefMapper)
466            .append("addExamplesTo", addExamplesTo)
467            .append("addDescriptionsTo", addDescriptionsTo)
468            .append("defaultSchemas", defaultSchemas)
469            .append("ignoreTypes", ignoreTypes)
470         );
471   }
472}