Skip to content

Commit b8056c7

Browse files
authored
Merge pull request #434 from IBM/issue-424-followon
Add validation for Element.id and Extension.url
2 parents 0f41463 + 35d0518 commit b8056c7

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)