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