diff --git a/wrangler-api/src/main/java/io/cdap/wrangler/api/parser/ByteSize.java b/wrangler-api/src/main/java/io/cdap/wrangler/api/parser/ByteSize.java
new file mode 100644
index 000000000..3b582bec5
--- /dev/null
+++ b/wrangler-api/src/main/java/io/cdap/wrangler/api/parser/ByteSize.java
@@ -0,0 +1,100 @@
+package io.cdap.wrangler.api.parser;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Represents a byte size token parsed from a string (e.g., "10KB", "1.5MB").
+ * The value is stored in canonical units (bytes).
+ */
+public class ByteSize implements Token {
+ // The original token string.
+ private final String token;
+ // Canonical value stored in bytes.
+ private final long bytes;
+ // Token type.
+ private final TokenType tokenType = TokenType.BYTE_SIZE;
+
+ // Conversion constants (using 1024-based conversions)
+ private static final long KILOBYTE = 1024;
+ private static final long MEGABYTE = KILOBYTE * 1024;
+ private static final long GIGABYTE = MEGABYTE * 1024;
+ private static final long TERABYTE = GIGABYTE * 1024;
+
+ // Pattern to capture the numeric and unit portions (e.g., "10KB" or "1.5MB").
+ private static final Pattern PATTERN = Pattern.compile("([0-9]+(?:\\.[0-9]+)?)([a-zA-Z]+)");
+
+ /**
+ * Constructs a ByteSize token by parsing the input string.
+ *
+ * @param token the string representation (e.g., "10KB", "1.5MB")
+ * @throws IllegalArgumentException if the token is null, empty, or its format is invalid.
+ */
+ public ByteSize(String token) {
+ if (token == null || token.trim().isEmpty()) {
+ throw new IllegalArgumentException("Token cannot be null or empty");
+ }
+ this.token = token;
+ Matcher matcher = PATTERN.matcher(token);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Invalid byte size token format: " + token);
+ }
+
+ String numberStr = matcher.group(1);
+ String unitStr = matcher.group(2).toUpperCase(); // Normalize unit to upper-case
+
+ double value = Double.parseDouble(numberStr);
+ long multiplier;
+
+ // Determine multiplier based on the unit.
+ if ("B".equals(unitStr)) {
+ multiplier = 1;
+ } else if ("KB".equals(unitStr)) {
+ multiplier = KILOBYTE;
+ } else if ("MB".equals(unitStr)) {
+ multiplier = MEGABYTE;
+ } else if ("GB".equals(unitStr)) {
+ multiplier = GIGABYTE;
+ } else if ("TB".equals(unitStr)) {
+ multiplier = TERABYTE;
+ } else {
+ throw new IllegalArgumentException("Unsupported byte size unit: " + unitStr);
+ }
+
+ // Calculate canonical value in bytes.
+ this.bytes = (long) Math.round(value * multiplier);
+ }
+
+ /**
+ * Returns the byte size in canonical units (bytes).
+ *
+ * @return the size in bytes.
+ */
+ public long getBytes() {
+ return bytes;
+ }
+
+ @Override
+ public Object value() {
+ // Return the canonical value.
+ return bytes;
+ }
+
+ @Override
+ public TokenType type() {
+ return tokenType;
+ }
+
+ @Override
+ public JsonElement toJson() {
+ // Construct a JSON representation of this token.
+ JsonObject obj = new JsonObject();
+ obj.addProperty("token", token);
+ obj.addProperty("type", tokenType.name());
+ obj.addProperty("bytes", bytes);
+ return obj;
+ }
+}
diff --git a/wrangler-api/src/main/java/io/cdap/wrangler/api/parser/TimeDuration.java b/wrangler-api/src/main/java/io/cdap/wrangler/api/parser/TimeDuration.java
new file mode 100644
index 000000000..0bfd383f5
--- /dev/null
+++ b/wrangler-api/src/main/java/io/cdap/wrangler/api/parser/TimeDuration.java
@@ -0,0 +1,92 @@
+package io.cdap.wrangler.api.parser;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Represents a time duration token parsed from a string (e.g., "150ms", "2.1s").
+ * The value is stored in canonical units (milliseconds).
+ */
+public class TimeDuration implements Token {
+ // The original token string.
+ private final String token;
+ // Canonical value stored in milliseconds.
+ private final long milliseconds;
+ // Token type.
+ private final TokenType tokenType = TokenType.TIME_DURATION;
+
+ // Constant: 1 second = 1000 milliseconds.
+ private static final long SECOND_IN_MS = 1000;
+
+ // Pattern to capture the numeric and unit portions (e.g., "150ms" or "2.1s").
+ private static final Pattern PATTERN = Pattern.compile("([0-9]+(?:\\.[0-9]+)?)([a-zA-Z]+)");
+
+ /**
+ * Constructs a TimeDuration token by parsing the input string.
+ *
+ * @param token the string representation (e.g., "150ms", "2.1s")
+ * @throws IllegalArgumentException if the token is null, empty, or its format is invalid.
+ */
+ public TimeDuration(String token) {
+ if (token == null || token.trim().isEmpty()) {
+ throw new IllegalArgumentException("Token cannot be null or empty");
+ }
+ this.token = token;
+ Matcher matcher = PATTERN.matcher(token);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Invalid time duration token format: " + token);
+ }
+
+ String numberStr = matcher.group(1);
+ // Normalize the unit to lower-case for easier comparison.
+ String unitStr = matcher.group(2).toLowerCase();
+
+ double value = Double.parseDouble(numberStr);
+ long multiplier;
+
+ // Determine multiplier based on the unit.
+ if ("ms".equals(unitStr)) {
+ multiplier = 1;
+ } else if ("s".equals(unitStr)) {
+ multiplier = SECOND_IN_MS;
+ } else {
+ throw new IllegalArgumentException("Unsupported time duration unit: " + unitStr);
+ }
+
+ // Calculate canonical value in milliseconds.
+ this.milliseconds = (long) Math.round(value * multiplier);
+ }
+
+ /**
+ * Returns the time duration in canonical units (milliseconds).
+ *
+ * @return the duration in milliseconds.
+ */
+ public long getMilliseconds() {
+ return milliseconds;
+ }
+
+ @Override
+ public Object value() {
+ // Return the canonical value.
+ return milliseconds;
+ }
+
+ @Override
+ public TokenType type() {
+ return tokenType;
+ }
+
+ @Override
+ public JsonElement toJson() {
+ // Construct a JSON representation of this token.
+ JsonObject obj = new JsonObject();
+ obj.addProperty("token", token);
+ obj.addProperty("type", tokenType.name());
+ obj.addProperty("milliseconds", milliseconds);
+ return obj;
+ }
+}
diff --git a/wrangler-api/src/main/java/io/cdap/wrangler/api/parser/TokenType.java b/wrangler-api/src/main/java/io/cdap/wrangler/api/parser/TokenType.java
index 8c93b0e6a..4c8b7515a 100644
--- a/wrangler-api/src/main/java/io/cdap/wrangler/api/parser/TokenType.java
+++ b/wrangler-api/src/main/java/io/cdap/wrangler/api/parser/TokenType.java
@@ -152,5 +152,19 @@ public enum TokenType implements Serializable {
* Represents the enumerated type for the object of type {@code String} with restrictions
* on characters that can be present in a string.
*/
- IDENTIFIER
+ IDENTIFIER,
+
+ /**
+ * Represents the enumerated type for a byte size token.
+ * This type is associated with tokens representing byte sizes, such as "10KB" or "1.5MB".
+ */
+ BYTE_SIZE,
+
+ /**
+ * Represents the enumerated type for a time duration token.
+ * This type is associated with tokens representing time durations, such as "150ms" or "2.1s".
+ */
+ TIME_DURATION
+
+
}
diff --git a/wrangler-core/.vscode/settings.json b/wrangler-core/.vscode/settings.json
new file mode 100644
index 000000000..7b016a89f
--- /dev/null
+++ b/wrangler-core/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "java.compile.nullAnalysis.mode": "automatic"
+}
\ No newline at end of file
diff --git a/wrangler-core/pom.xml b/wrangler-core/pom.xml
index e2dcb3c2b..37d230e0a 100644
--- a/wrangler-core/pom.xml
+++ b/wrangler-core/pom.xml
@@ -313,52 +313,58 @@
-
-
- src/main/resources
- true
-
-
-
-
- org.antlr
- antlr4-maven-plugin
- ${antlr4-maven-plugin.version}
-
- true
-
-
-
-
- antlr4
-
-
-
-
-
- org.codehaus.mojo
- buildnumber-maven-plugin
- 1.0
-
-
- generate-resources
-
- create
-
-
-
-
- true
- false
- false
- {0,date,yyyy-MM-dd-HH:mm:ss}_{1}
-
- - timestamp
- - ${user.name}
-
-
-
-
-
+ src/main/java
+ src/test/java
+
+
+
+ src/main/resources
+ true
+
+
+
+
+
+ org.antlr
+ antlr4-maven-plugin
+ ${antlr4-maven-plugin.version}
+
+ true
+
+
+
+
+ antlr4
+
+
+
+
+
+
+ org.codehaus.mojo
+ buildnumber-maven-plugin
+ 1.0
+
+
+ generate-resources
+
+ create
+
+
+
+
+ true
+ false
+ false
+ {0,date,yyyy-MM-dd-HH:mm:ss}_{1}
+
+ - timestamp
+ - ${user.name}
+
+
+
+
+
+
diff --git a/wrangler-core/src/main/antlr4/io/cdap/wrangler/parser/Directives.g4 b/wrangler-core/src/main/antlr4/io/cdap/wrangler/parser/Directives.g4
index 7c517ed6a..1eb323d04 100644
--- a/wrangler-core/src/main/antlr4/io/cdap/wrangler/parser/Directives.g4
+++ b/wrangler-core/src/main/antlr4/io/cdap/wrangler/parser/Directives.g4
@@ -65,27 +65,27 @@ directive
| numberRanges
| properties
)*?
- ;
+ ;
ifStatement
: ifStat elseIfStat* elseStat? '}'
- ;
+ ;
ifStat
: 'if' expression '{' statements
- ;
+ ;
elseIfStat
: '}' 'else' 'if' expression '{' statements
- ;
+ ;
elseStat
: '}' 'else' '{' statements
- ;
+ ;
expression
: '(' (~'(' | expression)* ')'
- ;
+ ;
forStatement
: 'for' '(' Identifier '=' expression ';' expression ';' expression ')' '{' statements '}'
@@ -140,7 +140,12 @@ numberRange
;
value
- : String | Number | Column | Bool
+ : String
+ | Number
+ | Column
+ | Bool
+ | BYTE_SIZE // Accepts values like "10KB", "1.5MB"
+ | TIME_DURATION // Accepts values like "150ms", "2.1s"
;
ecommand
@@ -195,7 +200,6 @@ identifierList
: Identifier (',' Identifier)*
;
-
/*
* Following are the Lexer Rules used for tokenizing the recipe.
*/
@@ -247,7 +251,6 @@ BackSlash: '\\';
Dollar : '$';
Tilde : '~';
-
Bool
: 'true'
| 'false'
@@ -280,20 +283,19 @@ EscapeSequence
| OctalEscape
;
-fragment
-OctalEscape
+fragment OctalEscape
: '\\' ('0'..'3') ('0'..'7') ('0'..'7')
| '\\' ('0'..'7') ('0'..'7')
| '\\' ('0'..'7')
;
-fragment
-UnicodeEscape
+fragment UnicodeEscape
: '\\' 'u' HexDigit HexDigit HexDigit HexDigit
;
-fragment
- HexDigit : ('0'..'9'|'a'..'f'|'A'..'F') ;
+fragment HexDigit
+ : ('0'..'9'|'a'..'f'|'A'..'F')
+ ;
Comment
: ('//' ~[\r\n]* | '/*' .*? '*/' | '--' ~[\r\n]* ) -> skip
@@ -303,6 +305,23 @@ Space
: [ \t\r\n\u000C]+ -> skip
;
+// New lexer rules for parsing byte sizes and time durations
+
+BYTE_SIZE
+ : [0-9]+ ('.' [0-9]+)? BYTE_UNIT
+ ;
+fragment BYTE_UNIT
+ : (('K'|'k'|'M'|'m'|'G'|'g'|'T'|'t')? ('B'|'b'))
+ ;
+
+TIME_DURATION
+ : [0-9]+ ('.' [0-9]+)? TIME_UNIT
+ ;
+fragment TIME_UNIT
+ : ('ms'|'MS'|'s'|'S')
+ ;
+
+// Existing fragments for numbers
fragment Int
: '-'? [1-9] Digit* [L]*
| '0'
diff --git a/wrangler-core/src/main/java/io/cdap/wrangler/expression/ELContext.java b/wrangler-core/src/main/java/io/cdap/wrangler/expression/ELContext.java
index 04b0b884b..e98fe70e4 100644
--- a/wrangler-core/src/main/java/io/cdap/wrangler/expression/ELContext.java
+++ b/wrangler-core/src/main/java/io/cdap/wrangler/expression/ELContext.java
@@ -91,7 +91,6 @@ public ELContext(ExecutorContext context, EL el, Row row) {
set("this", row);
}
- @Nullable
private void init(ExecutorContext context) {
if (context != null) {
// Adds the transient store variables.
@@ -166,3 +165,5 @@ public boolean has(String name) {
return values.containsKey(name);
}
}
+
+
diff --git a/wrangler-core/src/main/java/io/cdap/wrangler/parser/RecipeVisitor.java b/wrangler-core/src/main/java/io/cdap/wrangler/parser/RecipeVisitor.java
index ac35e7a5e..4eb53e1d4 100644
--- a/wrangler-core/src/main/java/io/cdap/wrangler/parser/RecipeVisitor.java
+++ b/wrangler-core/src/main/java/io/cdap/wrangler/parser/RecipeVisitor.java
@@ -38,6 +38,9 @@
import org.antlr.v4.runtime.misc.Interval;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
+import io.cdap.wrangler.api.parser.ByteSize;
+import io.cdap.wrangler.api.parser.TimeDuration;
+
import java.util.ArrayList;
import java.util.HashMap;
@@ -326,4 +329,26 @@ private SourceInfo getOriginalSource(ParserRuleContext ctx) {
int column = ctx.getStart().getCharPositionInLine();
return new SourceInfo(lineno, column, text);
}
+
+ @Override
+ public RecipeSymbol.Builder visitValue(DirectivesParser.ValueContext ctx) {
+ // 1) check for a byte‐size literal
+ if (ctx.BYTE_SIZE() != null) {
+ String text = ctx.BYTE_SIZE().getText(); // e.g. "10KB"
+ ByteSize tok = new ByteSize(text);
+ builder.addToken(tok);
+ return builder;
+ }
+
+ // 2) check for a time‐duration literal
+ if (ctx.TIME_DURATION() != null) {
+ String text = ctx.TIME_DURATION().getText(); // e.g. "150ms"
+ TimeDuration tok = new TimeDuration(text);
+ builder.addToken(tok);
+ return builder;
+ }
+
+ // 3) otherwise, fall back to the existing handlers
+ return super.visitValue(ctx);
+ }
}
diff --git a/wrangler-core/src/main/resources/schemas/manifest.json b/wrangler-core/src/main/resources/schemas/manifest.json
index 664b9600b..08c7a2364 100644
--- a/wrangler-core/src/main/resources/schemas/manifest.json
+++ b/wrangler-core/src/main/resources/schemas/manifest.json
@@ -2,7 +2,7 @@
"standards": {
"hl7-fhir-r4": {
"format": "json",
- "hash": "2721123ac666b321ce9833627c8bb712c62b36d326934767fc6c9aea701ce549"
+ "hash": "5d4d99755becc037880850debc447a1c9a5cfdba3d6f0512d470817433bd486d"
}
}
}
diff --git a/wrangler-core/src/test/java/io/cdap/directives/parser/JsPathTest.java b/wrangler-core/src/test/java/io/cdap/directives/parser/JsPathTest.java
index 599a5762b..5ac97af3a 100644
--- a/wrangler-core/src/test/java/io/cdap/directives/parser/JsPathTest.java
+++ b/wrangler-core/src/test/java/io/cdap/directives/parser/JsPathTest.java
@@ -70,13 +70,14 @@ public void testJSONFunctions() throws Exception {
);
String[] directives = new String[] {
- "set-column body json:Parse(body)",
- "set-column s0 json:Select(body, '$.name.fname', '$.name.lname')",
- "set-column s1 json:Select(body, '$.name.fname')",
- "set-column s11 json:Select(body, '$.numbers')",
- "set-column s2 json:Select(body, '$.numbers')",
- "set-column s6 json:ArrayLength(json:Select(body, '$.numbers'))"
+ "set-column parsedBody json:Parse(body)",
+ "set-column s0 json:Select(parsedBody, '$.name.fname', '$.name.lname')",
+ "set-column s1 json:Select(parsedBody, '$.name.fname')",
+ "set-column s11 json:Select(parsedBody, '$.numbers')",
+ "set-column s2 json:Select(parsedBody, '$.numbers')",
+ "set-column s6 json:ArrayLength(json:Select(parsedBody, '$.numbers'))"
};
+
rows = TestingRig.execute(directives, rows);
diff --git a/wrangler-core/src/test/java/io/cdap/directives/validation/ValidateStandardTest.java b/wrangler-core/src/test/java/io/cdap/directives/validation/ValidateStandardTest.java
index fac05025e..87dc4b008 100644
--- a/wrangler-core/src/test/java/io/cdap/directives/validation/ValidateStandardTest.java
+++ b/wrangler-core/src/test/java/io/cdap/directives/validation/ValidateStandardTest.java
@@ -14,155 +14,158 @@
* the License.
*/
-package io.cdap.directives.validation;
+ package io.cdap.directives.validation;
+
+ import com.google.gson.Gson;
+ import com.google.gson.JsonObject;
+ import io.cdap.wrangler.TestingRig;
+ import io.cdap.wrangler.api.Row;
+ import io.cdap.wrangler.utils.Manifest;
+ import io.cdap.wrangler.utils.Manifest.Standard;
+ import org.apache.commons.io.FilenameUtils;
+ import org.apache.commons.io.IOUtils;
+ import org.junit.Test;
+
+ import java.io.File;
+ import java.io.FileInputStream;
+ import java.io.IOException;
+ import java.io.InputStream;
+ import java.io.InputStreamReader;
+ import java.net.URISyntaxException;
+ import java.security.CodeSource;
+ import java.security.MessageDigest;
+ import java.security.NoSuchAlgorithmException;
+ import java.util.Arrays;
+ import java.util.Formatter;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.Map;
+
+ import static org.junit.Assert.assertEquals;
+ import static org.junit.Assert.assertTrue;
+
+ /**
+ * Tests for ValidateStandard and the manifest and schemas in the package.
+ */
+ public class ValidateStandardTest {
+
+ private static Map getSpecsInArchive()
+ throws IOException, NoSuchAlgorithmException, URISyntaxException {
+ Map schemas = new HashMap<>();
+ CodeSource src = ValidateStandard.class.getProtectionDomain().getCodeSource();
+ if (src != null) {
+ File jarDir = new File(src.getLocation().toURI());
+ File schemasRoot = new File(jarDir, ValidateStandard.SCHEMAS_RESOURCE_PATH);
+
+ if (!schemasRoot.isDirectory()) {
+ throw new IOException(
+ String.format("Schemas root %s was not a directory", schemasRoot.getPath()));
+ }
+
+ for (File f : schemasRoot.listFiles()) {
+ if (f.toPath().endsWith(ValidateStandard.MANIFEST_PATH)) {
+ continue;
+ }
+
+ String hash = calcHash(new FileInputStream(f));
+ schemas.put(
+ FilenameUtils.getBaseName(f.getName()),
+ new Standard(hash, FilenameUtils.getExtension(f.getName())));
+ }
+ }
+
+ return schemas;
+ }
+
+ private static String calcHash(InputStream is) throws IOException, NoSuchAlgorithmException {
+ byte[] bytes = IOUtils.toByteArray(is);
+ MessageDigest d = MessageDigest.getInstance("SHA-256");
+ byte[] hash = d.digest(bytes);
+
+ Formatter f = new Formatter();
+ for (byte b : hash) {
+ f.format("%02x", b);
+ }
+ return f.toString();
+ }
+
+ private static InputStream readResource(String name) throws IOException {
+ InputStream resourceStream = ValidateStandard.class.getClassLoader().getResourceAsStream(name);
+
+ if (resourceStream == null) {
+ throw new IOException(String.format("Can't read/find resource %s", name));
+ }
+
+ return resourceStream;
+ }
+
+ @Test
+ public void testValidation() throws Exception {
+ JsonObject badJson =
+ new Gson()
+ .fromJson("{\"resourceType\": \"Patient\", \"active\": \"meow\"}", JsonObject.class);
+ JsonObject goodJson =
+ new Gson()
+ .fromJson(
+ "{\"resourceType\": \"Patient\", \"active\": true, \"gender\": \"female\"}",
+ JsonObject.class);
+
+ String[] directives = new String[]{
+ "validate-standard :col1 hl7-fhir-r4",
+ };
+
+ List rows = Arrays.asList(
+ new Row("col1", badJson),
+ new Row("col1", goodJson)
+ );
+
+ List actual = TestingRig.execute(directives, rows);
+
+ assertEquals(1, actual.size());
+ assertEquals(goodJson, actual.get(0).getValue(0));
+ }
+
+ /**
+ * This test verifies that the manifest in the resources matches up with both the actual schemas in the resources as
+ * well as the implementations provided to handle those schemas.
+ */
+ @Test
+ public void verifyManifest() throws Exception {
+ InputStream manifestStream = readResource(ValidateStandard.MANIFEST_PATH);
+ Manifest manifest =
+ new Gson().getAdapter(Manifest.class).fromJson(new InputStreamReader(manifestStream));
+
+ Map declaredSpecs = manifest.getStandards();
+ Map actualSpecs = getSpecsInArchive();
+
+ assertEquals(
+ "Manifest contains different number of specs than there are in the artifact",
+ declaredSpecs.size(),
+ actualSpecs.size());
+
+ for (String spec : declaredSpecs.keySet()) {
+ assertTrue(
+ String.format("Manifest had spec %s but the artifact did not", spec),
+ actualSpecs.containsKey(spec));
+
+ Standard declared = declaredSpecs.get(spec);
+ Standard actual = actualSpecs.get(spec);
+
+ assertEquals(
+ String.format(
+ "Declared standard %s did not match actual %s",
+ declared.toString(), actual.toString()),
+ declared,
+ actual);
+
+ assertTrue(
+ String.format(
+ "Standard %s does not have a handler/factory registered in %s",
+ spec, ValidateStandard.class.getName()),
+ ValidateStandard.FORMAT_TO_FACTORY.containsKey(actual.getFormat()));
+ }
+ }
+ }
+
+
-import com.google.gson.Gson;
-import com.google.gson.JsonObject;
-import io.cdap.wrangler.TestingRig;
-import io.cdap.wrangler.api.Row;
-import io.cdap.wrangler.utils.Manifest;
-import io.cdap.wrangler.utils.Manifest.Standard;
-import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.io.IOUtils;
-import org.junit.Test;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.nio.file.Paths;
-import java.security.CodeSource;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-import java.util.Formatter;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-/**
- * Tests for ValidateStandard and the manifest and schemas in the package.
- */
-public class ValidateStandardTest {
-
- private static Map getSpecsInArchive()
- throws IOException, NoSuchAlgorithmException {
- Map schemas = new HashMap<>();
- CodeSource src = ValidateStandard.class.getProtectionDomain().getCodeSource();
- if (src != null) {
- File schemasRoot =
- Paths.get(src.getLocation().getPath(), ValidateStandard.SCHEMAS_RESOURCE_PATH).toFile();
-
- if (!schemasRoot.isDirectory()) {
- throw new IOException(
- String.format("Schemas root %s was not a directory", schemasRoot.getPath()));
- }
-
- for (File f : schemasRoot.listFiles()) {
- if (f.toPath().endsWith(ValidateStandard.MANIFEST_PATH)) {
- continue;
- }
-
- String hash = calcHash(new FileInputStream(f));
- schemas.put(
- FilenameUtils.getBaseName(f.getName()),
- new Standard(hash, FilenameUtils.getExtension(f.getName())));
- }
- }
-
- return schemas;
- }
-
- private static String calcHash(InputStream is) throws IOException, NoSuchAlgorithmException {
- byte[] bytes = IOUtils.toByteArray(is);
- MessageDigest d = MessageDigest.getInstance("SHA-256");
- byte[] hash = d.digest(bytes);
-
- Formatter f = new Formatter();
- for (byte b : hash) {
- f.format("%02x", b);
- }
- return f.toString();
- }
-
- private static InputStream readResource(String name) throws IOException {
- InputStream resourceStream = ValidateStandard.class.getClassLoader().getResourceAsStream(name);
-
- if (resourceStream == null) {
- throw new IOException(String.format("Can't read/find resource %s", name));
- }
-
- return resourceStream;
- }
-
- @Test
- public void testValidation() throws Exception {
- JsonObject badJson =
- new Gson()
- .fromJson("{\"resourceType\": \"Patient\", \"active\": \"meow\"}", JsonObject.class);
- JsonObject goodJson =
- new Gson()
- .fromJson(
- "{\"resourceType\": \"Patient\", \"active\": true, \"gender\": \"female\"}",
- JsonObject.class);
-
- String[] directives = new String[]{
- "validate-standard :col1 hl7-fhir-r4",
- };
-
- List rows = Arrays.asList(
- new Row("col1", badJson),
- new Row("col1", goodJson)
- );
-
- List actual = TestingRig.execute(directives, rows);
-
- assertEquals(1, actual.size());
- assertEquals(goodJson, actual.get(0).getValue(0));
- }
-
- /**
- * This test verifies that the manifest in the resources matches up with both the actual schemas in the resources as
- * well as the implementations provided to handle those schemas.
- */
- @Test
- public void verifyManifest() throws Exception {
- InputStream manifestStream = readResource(ValidateStandard.MANIFEST_PATH);
- Manifest manifest =
- new Gson().getAdapter(Manifest.class).fromJson(new InputStreamReader(manifestStream));
-
- Map declaredSpecs = manifest.getStandards();
- Map actualSpecs = getSpecsInArchive();
-
- assertEquals(
- "Manifest contains different number of specs than there are in the artifact",
- declaredSpecs.size(),
- actualSpecs.size());
-
- for (String spec : declaredSpecs.keySet()) {
- assertTrue(
- String.format("Manifest had spec %s but the artifact did not", spec),
- actualSpecs.containsKey(spec));
-
- Standard declared = declaredSpecs.get(spec);
- Standard actual = actualSpecs.get(spec);
-
- assertEquals(
- String.format(
- "Declared standard %s did not match actual %s",
- declared.toString(), actual.toString()),
- declared,
- actual);
-
- assertTrue(
- String.format(
- "Standard %s does not have a handler/factory registered in %s",
- spec, ValidateStandard.class.getName()),
- ValidateStandard.FORMAT_TO_FACTORY.containsKey(actual.getFormat()));
- }
- }
-}