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