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: </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: </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: </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: </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: </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: </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}