View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2016 ForgeRock AS.
15   */
16  
17  package org.forgerock.api.jackson;
18  
19  import static org.forgerock.api.jackson.JacksonUtils.*;
20  import static org.forgerock.api.util.ValidationUtil.isEmpty;
21  
22  import java.net.URI;
23  import java.util.Collections;
24  import java.util.List;
25  import java.util.Map;
26  
27  import javax.mail.internet.AddressException;
28  import javax.mail.internet.InternetAddress;
29  import javax.validation.ValidationException;
30  import javax.xml.bind.DatatypeConverter;
31  
32  import org.forgerock.api.enums.ReadPolicy;
33  import org.wrensecurity.guava.common.net.InetAddresses;
34  import org.wrensecurity.guava.common.net.InternetDomainName;
35  import org.forgerock.json.JsonValue;
36  
37  import com.fasterxml.jackson.annotation.JsonProperty;
38  import com.fasterxml.jackson.module.jsonSchema.types.StringSchema;
39  import org.forgerock.api.enums.WritePolicy;
40  
41  /**
42   * An extension to the Jackson {@code StringSchema} that includes the custom CREST JSON Schema attributes.
43   */
44  class CrestStringSchema extends StringSchema implements CrestReadWritePoliciesSchema, OrderedFieldSchema, EnumSchema,
45          ValidatableSchema, PropertyFormatSchema, WithExampleSchema<String> {
46      private WritePolicy writePolicy;
47      private ReadPolicy readPolicy;
48      private Boolean errorOnWritePolicyFailure;
49      private Boolean returnOnDemand;
50      private Integer propertyOrder;
51      private String propertyFormat;
52      @JsonProperty
53      private Map<String, List<String>> options;
54      private String example;
55  
56      @Override
57      public WritePolicy getWritePolicy() {
58          return writePolicy;
59      }
60  
61      @Override
62      public void setWritePolicy(WritePolicy policy) {
63          this.writePolicy = policy;
64      }
65  
66      @Override
67      public ReadPolicy getReadPolicy() {
68          return readPolicy;
69      }
70  
71      @Override
72      public void setReadPolicy(ReadPolicy readPolicy) {
73          this.readPolicy = readPolicy;
74      }
75  
76      @Override
77      public Boolean getErrorOnWritePolicyFailure() {
78          return errorOnWritePolicyFailure;
79      }
80  
81      @Override
82      public void setErrorOnWritePolicyFailure(Boolean errorOnWritePolicyFailure) {
83          this.errorOnWritePolicyFailure = errorOnWritePolicyFailure;
84      }
85  
86      @Override
87      public Boolean getReturnOnDemand() {
88          return returnOnDemand;
89      }
90  
91      @Override
92      public void setReturnOnDemand(Boolean returnOnDemand) {
93          this.returnOnDemand = returnOnDemand;
94      }
95  
96      @Override
97      public Integer getPropertyOrder() {
98          return propertyOrder;
99      }
100 
101     @Override
102     public void setPropertyOrder(Integer order) {
103         this.propertyOrder = order;
104     }
105 
106     @Override
107     public List<String> getEnumTitles() {
108         return options == null ? null : options.get(ENUM_TITLES);
109     }
110 
111     @Override
112     public void setEnumTitles(List<String> titles) {
113         this.options = Collections.singletonMap(ENUM_TITLES, titles);
114     }
115 
116     @Override
117     public void validate(JsonValue object) throws ValidationException {
118         if (!object.isString()) {
119             throw new ValidationException("Expected string but got: " + object.getObject());
120         }
121         String s = object.asString();
122         validateEnum(enums, s);
123         if (!isEmpty(propertyFormat)) {
124             validateFormat(s);
125         }
126     }
127 
128     /**
129      * Validates a subset of known {@code format} values, but allows unknown values to pass-through, according to
130      * JSON Schema v4 spec.
131      *
132      * @param s Value to validate
133      * @throws ValidationException Indicates that {@code s} does not conform to a known JSON Schema format.
134      */
135     private void validateFormat(final String s) throws ValidationException {
136         switch (propertyFormat) {
137         case "date-time":
138             // http://tools.ietf.org/html/rfc3339#section-5.6
139             try {
140                 DatatypeConverter.parseDateTime(s);
141             } catch (IllegalArgumentException e) {
142                 throw new ValidationException("Expected date-time format, but got " + s, e);
143             }
144             return;
145         case "date":
146         case "full-date":
147             // NOTE: supported by OpenAPI, but not defined by JSON Schema v4 spec
148             // http://tools.ietf.org/html/rfc3339#section-5.6
149             try {
150                 DatatypeConverter.parseDate(s);
151             } catch (IllegalArgumentException e) {
152                 throw new ValidationException("Expected date/full-date format, but got " + s, e);
153             }
154             return;
155         case "email":
156             // http://tools.ietf.org/html/rfc5322#section-3.4.1
157             try {
158                 new InternetAddress(s).validate();
159             } catch (AddressException e) {
160                 throw new ValidationException("Expected email, but got " + s, e);
161             }
162             return;
163         case "hostname":
164             // http://tools.ietf.org/html/rfc1034#section-3.1
165             if (!InternetDomainName.isValid(s)) {
166                 throw new ValidationException("Expected host-name, but got " + s);
167             }
168             return;
169         case "ipv4":
170             // http://tools.ietf.org/html/rfc2673#section-3.2
171             if (!InetAddresses.isInetAddress(s) || s.indexOf(':') != -1) {
172                 throw new ValidationException("Expected ipv4, but got " + s);
173             }
174             return;
175         case "ipv6":
176             // http://tools.ietf.org/html/rfc2373#section-2.2
177             if (!InetAddresses.isInetAddress(s) || s.indexOf(':') == -1) {
178                 throw new ValidationException("Expected ipv6, but got " + s);
179             }
180             return;
181         case "uri":
182             // http://tools.ietf.org/html/rfc3986
183             try {
184                 URI.create(s);
185             } catch (IllegalArgumentException e) {
186                 throw new ValidationException("Expected URI format, but got " + s, e);
187             }
188             return;
189         }
190     }
191 
192     // This method overrides the superclass' definition of "format" via JsonProperty annotation
193     @JsonProperty("format")
194     @Override
195     public String getPropertyFormat() {
196         if (!isEmpty(propertyFormat)) {
197             return propertyFormat;
198         }
199         // fallback to old behavior
200         return format == null ? null : format.toString();
201     }
202 
203     @Override
204     public void setPropertyFormat(String propertyFormat) {
205         this.propertyFormat = propertyFormat;
206     }
207 
208     @Override
209     public String getExample() {
210         return example;
211     }
212 
213     @Override
214     public void setExample(String example) {
215         this.example = example;
216     }
217 }