Skip to content

Commit 35d0518

Browse files
committed
Add validation for Element.id and Extension.url
These elements are "special" in that, in the XML serialization, they are attributes and not elements. To facilitate this "specialness", we keep them as raw java strings in our model, but according to the spec they should still be valid data types (String and Id respectively). Also added javadoc for ValidationSupport.java Signed-off-by: Lee Surprenant <lmsurpre@us.ibm.com>
1 parent 0f41463 commit 35d0518

7 files changed

Lines changed: 1306 additions & 33 deletions

File tree

fhir-model/src/main/java/com/ibm/fhir/model/type/Element.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public abstract class Element extends AbstractVisitable {
3737
protected Element(Builder builder) {
3838
id = builder.id;
3939
extension = Collections.unmodifiableList(ValidationSupport.requireNonNull(builder.extension, "extension"));
40+
ValidationSupport.checkString(id);
4041
}
4142

4243
/**

fhir-model/src/main/java/com/ibm/fhir/model/type/Extension.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ private Extension(Builder builder) {
4040
super(builder);
4141
url = ValidationSupport.requireNonNull(builder.url, "url");
4242
value = ValidationSupport.choiceElement(builder.value, "value", Base64Binary.class, Boolean.class, Canonical.class, Code.class, Date.class, DateTime.class, Decimal.class, Id.class, Instant.class, Integer.class, Markdown.class, Oid.class, PositiveInt.class, String.class, Time.class, UnsignedInt.class, Uri.class, Url.class, Uuid.class, Address.class, Age.class, Annotation.class, Attachment.class, CodeableConcept.class, Coding.class, ContactPoint.class, Count.class, Distance.class, Duration.class, HumanName.class, Identifier.class, Money.class, Period.class, Quantity.class, Range.class, Ratio.class, Reference.class, SampledData.class, Signature.class, Timing.class, ContactDetail.class, Contributor.class, DataRequirement.class, Expression.class, ParameterDefinition.class, RelatedArtifact.class, TriggerDefinition.class, UsageContext.class, Dosage.class);
43+
ValidationSupport.checkUri(url);
4344
ValidationSupport.requireValueOrChildren(this);
4445
}
4546

fhir-model/src/main/java/com/ibm/fhir/model/type/Id.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import java.util.Collection;
1010
import java.util.Objects;
11-
import java.util.regex.Pattern;
1211

1312
import javax.annotation.Generated;
1413

@@ -21,13 +20,11 @@
2120
*/
2221
@Generated("com.ibm.fhir.tools.CodeGenerator")
2322
public class Id extends String {
24-
private static final Pattern PATTERN = Pattern.compile("[A-Za-z0-9\\-\\.]{1,64}");
25-
2623
private volatile int hashCode;
2724

2825
private Id(Builder builder) {
2926
super(builder);
30-
ValidationSupport.checkValue(value, PATTERN);
27+
ValidationSupport.checkId(value);
3128
}
3229

3330
@Override

fhir-model/src/main/java/com/ibm/fhir/model/util/ValidationSupport.java

Lines changed: 167 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@
2424
import com.ibm.fhir.model.resource.Resource;
2525
import com.ibm.fhir.model.type.Element;
2626

27-
public final class ValidationSupport {
28-
private static final int MIN_LENGTH = 1;
27+
/**
28+
* Static helper methods for validating model objects during construction
29+
*/
30+
public final class ValidationSupport {
31+
private static final int MIN_STRING_LENGTH = 1;
2932
private static final int MAX_STRING_LENGTH = 1048576; // 1024 * 1024 = 1MB
3033
private static final String FHIR_XHTML_XSD = "fhir-xhtml.xsd";
3134
private static final String FHIR_XML_XSD = "xml.xsd";
@@ -42,6 +45,17 @@ public Validator initialValue() {
4245
private ValidationSupport() { }
4346

4447
private static final Set<Character> WHITESPACE = new HashSet<>(Arrays.asList(' ', '\t', '\r', '\n'));
48+
49+
/**
50+
* A sequence of Unicode characters
51+
* <pre>
52+
* pattern: [ \r\n\t\S]+
53+
* </pre>
54+
*
55+
* @throws IllegalStateException if the passed String is not a valid FHIR String value
56+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
57+
* methods can throw the most appropriate exception without catching and wrapping.
58+
*/
4559
public static void checkString(String s) {
4660
if (s == null) {
4761
return;
@@ -59,11 +73,22 @@ public static void checkString(String s) {
5973
throw new IllegalStateException(String.format("String value: '%s' is not valid with respect to pattern: [ \\r\\n\\t\\S]+", s));
6074
}
6175
}
62-
if (count < MIN_LENGTH) {
63-
throw new IllegalStateException(String.format("Trimmed String value length: %d is less than minimum required length: %d", count, MIN_LENGTH));
76+
if (count < MIN_STRING_LENGTH) {
77+
throw new IllegalStateException(String.format("Trimmed String value length: %d is less than minimum required length: %d", count, MIN_STRING_LENGTH));
6478
}
6579
}
6680

81+
/**
82+
* A string which has at least one character and no leading or trailing whitespace and where there is no whitespace other
83+
* than single spaces in the contents.
84+
* <pre>
85+
* pattern: [^\s]+(\s[^\s]+)*
86+
* </pre>
87+
*
88+
* @throws IllegalStateException if the passed String is not a valid FHIR Code value
89+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
90+
* methods can throw the most appropriate exception without catching and wrapping.
91+
*/
6792
public static void checkCode(String s) {
6893
if (s == null) {
6994
return;
@@ -93,6 +118,54 @@ public static void checkCode(String s) {
93118
}
94119
}
95120

121+
/**
122+
* Any combination of letters, numerals, "-" and ".", with a length limit of 64 characters. (This might be an integer, an
123+
* unprefixed OID, UUID or any other identifier pattern that meets these constraints.) Ids are case-insensitive.
124+
* <pre>
125+
* pattern: [A-Za-z0-9\-\.]{1,64}
126+
* </pre>
127+
*
128+
* @throws IllegalStateException if the passed String is not a valid FHIR Id value
129+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
130+
* methods can throw the most appropriate exception without catching and wrapping.
131+
*/
132+
public static void checkId(String s) {
133+
if (s == null) {
134+
return;
135+
}
136+
if (s.isEmpty()) {
137+
throw new IllegalStateException(String.format("Id value must not be empty"));
138+
}
139+
if (s.length() > 64) {
140+
throw new IllegalStateException(String.format("Id value length: %d is greater than maximum allowed length: %d", s.length(), 64));
141+
}
142+
143+
for (int i = 0; i < s.length(); i++) {
144+
char c = s.charAt(i);
145+
//45 = '-'
146+
//46 = '.'
147+
//48 = '0'
148+
//57 = '9'
149+
//65 = 'A'
150+
//90 = 'Z'
151+
//97 = 'a'
152+
//122 = 'z'
153+
if (c < 45 || c == 47 || (c > 57 && c < 65) || (c > 90 && c < 97) || c > 122 ) {
154+
throw new IllegalStateException(String.format("Id value: '%s' contain invalid character '%s'", s, c));
155+
}
156+
}
157+
}
158+
159+
/**
160+
* String of characters used to identify a name or a resource
161+
* <pre>
162+
* pattern: \S*
163+
* </pre>
164+
*
165+
* @throws IllegalStateException if the passed String is not a valid FHIR Id value
166+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
167+
* methods can throw the most appropriate exception without catching and wrapping.
168+
*/
96169
public static void checkUri(String s) {
97170
if (s == null) {
98171
return;
@@ -108,6 +181,11 @@ public static void checkUri(String s) {
108181
}
109182
}
110183

184+
/**
185+
* @throws IllegalStateException if the passed String is longer than the maximum string length
186+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
187+
* methods can throw the most appropriate exception without catching and wrapping.
188+
*/
111189
public static void checkMaxLength(String value) {
112190
if (value != null) {
113191
if (value.length() > MAX_STRING_LENGTH) {
@@ -116,14 +194,24 @@ public static void checkMaxLength(String value) {
116194
}
117195
}
118196

197+
/**
198+
* @throws IllegalStateException if the passed String is shorter than the minimum string length
199+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
200+
* methods can throw the most appropriate exception without catching and wrapping.
201+
*/
119202
public static void checkMinLength(String value) {
120203
if (value != null) {
121-
if (value.trim().length() < MIN_LENGTH) {
122-
throw new IllegalStateException(String.format("String value length: %d is less than minimum required length: %d", value.trim().length(), MIN_LENGTH));
204+
if (value.trim().length() < MIN_STRING_LENGTH) {
205+
throw new IllegalStateException(String.format("Trimmed String value length: %d is less than minimum required length: %d", value.trim().length(), MIN_STRING_LENGTH));
123206
}
124207
}
125208
}
126209

210+
/**
211+
* @throws IllegalStateException if the passed Integer value is less than the passed minValue
212+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
213+
* methods can throw the most appropriate exception without catching and wrapping.
214+
*/
127215
public static void checkValue(Integer value, int minValue) {
128216
if (value != null) {
129217
if (value < minValue) {
@@ -132,6 +220,11 @@ public static void checkValue(Integer value, int minValue) {
132220
}
133221
}
134222

223+
/**
224+
* @throws IllegalStateException if the passed String value does not match the passed pattern
225+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
226+
* methods can throw the most appropriate exception without catching and wrapping.
227+
*/
135228
public static void checkValue(String value, Pattern pattern) {
136229
if (value != null) {
137230
if (!pattern.matcher(value).matches()) {
@@ -140,6 +233,11 @@ public static void checkValue(String value, Pattern pattern) {
140233
}
141234
}
142235

236+
/**
237+
* @throws IllegalStateException if the type of the passed value is not one of the passed types
238+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
239+
* methods can throw the most appropriate exception without catching and wrapping.
240+
*/
143241
public static <T> T checkValueType(T value, Class<?>... types) {
144242
if (value != null) {
145243
List<Class<?>> typeList = Arrays.asList(types);
@@ -151,17 +249,13 @@ public static <T> T checkValueType(T value, Class<?>... types) {
151249
}
152250
return value;
153251
}
154-
155-
public static void checkXHTMLContent(String value) {
156-
try {
157-
Validator validator = THREAD_LOCAL_VALIDATOR.get();
158-
validator.reset();
159-
validator.validate(new StreamSource(new StringReader(value)));
160-
} catch (Exception e) {
161-
throw new IllegalStateException(String.format("Invalid XHTML content: %s", e.getMessage()), e);
162-
}
163-
}
164-
252+
253+
/**
254+
* @throws IllegalStateException if the type of the passed element is not one of the passed types
255+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
256+
* methods can throw the most appropriate exception without catching and wrapping.
257+
* @apiNote Only differs from {@link #checkValueType} in that we can provide a better error message
258+
*/
165259
public static <T extends Element> T choiceElement(T element, String elementName, Class<?>... types) {
166260
if (element != null) {
167261
Class<?> elementType = element.getClass();
@@ -172,7 +266,22 @@ public static <T extends Element> T choiceElement(T element, String elementName,
172266
}
173267
return element;
174268
}
175-
269+
270+
/**
271+
* @throws IllegalStateException if the passed String value is not valid XHTML
272+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
273+
* methods can throw the most appropriate exception without catching and wrapping.
274+
*/
275+
public static void checkXHTMLContent(String value) {
276+
try {
277+
Validator validator = THREAD_LOCAL_VALIDATOR.get();
278+
validator.reset();
279+
validator.validate(new StreamSource(new StringReader(value)));
280+
} catch (Exception e) {
281+
throw new IllegalStateException(String.format("Invalid XHTML content: %s", e.getMessage()), e);
282+
}
283+
}
284+
176285
private static Schema createSchema() {
177286
try {
178287
StreamSource[] sources = new StreamSource[3];
@@ -195,11 +304,21 @@ private static SchemaFactory createSchemaFactory() {
195304
}
196305
}
197306

307+
/**
308+
* @throws IllegalStateException if the passed element is null or if its type is not one of the passed types
309+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
310+
* methods can throw the most appropriate exception without catching and wrapping.
311+
*/
198312
public static <T extends Element> T requireChoiceElement(T element, String elementName, Class<?>... types) {
199313
requireNonNull(element, elementName);
200314
return choiceElement(element, elementName, types);
201315
}
202316

317+
/**
318+
* @throws IllegalStateException if the passed list is empty or contains any null objects
319+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
320+
* methods can throw the most appropriate exception without catching and wrapping.
321+
*/
203322
public static <T> List<T> requireNonEmpty(List<T> elements, String elementName) {
204323
requireNonNull(elements, elementName);
205324
if (elements.isEmpty()) {
@@ -208,38 +327,68 @@ public static <T> List<T> requireNonEmpty(List<T> elements, String elementName)
208327
return elements;
209328
}
210329

330+
/**
331+
* @throws IllegalStateException if the passed element is null
332+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
333+
* methods can throw the most appropriate exception without catching and wrapping.
334+
*/
211335
public static <T> T requireNonNull(T element, String elementName) {
212336
if (element == null) {
213337
throw new IllegalStateException(String.format("Missing required element: '%s'", elementName));
214338
}
215339
return element;
216340
}
217341

342+
/**
343+
* @throws IllegalStateException if the passed list contains any null objects
344+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
345+
* methods can throw the most appropriate exception without catching and wrapping.
346+
*/
218347
public static <T> List<T> requireNonNull(List<T> elements, String elementName) {
219348
if (elements.stream().anyMatch(Objects::isNull)) {
220349
throw new IllegalStateException(String.format("Repeating element: '%s' does not permit null elements", elementName));
221350
}
222351
return elements;
223352
}
224353

354+
/**
355+
* @throws IllegalStateException if the passed element has no value and no children
356+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
357+
* methods can throw the most appropriate exception without catching and wrapping.
358+
*/
225359
public static void requireValueOrChildren(Element element) {
226360
if (!element.hasValue() && !element.hasChildren()) {
227361
throw new IllegalStateException("ele-1: All FHIR elements must have a @value or children");
228362
}
229363
}
230364

365+
/**
366+
* @throws IllegalStateException if the passed element has no children
367+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
368+
* methods can throw the most appropriate exception without catching and wrapping.
369+
*/
231370
public static void requireChildren(Resource resource) {
232371
if (!resource.hasChildren()) {
233372
throw new IllegalStateException("global-1: All FHIR elements must have a @value or children");
234373
}
235374
}
236375

376+
/**
377+
* @throws IllegalStateException if the passed element is not null
378+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
379+
* methods can throw the most appropriate exception without catching and wrapping.
380+
*/
237381
public static void prohibited(Element element, String elementName) {
238382
if (element != null) {
239383
throw new IllegalStateException(String.format("Element: '%s' is prohibited.", elementName));
240384
}
241385
}
242386

387+
/**
388+
* @throws IllegalStateException if the passed list is not empty
389+
* @apiNote IllegalStateException is chosen in favor of IllegalArgumentException so that Builder.build()
390+
* methods can throw the most appropriate exception without catching and wrapping.
391+
*/
243392
public static <T extends Element> void prohibited(List<T> elements, String elementName) {
244393
if (!elements.isEmpty()) {
245394
throw new IllegalStateException(String.format("Element: '%s' is prohibited.", elementName));

0 commit comments

Comments
 (0)