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.json;
018
019import java.io.*;
020import java.util.*;
021import java.util.concurrent.*;
022
023import org.apache.juneau.commons.bean.*;
024import org.apache.juneau.commons.collections.*;
025import org.apache.juneau.marshall.*;
026import org.apache.juneau.marshall.jsonschema.*;
027import org.apache.juneau.marshall.stream.*;
028
029/**
030 * Serializes POJO metadata to HTTP responses as JSON-Schema.
031 *
032 * <h5 class='topic'>Media types</h5>
033 *
034 * Handles <c>Accept</c> types:  <bc>application/json+schema, text/json+schema</bc>
035 * <p>
036 * Produces <c>Content-Type</c> types:  <bc>application/json</bc>
037 *
038 * <h5 class='topic'>Description</h5>
039 *
040 * Produces the JSON-schema for the JSON produced by the {@link JsonSerializer} class with the same properties.
041 *
042 * <h5 class='section'>Notes:</h5><ul>
043 *    <li class='note'>This class is thread safe and reusable.
044 * </ul>
045 *
046 * <h5 class='section'>See Also:</h5><ul>
047 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JsonSupport">JSON</a>
048
049 * </ul>
050 */
051@SuppressWarnings({
052   "java:S110", // Inheritance depth acceptable for JsonSchemaSerializer hierarchy
053   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
054   "java:S9149", // Per-format static factories intentionally shadow the parent's.
055   "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design.
056})
057public class JsonSchemaSerializer extends JsonSerializer implements JsonSchemaMetaProvider {
058
059   /**
060    * Builder class.
061    */
062   public static class Builder extends JsonSerializer.Builder<Builder> {
063
064      private static final Cache<HashKey,JsonSchemaSerializer> CACHE = Cache.of(HashKey.class, JsonSchemaSerializer.class).build();
065
066      JsonSchemaGenerator.Builder generatorBuilder;
067
068      /**
069       * Constructor, default settings.
070       */
071      protected Builder() {
072         produces("application/json");
073         accept("application/json+schema,text/json+schema");
074         generatorBuilder = JsonSchemaGenerator.create().marshallingContext(marshallingContext());
075      }
076
077      /**
078       * Copy constructor.
079       *
080       * @param copyFrom The builder to copy from.
081       *    <br>Cannot be <jk>null</jk>.
082       */
083      protected Builder(Builder copyFrom) {
084         super(copyFrom);
085         generatorBuilder = copyFrom.generatorBuilder.copy().marshallingContext(marshallingContext());
086      }
087
088      /**
089       * Copy constructor.
090       *
091       * @param copyFrom The bean to copy from.
092       *    <br>Cannot be <jk>null</jk>.
093       */
094      protected Builder(JsonSchemaSerializer copyFrom) {
095         super(copyFrom);
096         generatorBuilder = copyFrom.generator.copy().marshallingContext(marshallingContext());
097      }
098
099      /**
100       * <i><l>JsonSchemaSerializer</l> configuration property:&emsp;</i>  Add descriptions.
101       *
102       * <p>
103       * Identifies which categories of types that descriptions should be automatically added to generated schemas.
104       * <p>
105       * The description is the result of calling {@link ClassMeta#getName()}.
106       *
107       * <h5 class='section'>See Also:</h5><ul>
108       *    <li class='jm'>{@link org.apache.juneau.marshall.jsonschema.JsonSchemaGenerator.Builder#addDescriptionsTo(TypeCategory...) JsonSchemaGenerator.Builder#addDescriptionsTo(TypeCategory...)}
109       * </ul>
110       *
111       * @param values
112       *    The values to add to this setting.
113       *    <br>The default is an empty string.
114       * @return This object.
115       */
116      public Builder addDescriptionsTo(TypeCategory...values) {
117         generatorBuilder.addDescriptionsTo(values);
118         return this;
119      }
120
121      /**
122       * <i><l>JsonSchemaSerializer</l> configuration property:&emsp;</i>  Add examples.
123       *
124       * <p>
125       * Identifies which categories of types that examples should be automatically added to generated schemas.
126       * <p>
127       * The examples come from calling {@link ClassMeta#getExample(MarshallingSession,JsonParserSession)} which in turn gets examples
128       * from the following:
129       * <ul class='javatree'>
130       *    <li class='ja'>{@link Example}
131       *    <li class='ja'>{@link Marshalled#example() Marshalled(example)}
132       * </ul>
133       *
134       * <h5 class='section'>See Also:</h5><ul>
135       *    <li class='jm'>{@link org.apache.juneau.marshall.jsonschema.JsonSchemaGenerator.Builder#addExamplesTo(TypeCategory...) JsonSchemaGenerator.Builder#addExamplesTo(TypeCategory...)}
136       * </ul>
137       *
138       * @param values
139       *    The values to add to this setting.
140       *    <br>The default is an empty string.
141       * @return This object.
142       */
143      public Builder addExamplesTo(TypeCategory...values) {
144         generatorBuilder.addExamplesTo(values);
145         return this;
146      }
147
148      /**
149       * <i><l>JsonSchemaSerializer</l> configuration property:&emsp;</i>  Allow nested descriptions.
150       *
151       * <p>
152       * Identifies whether nested descriptions are allowed in schema definitions.
153       *
154       * <h5 class='section'>See Also:</h5><ul>
155       *    <li class='jm'>{@link org.apache.juneau.marshall.jsonschema.JsonSchemaGenerator.Builder#allowNestedDescriptions() JsonSchemaGenerator.Builder#allowNestedDescriptions()}
156       * </ul>
157       *
158       * @return This object.
159       */
160      public Builder allowNestedDescriptions() {
161         generatorBuilder.allowNestedDescriptions();
162         return this;
163      }
164
165      /**
166       * <i><l>JsonSchemaSerializer</l> configuration property:&emsp;</i>  Allow nested examples.
167       *
168       * <p>
169       * Identifies whether nested examples are allowed in schema definitions.
170       *
171       * <h5 class='section'>See Also:</h5><ul>
172       *    <li class='jm'>{@link org.apache.juneau.marshall.jsonschema.JsonSchemaGenerator.Builder#allowNestedExamples() JsonSchemaGenerator.Builder#allowNestedExamples()}
173       * </ul>
174       *
175       * @return This object.
176       */
177      public Builder allowNestedExamples() {
178         generatorBuilder.allowNestedExamples();
179         return this;
180      }
181
182      /**
183       * <i><l>JsonSchemaSerializer</l> configuration property:&emsp;</i>  Schema definition mapper.
184       *
185       * <p>
186       * Interface to use for converting Bean classes to definition IDs and URIs.
187       * <p>
188       * Used primarily for defining common definition sections for beans in Swagger JSON.
189       * <p>
190       * This setting is ignored if {@link org.apache.juneau.marshall.jsonschema.JsonSchemaGenerator.Builder#useBeanDefs() JsonSchemaGenerator.Builder#useBeanDefs()} is not enabled.
191       *
192       * <h5 class='section'>See Also:</h5><ul>
193       *    <li class='jm'>{@link org.apache.juneau.marshall.jsonschema.JsonSchemaGenerator.Builder#beanDefMapper(Class) JsonSchemaGenerator.Builder#beanDefMapper(Class)}
194       * </ul>
195       *
196       * @param value
197       *    The new value for this property.
198       *    <br>The default is {@link BasicBeanDefMapper}.
199       * @return This object.
200       */
201      public Builder beanDefMapper(Class<? extends MarshallingDefMapper> value) {
202         generatorBuilder.beanDefMapper(value);
203         return this;
204      }
205
206      @Override /* Overridden from Context.Builder<?> */
207      public JsonSchemaSerializer build() {
208         return cache(CACHE).build(JsonSchemaSerializer.class);
209      }
210
211      @Override /* Overridden from Context.Builder<?> */
212      public Builder copy() {
213         return new Builder(this);
214      }
215
216      @Override /* Overridden from Context.Builder<?> */
217      public HashKey hashKey() {
218         return HashKey.of(super.hashKey(), generatorBuilder.hashKey());
219      }
220
221      /**
222       * <i><l>JsonSchemaSerializer</l> configuration property:&emsp;</i>  Use bean definitions.
223       *
224       * <p>
225       * When enabled, schemas on beans will be serialized as the following:
226       * <p class='bjson'>
227       *    {
228       *       type: <js>'object'</js>,
229       *       <js>'$ref'</js>: <js>'#/definitions/TypeId'</js>
230       *    }
231       * </p>
232       *
233       * @return This object.
234       */
235      public Builder useBeanDefs() {
236         generatorBuilder.useBeanDefs();
237         return this;
238      }
239
240
241   }
242
243   /** Default serializer, with whitespace. */
244   public static class Readable extends JsonSchemaSerializer {
245
246      /**
247       * Constructor.
248       *
249       * @param builder The builder for this object.
250       */
251      public Readable(Builder builder) {
252         super(builder.useWhitespace());
253      }
254   }
255
256   /** Default serializer, all default settings.*/
257   public static final JsonSchemaSerializer DEFAULT = new JsonSchemaSerializer(create());
258   /** Default serializer, all default settings.*/
259   public static final JsonSchemaSerializer DEFAULT_READABLE = new Readable(create());
260
261   /**
262    * Creates a new builder for this object.
263    *
264    * @return A new builder.
265    */
266   public static Builder create() {
267      return new Builder();
268   }
269
270   final JsonSchemaGenerator generator;
271
272   private final Map<ClassMeta<?>,JsonSchemaClassMeta> jsonSchemaClassMetas = new ConcurrentHashMap<>();
273   private final Map<BeanPropertyMeta,JsonSchemaBeanPropertyMeta> jsonSchemaBeanPropertyMetas = new ConcurrentHashMap<>();
274
275   /**
276    * Constructor.
277    *
278    * @param builder The builder for this object.
279    */
280   public JsonSchemaSerializer(Builder builder) {
281      super(builder.detectRecursions().ignoreRecursions());
282
283      generator = builder.generatorBuilder.build();
284   }
285
286   @Override /* Overridden from Context */
287   public Builder copy() {
288      return new Builder(this);
289   }
290
291   @Override /* Overridden from Context */
292   public JsonSchemaSerializerSession.Builder createSession() {
293      return JsonSchemaSerializerSession.create(this);
294   }
295
296   @Override /* Overridden from JsonSchemaMetaProvider */
297   public JsonSchemaBeanPropertyMeta getJsonSchemaBeanPropertyMeta(BeanPropertyMeta bpm) {
298      return jsonSchemaBeanPropertyMetas.computeIfAbsent(bpm, k -> new JsonSchemaBeanPropertyMeta(k.getDelegateFor(), this));
299   }
300
301   @Override /* Overridden from JsonSchemaMetaProvider */
302   public JsonSchemaClassMeta getJsonSchemaClassMeta(ClassMeta<?> cm) {
303      return jsonSchemaClassMetas.computeIfAbsent(cm, k -> new JsonSchemaClassMeta(k, this));
304   }
305
306   @Override /* Overridden from Context */
307   public JsonSchemaSerializerSession getSession() { return createSession().build(); }
308
309   /**
310    * Convenience delegator (default session args) for the raw-JSON token writer.  Real impl on
311    * {@link JsonSchemaSerializerSession#writeTokens(Object)}.
312    *
313    * <p>
314    * <b>Note:</b> the cursor's structural methods produce ordinary JSON regardless of the
315    * JsonSchema-aware {@link #write(Object)} path; the {@link TokenWriter#object(Object)
316    * object(Object)} bridge is <b>disabled</b>.  Use {@link #write(Object)} for schema
317    * generation, or compose the schema manually via the structural methods.
318    *
319    * @param output The output.
320    * @return A new {@link JsonTokenWriter} with {@code object(...)} disabled.
321    * @throws IOException If the output type is not supported or could not be opened.
322    */
323   @Override /* TokenWritable */
324   public TokenWriter writeTokens(Object output) throws IOException {
325      return getSession().writeTokens(output);
326   }
327
328   /**
329    * Convenience delegator (default session args) for a record writer that emits the JSON Schema
330    * for each value passed to {@link RecordWriter#write(Object) write(...)}.  Real impl on
331    * {@link JsonSchemaSerializerSession#writeRecords(Object)}.
332    *
333    * @param output The output.
334    * @return A new {@link RecordWriter}.
335    * @throws IOException If the output type is not supported or could not be opened.
336    */
337   @Override /* RecordWritable */
338   public RecordWriter writeRecords(Object output) throws IOException {
339      return getSession().writeRecords(output);
340   }
341
342   @Override /* RecordWritable */
343   public boolean isRecordStreaming() { return false; }
344
345   @Override /* Overridden from JsonSerializer */
346   protected FluentMap<String,Object> properties() {
347      return super.properties()
348         .a("generator", generator);
349   }
350
351   JsonSchemaGenerator getGenerator() { return generator; }
352}