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