| // *************************************************************************************************************************** |
| // * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * |
| // * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * |
| // * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * |
| // * with the License. You may obtain a copy of the License at * |
| // * * |
| // * http://www.apache.org/licenses/LICENSE-2.0 * |
| // * * |
| // * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * |
| // * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * |
| // * specific language governing permissions and limitations under the License. * |
| // *************************************************************************************************************************** |
| package org.apache.juneau.jsonschema; |
| |
| import static org.apache.juneau.internal.ObjectUtils.*; |
| import static org.apache.juneau.jsonschema.TypeCategory.*; |
| |
| import java.lang.reflect.*; |
| import java.util.*; |
| import java.util.regex.*; |
| |
| import org.apache.juneau.*; |
| import org.apache.juneau.json.*; |
| import org.apache.juneau.jsonschema.annotation.*; |
| import org.apache.juneau.parser.ParseException; |
| import org.apache.juneau.serializer.*; |
| import org.apache.juneau.transform.*; |
| |
| /** |
| * Session object that lives for the duration of a single use of {@link JsonSchemaSerializer}. |
| * |
| * <p> |
| * This class is NOT thread safe. |
| * It is typically discarded after one-time use although it can be reused within the same thread. |
| */ |
| public class JsonSchemaGeneratorSession extends BeanTraverseSession { |
| |
| private final JsonSchemaGenerator ctx; |
| private final Map<String,ObjectMap> defs; |
| private JsonSerializerSession jsSession; |
| |
| /** |
| * Create a new session using properties specified in the context. |
| * |
| * @param ctx |
| * The context creating this session object. |
| * The context contains all the configuration settings for this object. |
| * @param args |
| * Runtime arguments. |
| * These specify session-level information such as locale and URI context. |
| * It also include session-level properties that override the properties defined on the bean and |
| * serializer contexts. |
| */ |
| protected JsonSchemaGeneratorSession(JsonSchemaGenerator ctx, BeanSessionArgs args) { |
| super(ctx, args); |
| this.ctx = ctx; |
| if (isUseBeanDefs()) |
| defs = new TreeMap<>(); |
| else |
| defs = null; |
| } |
| |
| /** |
| * Returns the JSON-schema for the specified object. |
| * |
| * @param o |
| * The object. |
| * <br>Can either be a POJO or a <c>Class</c>/<c>Type</c>. |
| * @return The schema for the type. |
| * @throws BeanRecursionException Bean recursion occurred. |
| * @throws SerializeException Error occurred. |
| */ |
| public ObjectMap getSchema(Object o) throws BeanRecursionException, SerializeException { |
| return getSchema(toClassMeta(o), "root", null, false, false, null); |
| } |
| |
| /** |
| * Returns the JSON-schema for the specified type. |
| * |
| * @param type The object type. |
| * @return The schema for the type. |
| * @throws BeanRecursionException Bean recursion occurred. |
| * @throws SerializeException Error occurred. |
| */ |
| public ObjectMap getSchema(Type type) throws BeanRecursionException, SerializeException { |
| return getSchema(getClassMeta(type), "root", null, false, false, null); |
| } |
| |
| /** |
| * Returns the JSON-schema for the specified type. |
| * |
| * @param cm The object type. |
| * @return The schema for the type. |
| * @throws BeanRecursionException Bean recursion occurred. |
| * @throws SerializeException Error occurred. |
| */ |
| public ObjectMap getSchema(ClassMeta<?> cm) throws BeanRecursionException, SerializeException { |
| return getSchema(cm, "root", null, false, false, null); |
| } |
| |
| @SuppressWarnings({ "unchecked", "rawtypes" }) |
| private ObjectMap getSchema(ClassMeta<?> eType, String attrName, String[] pNames, boolean exampleAdded, boolean descriptionAdded, JsonSchemaBeanPropertyMeta jsbpm) throws BeanRecursionException, SerializeException { |
| |
| if (ctx.isIgnoredType(eType)) |
| return null; |
| |
| ObjectMap out = new ObjectMap(); |
| |
| if (eType == null) |
| eType = object(); |
| |
| ClassMeta<?> aType; // The actual type (will be null if recursion occurs) |
| ClassMeta<?> sType; // The serialized type |
| PojoSwap pojoSwap = eType.getPojoSwap(this); |
| |
| aType = push(attrName, eType, null); |
| |
| sType = eType.getSerializedClassMeta(this); |
| |
| String type = null, format = null; |
| Object example = null, description = null; |
| |
| boolean useDef = isUseBeanDefs() && sType.isBean() && pNames == null; |
| |
| if (useDef) { |
| exampleAdded = false; |
| descriptionAdded = false; |
| } |
| |
| if (useDef && defs.containsKey(getBeanDefId(sType))) { |
| pop(); |
| return new ObjectMap().append("$ref", getBeanDefUri(sType)); |
| } |
| |
| ObjectMap ds = getDefaultSchemas().get(sType.getInnerClass().getName()); |
| if (ds != null && ds.containsKey("type")) { |
| pop(); |
| return out.appendAll(ds); |
| } |
| |
| JsonSchemaClassMeta jscm = null; |
| ClassMeta pojoSwapCM = pojoSwap == null ? null : getClassMeta(pojoSwap.getClass()); |
| if (pojoSwapCM != null && pojoSwapCM.getAnnotation(Schema.class) != null) |
| jscm = getJsonSchemaClassMeta(pojoSwapCM); |
| if (jscm == null) |
| jscm = getJsonSchemaClassMeta(sType); |
| |
| TypeCategory tc = null; |
| |
| if (sType.isNumber()) { |
| tc = NUMBER; |
| if (sType.isDecimal()) { |
| type = "number"; |
| if (sType.isFloat()) { |
| format = "float"; |
| } else if (sType.isDouble()) { |
| format = "double"; |
| } |
| } else { |
| type = "integer"; |
| if (sType.isShort()) { |
| format = "int16"; |
| } else if (sType.isInteger()) { |
| format = "int32"; |
| } else if (sType.isLong()) { |
| format = "int64"; |
| } |
| } |
| } else if (sType.isBoolean()) { |
| tc = BOOLEAN; |
| type = "boolean"; |
| } else if (sType.isMap()) { |
| tc = MAP; |
| type = "object"; |
| } else if (sType.isBean()) { |
| tc = BEAN; |
| type = "object"; |
| } else if (sType.isCollection()) { |
| tc = COLLECTION; |
| type = "array"; |
| } else if (sType.isArray()) { |
| tc = ARRAY; |
| type = "array"; |
| } else if (sType.isEnum()) { |
| tc = ENUM; |
| type = "string"; |
| } else if (sType.isCharSequence() || sType.isChar()) { |
| tc = STRING; |
| type = "string"; |
| } else if (sType.isUri()) { |
| tc = STRING; |
| type = "string"; |
| format = "uri"; |
| } else { |
| tc = STRING; |
| type = "string"; |
| } |
| |
| // Add info from @Schema on bean property. |
| if (jsbpm != null) { |
| out.appendAll(jsbpm.getSchema()); |
| } |
| |
| out.appendAll(jscm.getSchema()); |
| |
| out.appendIf(false, true, true, "type", type); |
| out.appendIf(false, true, true, "format", format); |
| |
| if (aType != null) { |
| |
| example = getExample(sType, tc, exampleAdded); |
| description = getDescription(sType, tc, descriptionAdded); |
| exampleAdded |= example != null; |
| descriptionAdded |= description != null; |
| |
| if (tc == BEAN) { |
| ObjectMap properties = new ObjectMap(); |
| BeanMeta bm = getBeanMeta(sType.getInnerClass()); |
| if (pNames != null) |
| bm = new BeanMetaFiltered(bm, pNames); |
| for (Iterator<BeanPropertyMeta> i = bm.getPropertyMetas().iterator(); i.hasNext();) { |
| BeanPropertyMeta p = i.next(); |
| if (p.canRead()) |
| properties.put(p.getName(), getSchema(p.getClassMeta(), p.getName(), p.getProperties(), exampleAdded, descriptionAdded, getJsonSchemaBeanPropertyMeta(p))); |
| } |
| out.put("properties", properties); |
| |
| } else if (tc == COLLECTION) { |
| ClassMeta et = sType.getElementType(); |
| if (sType.isCollection() && sType.getInfo().isChildOf(Set.class)) |
| out.put("uniqueItems", true); |
| out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null)); |
| |
| } else if (tc == ARRAY) { |
| ClassMeta et = sType.getElementType(); |
| if (sType.isCollection() && sType.getInfo().isChildOf(Set.class)) |
| out.put("uniqueItems", true); |
| out.put("items", getSchema(et, "items", pNames, exampleAdded, descriptionAdded, null)); |
| |
| } else if (tc == ENUM) { |
| out.put("enum", getEnums(sType)); |
| |
| } else if (tc == MAP) { |
| ObjectMap om = getSchema(sType.getValueType(), "additionalProperties", null, exampleAdded, descriptionAdded, null); |
| if (! om.isEmpty()) |
| out.put("additionalProperties", om); |
| |
| } |
| } |
| |
| out.appendAll(jscm.getSchema()); |
| |
| out.appendIf(false, true, true, "description", description); |
| out.appendIf(false, true, true, "x-example", example); |
| |
| if (ds != null) |
| out.appendAll(ds); |
| |
| if (useDef) { |
| defs.put(getBeanDefId(sType), out); |
| out = new ObjectMap().append("$ref", getBeanDefUri(sType)); |
| } |
| |
| pop(); |
| |
| return out; |
| } |
| |
| private List<String> getEnums(ClassMeta<?> cm) { |
| List<String> l = new ArrayList<>(); |
| for (Enum<?> e : getEnumConstants(cm.getInnerClass())) |
| l.add(cm.toString(e)); |
| return l; |
| } |
| |
| private Object getExample(ClassMeta<?> sType, TypeCategory t, boolean exampleAdded) throws SerializeException { |
| boolean canAdd = isAllowNestedExamples() || ! exampleAdded; |
| if (canAdd && (getAddExamplesTo().contains(t) || getAddExamplesTo().contains(ANY))) { |
| Object example = sType.getExample(this); |
| if (example != null) { |
| try { |
| return JsonParser.DEFAULT.parse(toJson(example), Object.class); |
| } catch (ParseException e) { |
| throw new SerializeException(e); |
| } |
| } |
| } |
| return null; |
| } |
| |
| private String toJson(Object o) throws SerializeException { |
| if (jsSession == null) |
| jsSession = ctx.getJsonSerializer().createSession(null); |
| return jsSession.serializeToString(o); |
| } |
| |
| private Object getDescription(ClassMeta<?> sType, TypeCategory t, boolean descriptionAdded) { |
| boolean canAdd = isAllowNestedDescriptions() || ! descriptionAdded; |
| if (canAdd && (getAddDescriptionsTo().contains(t) || getAddDescriptionsTo().contains(ANY))) |
| return sType.toString(); |
| return null; |
| } |
| |
| /** |
| * Returns the definition ID for the specified class. |
| * |
| * @param cm The class to get the definition ID of. |
| * @return The definition ID for the specified class. |
| */ |
| public String getBeanDefId(ClassMeta<?> cm) { |
| return getBeanDefMapper().getId(cm); |
| } |
| |
| /** |
| * Returns the definition URI for the specified class. |
| * |
| * @param cm The class to get the definition URI of. |
| * @return The definition URI for the specified class. |
| */ |
| public java.net.URI getBeanDefUri(ClassMeta<?> cm) { |
| return getBeanDefMapper().getURI(cm); |
| } |
| |
| /** |
| * Returns the definition URI for the specified class. |
| * |
| * @param id The definition ID to get the definition URI of. |
| * @return The definition URI for the specified class. |
| */ |
| public java.net.URI getBeanDefUri(String id) { |
| return getBeanDefMapper().getURI(id); |
| } |
| |
| /** |
| * Returns the definitions that were gathered during this session. |
| * |
| * <p> |
| * This map is modifiable and affects the map in the session. |
| * |
| * @return |
| * The definitions that were gathered during this session, or <jk>null</jk> if {@link JsonSchemaGenerator#JSONSCHEMA_useBeanDefs} was not enabled. |
| */ |
| public Map<String,ObjectMap> getBeanDefs() { |
| return defs; |
| } |
| |
| /** |
| * Adds a schema definition to this session. |
| * |
| * @param id The definition ID. |
| * @param def The definition schema. |
| * @return This object (for method chaining). |
| */ |
| public JsonSchemaGeneratorSession addBeanDef(String id, ObjectMap def) { |
| if (defs != null) |
| defs.put(id, def); |
| return this; |
| } |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Properties |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| /** |
| * Configuration property: Add descriptions to types. |
| * |
| * @see JsonSchemaGenerator#JSONSCHEMA_addDescriptionsTo |
| * @return |
| * Set of categories of types that descriptions should be automatically added to generated schemas. |
| */ |
| protected final Set<TypeCategory> getAddDescriptionsTo() { |
| return ctx.getAddDescriptionsTo(); |
| } |
| |
| /** |
| * Configuration property: Add examples. |
| * |
| * @see JsonSchemaGenerator#JSONSCHEMA_addExamplesTo |
| * @return |
| * Set of categories of types that examples should be automatically added to generated schemas. |
| */ |
| protected final Set<TypeCategory> getAddExamplesTo() { |
| return ctx.getAddExamplesTo(); |
| } |
| |
| /** |
| * Configuration property: Allow nested descriptions. |
| * |
| * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedDescriptions |
| * @return |
| * <jk>true</jk> if nested descriptions are allowed in schema definitions. |
| */ |
| protected final boolean isAllowNestedDescriptions() { |
| return ctx.isAllowNestedDescriptions(); |
| } |
| |
| /** |
| * Configuration property: Allow nested examples. |
| * |
| * @see JsonSchemaGenerator#JSONSCHEMA_allowNestedExamples |
| * @return |
| * <jk>true</jk> if nested examples are allowed in schema definitions. |
| */ |
| protected final boolean isAllowNestedExamples() { |
| return ctx.isAllowNestedExamples(); |
| } |
| |
| /** |
| * Configuration property: Bean schema definition mapper. |
| * |
| * @see JsonSchemaGenerator#JSONSCHEMA_beanDefMapper |
| * @return |
| * Interface to use for converting Bean classes to definition IDs and URIs. |
| */ |
| protected final BeanDefMapper getBeanDefMapper() { |
| return ctx.getBeanDefMapper(); |
| } |
| |
| /** |
| * Configuration property: Default schemas. |
| * |
| * @see JsonSchemaGenerator#JSONSCHEMA_defaultSchemas |
| * @return |
| * Custom schema information for particular class types. |
| */ |
| protected final Map<String,ObjectMap> getDefaultSchemas() { |
| return ctx.getDefaultSchemas(); |
| } |
| |
| /** |
| * Configuration property: Ignore types from schema definitions. |
| * |
| * @see JsonSchemaGenerator#JSONSCHEMA_ignoreTypes |
| * @return |
| * Custom schema information for particular class types. |
| */ |
| protected final Set<Pattern> getIgnoreTypes() { |
| return ctx.getIgnoreTypes(); |
| } |
| |
| /** |
| * Configuration property: Use bean definitions. |
| * |
| * @see JsonSchemaGenerator#JSONSCHEMA_useBeanDefs |
| * @return |
| * <jk>true</jk> if schemas on beans will be serialized with <js>'$ref'</js> tags. |
| */ |
| protected final boolean isUseBeanDefs() { |
| return ctx.isUseBeanDefs(); |
| } |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Extended metadata |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| /** |
| * Returns the language-specific metadata on the specified class. |
| * |
| * @param cm The class to return the metadata on. |
| * @return The metadata. |
| */ |
| public JsonSchemaClassMeta getJsonSchemaClassMeta(ClassMeta<?> cm) { |
| return ctx.getJsonSchemaClassMeta(cm); |
| } |
| |
| /** |
| * Returns the language-specific metadata on the specified bean property. |
| * |
| * @param bpm The bean property to return the metadata on. |
| * @return The metadata. |
| */ |
| public JsonSchemaBeanPropertyMeta getJsonSchemaBeanPropertyMeta(BeanPropertyMeta bpm) { |
| return ctx.getJsonSchemaBeanPropertyMeta(bpm); |
| } |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Utility methods |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| private ClassMeta<?> toClassMeta(Object o) { |
| if (o instanceof Type) |
| return getClassMeta((Type)o); |
| return getClassMetaForObject(o); |
| } |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Other methods |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| @Override /* Session */ |
| public ObjectMap toMap() { |
| return super.toMap() |
| .append("JsonSchemaGeneratorSession", new DefaultFilteringObjectMap() |
| ); |
| } |
| } |