001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2016 ForgeRock AS. 015 */ 016 017package org.forgerock.api.models; 018 019import static org.forgerock.api.jackson.JacksonUtils.OBJECT_MAPPER; 020import static org.forgerock.api.jackson.JacksonUtils.schemaFor; 021import static org.forgerock.json.JsonValue.json; 022 023import com.fasterxml.jackson.annotation.JsonAnySetter; 024import com.fasterxml.jackson.annotation.JsonIgnore; 025import com.fasterxml.jackson.annotation.JsonProperty; 026import com.fasterxml.jackson.databind.JsonMappingException; 027import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 028import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; 029import java.io.IOException; 030import java.io.InputStream; 031import java.util.HashMap; 032import java.util.Map; 033import java.util.Objects; 034import org.forgerock.api.jackson.JacksonUtils; 035import org.forgerock.json.JsonValue; 036import org.forgerock.util.Reject; 037import org.wrensecurity.guava.common.base.Strings; 038 039/** 040 * Class that represents the Schema type in API descriptor. 041 */ 042@JsonDeserialize(builder = Schema.Builder.class) 043public abstract class Schema { 044 045 private Schema() { 046 // This class only has two private inner sub-classes, so constructor is private. 047 } 048 049 /** 050 * Getter for reference. May be null if the schema is specified here. 051 * @return The reference. 052 */ 053 public abstract Reference getReference(); 054 055 /** 056 * Obtain the schema definition if it is not a reference. 057 * @return The schema. 058 */ 059 public abstract JsonValue getSchema(); 060 061 @Override 062 public boolean equals(Object o) { 063 if (this == o) { 064 return true; 065 } 066 if (o == null || getClass() != o.getClass()) { 067 return false; 068 } 069 070 Schema schema1 = (Schema) o; 071 return Objects.equals(getReference(), schema1.getReference()) 072 && isSchemaPropertyMatches(schema1); 073 074 } 075 076 private boolean isSchemaPropertyMatches(Schema schema1) { 077 return getSchema() != null && schema1.getSchema() != null 078 ? Objects.equals(getSchema().getObject(), schema1.getSchema().getObject()) 079 : schema1.getSchema() == getSchema(); 080 } 081 082 @Override 083 public int hashCode() { 084 JsonValue schema = getSchema(); 085 return Objects.hash(getReference(), schema == null ? null : schema.getObject()); 086 } 087 088 /** 089 * Create a new Builder for Schema. 090 * @return The builder. 091 */ 092 public static Builder newBuilder() { 093 return new Builder(); 094 } 095 096 /** 097 * Create a new Builder for Schema. A synonym for {@link #newBuilder()} that is useful for static imports. 098 * @return The builder. 099 */ 100 public static Builder schema() { 101 return newBuilder(); 102 } 103 104 /** 105 * Builds Schema object from the data in the annotation parameter. If the {@code schema} has an {@code id} defined, 106 * or if the type being used for the schema definition has an {@code id} defined, the schema will be defined in the 107 * top-level {@code descriptor}, and a reference to that definition will be returned. 108 * 109 * @param schema The annotation that holds the data 110 * @param descriptor The root descriptor to add definitions to. 111 * @param relativeType The type relative to which schema resources should be resolved. 112 * @return Schema instance 113 */ 114 public static Schema fromAnnotation(org.forgerock.api.annotations.Schema schema, ApiDescription descriptor, 115 final Class<?> relativeType) { 116 Class<?> type = schema.fromType(); 117 if (type.equals(Void.class) && Strings.isNullOrEmpty(schema.schemaResource())) { 118 return null; 119 } 120 Builder builder = schema(); 121 String id = schema.id(); 122 if (!type.equals(Void.class)) { 123 // the annotation declares a type to use as the schema 124 builder.type(type); 125 if (Strings.isNullOrEmpty(id)) { 126 // if the schema annotation passed to this method does not have an id, check to see if the type being 127 // used to generate the schema is annotated with an id. 128 org.forgerock.api.annotations.Schema typeSchema = 129 type.getAnnotation(org.forgerock.api.annotations.Schema.class); 130 if (typeSchema != null && !Strings.isNullOrEmpty(typeSchema.id())) { 131 id = typeSchema.id(); 132 } 133 } 134 } else { 135 // not using a type, so must be using a resource file containing JSON Schema json. 136 InputStream resource = relativeType.getResourceAsStream(schema.schemaResource()); 137 try { 138 JsonValue json = json(JacksonUtils.OBJECT_MAPPER.readValue(resource, Object.class)) 139 .as(new TranslateJsonSchema(relativeType.getClassLoader())); 140 builder.schema(json); 141 } catch (IOException e) { 142 throw new IllegalArgumentException("Could not read declared resource " + schema.schemaResource(), e); 143 } 144 } 145 if (!Strings.isNullOrEmpty(id)) { 146 // we've got an id for this schema, so define it at the top level and return a reference. 147 descriptor.addDefinition(id, builder.build()); 148 return schema().reference(Reference.reference().value("#/definitions/" + id).build()).build(); 149 } else { 150 return builder.build(); 151 } 152 } 153 154 /** 155 * A builder class for {@code Schema} instances. 156 */ 157 public static final class Builder { 158 159 private JsonValue schema; 160 private Reference reference; 161 private Map<String, Object> jsonValueObject = new HashMap<>(); 162 163 /** 164 * Private default constructor. 165 */ 166 private Builder() { } 167 168 /** 169 * Sets the schema reference. 170 * @param reference The reference. 171 * @return This builder. 172 */ 173 @JsonProperty("$ref") 174 public Builder reference(Reference reference) { 175 Reject.ifNull(reference); 176 this.reference = reference; 177 return this; 178 } 179 180 /** 181 * Sets the schema. 182 * @param schema The schema. 183 * @return This builder. 184 */ 185 public Builder schema(JsonValue schema) { 186 Reject.ifNull(schema); 187 this.schema = schema; 188 return this; 189 } 190 191 /** 192 * Sets the schema by json key-value pairs. 193 * @param key json parameter name 194 * @param value json parametr value 195 * @return This builder. 196 */ 197 @JsonAnySetter 198 public Builder schema(String key, Object value) { 199 jsonValueObject.put(key, value); 200 return this; 201 } 202 203 /** 204 * Sets the schema. 205 * @param type The type to derive the schema from. 206 * @return This builder. 207 */ 208 public Builder type(Class<?> type) { 209 Reject.ifNull(type); 210 try { 211 JsonSchema jsonSchema = schemaFor(type); 212 String schemaString = OBJECT_MAPPER.writer().writeValueAsString(jsonSchema); 213 this.schema = json(OBJECT_MAPPER.readValue(schemaString, Object.class)) 214 .as(new TranslateJsonSchema(type.getClassLoader())); 215 } catch (JsonMappingException e) { 216 throw new IllegalArgumentException(e); 217 } catch (IOException e) { 218 throw new IllegalStateException("Jackson cannot read its own JSON", e); 219 } 220 return this; 221 } 222 223 /** 224 * Builds the Schema instance. 225 * 226 * @return Schema instance. 227 */ 228 public Schema build() { 229 if (!jsonValueObject.isEmpty()) { 230 schema(json(jsonValueObject)); 231 } 232 return reference == null ? new SchemaSchema(schema) : new ReferenceSchema(reference); 233 } 234 } 235 236 private static final class ReferenceSchema extends Schema { 237 238 private final Reference reference; 239 240 private ReferenceSchema(Reference reference) { 241 this.reference = reference; 242 } 243 244 @Override 245 @JsonProperty("$ref") 246 public Reference getReference() { 247 return reference; 248 } 249 250 @Override 251 @JsonIgnore 252 public JsonValue getSchema() { 253 return null; 254 } 255 } 256 257 private static final class SchemaSchema extends Schema { 258 259 private final JsonValue schema; 260 261 private SchemaSchema(JsonValue schema) { 262 this.schema = schema; 263 } 264 265 @Override 266 @JsonIgnore 267 public Reference getReference() { 268 return null; 269 } 270 271 @Override 272 @com.fasterxml.jackson.annotation.JsonValue 273 public JsonValue getSchema() { 274 return schema; 275 } 276 } 277}