diff --git a/helper/helper/build.gradle b/helper/helper/build.gradle index 0602a12e9..4ae19fbaf 100644 --- a/helper/helper/build.gradle +++ b/helper/helper/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation project(":helper:impls:helper-impl-18") implementation project(":helper:impls:helper-impl-19") implementation project(":helper:impls:helper-impl-110") + implementation "org.apache.commons:commons-text:1.9" } shadowJar { @@ -37,6 +38,9 @@ shadowJar { //do not pack slf4j exclude(dependency("org.slf4j:slf4j-api:.*")) } + + //relocate commons-text to not conflict with anything that might be on a user's classpath + relocate 'org.apache.commons', 'com.linkedin.avroutil1.compatibility.shaded.org.apache.commons' } //"fat" sources jar diff --git a/helper/helper/src/main/java/com/linkedin/avroutil1/compatibility/AvroCompatibilityHelper.java b/helper/helper/src/main/java/com/linkedin/avroutil1/compatibility/AvroCompatibilityHelper.java index eda0d9903..5a4003d0e 100644 --- a/helper/helper/src/main/java/com/linkedin/avroutil1/compatibility/AvroCompatibilityHelper.java +++ b/helper/helper/src/main/java/com/linkedin/avroutil1/compatibility/AvroCompatibilityHelper.java @@ -31,6 +31,7 @@ import org.apache.avro.io.JsonDecoder; import org.apache.avro.io.JsonEncoder; import org.apache.avro.specific.SpecificRecord; +import org.apache.commons.text.StringEscapeUtils; /** @@ -649,10 +650,73 @@ public static Schema.Field createSchemaField(String name, Schema schema, String * @return field property value as json literal, or null if no such property */ public static String getFieldPropAsJsonString(Schema.Field field, String propName) { - return ADAPTER.getFieldPropAsJsonString(field, propName); + return getFieldPropAsJsonString(field, propName, true, false); } + /** + * returns the value of the specified field prop as a json literal. + * returns null if the field has no such property. + * optionally returns string literals as "naked" strings. also optionally unescapes any nested json + * inside such nested strings + * @param field the field who's property value we wish to get + * @param propName the name of the property + * @param quoteStringValues true to return string literals quoted. false to strip such quotes. false matches avro behaviour + * @param unescapeInnerJson true to unescape inner json inside string literals. true matches avro behaviour + * @return field property value as json literal, or null if no such property + */ + public static String getFieldPropAsJsonString(Schema.Field field, String propName, boolean quoteStringValues, boolean unescapeInnerJson) { + String value = ADAPTER.getFieldPropAsJsonString(field, propName); + return unquoteAndEscapeStringProp(value, quoteStringValues, unescapeInnerJson); + } + + /** + * returns the value of the specified schema prop as a json literal. + * returns null if the schema has no such property. + * note that string values are returned quoted (as a proper json string literal) + * @param schema the schema who's property value we wish to get + * @param propName the name of the property + * @return schema property value as json literal, or null if no such property + */ public static String getSchemaPropAsJsonString(Schema schema, String propName) { - return ADAPTER.getSchemaPropAsJsonString(schema, propName); + return getSchemaPropAsJsonString(schema, propName, true, false); + } + + /** + * returns the value of the specified schema prop as a json literal. + * returns null if the schema has no such property. + * optionally returns string literals as "naked" strings. also optionally unescapes any nested json + * inside such nested strings + * @param schema the schema who's property value we wish to get + * @param propName the name of the property + * @param quoteStringValues true to return string literals quoted. false to strip such quotes. false matches avro behaviour + * @param unescapeInnerJson true to unescape inner json inside string literals. true matches avro behaviour + * @return schema property value as json literal, or null if no such property + */ + public static String getSchemaPropAsJsonString(Schema schema, String propName, boolean quoteStringValues, boolean unescapeInnerJson) { + String value = ADAPTER.getSchemaPropAsJsonString(schema, propName); + return unquoteAndEscapeStringProp(value, quoteStringValues, unescapeInnerJson); + } + + private static String unquoteAndEscapeStringProp(String maybeAStringProp, boolean quoteStringValues, boolean unescapeInnerJson) { + if (maybeAStringProp == null) { + //no such prop + return null; + } + if (quoteStringValues && !unescapeInnerJson) { + //no changes actually required + return maybeAStringProp; + } + boolean isAStringValue = maybeAStringProp.startsWith("\"") && maybeAStringProp.endsWith("\""); + if (!isAStringValue) { + return maybeAStringProp; + } + String processed = maybeAStringProp; + if (!quoteStringValues) { + processed = processed.substring(1, processed.length() - 1); //remove enclosing quotes + } + if (unescapeInnerJson) { + processed = StringEscapeUtils.unescapeJson(processed); + } + return processed; } } diff --git a/helper/tests/helper-tests-allavro/src/test/java/com/linkedin/avroutil1/compatibility/SchemaBuilderTest.java b/helper/tests/helper-tests-allavro/src/test/java/com/linkedin/avroutil1/compatibility/SchemaBuilderTest.java index 0787b9d9d..46408f413 100644 --- a/helper/tests/helper-tests-allavro/src/test/java/com/linkedin/avroutil1/compatibility/SchemaBuilderTest.java +++ b/helper/tests/helper-tests-allavro/src/test/java/com/linkedin/avroutil1/compatibility/SchemaBuilderTest.java @@ -41,5 +41,9 @@ public void testSchemaPropSupportOnClone() throws Exception { Assert.assertEquals(AvroCompatibilityHelper.getSchemaPropAsJsonString(newSchema, "schemaNullProp"), "null"); Assert.assertEquals(AvroCompatibilityHelper.getSchemaPropAsJsonString(newSchema, "schemaBoolProp"), "false"); Assert.assertEquals(AvroCompatibilityHelper.getSchemaPropAsJsonString(newSchema, "schemaObjectProp"), "{\"e\":\"f\",\"g\":\"h\"}"); + + Assert.assertEquals(AvroCompatibilityHelper.getSchemaPropAsJsonString(newSchema, "schemaNestedJsonProp"), "\"{\\\"innerKey\\\" : \\\"innerValue\\\"}\""); + Assert.assertEquals(AvroCompatibilityHelper.getSchemaPropAsJsonString(newSchema, "schemaNestedJsonProp", false, false), "{\\\"innerKey\\\" : \\\"innerValue\\\"}"); + Assert.assertEquals(AvroCompatibilityHelper.getSchemaPropAsJsonString(newSchema, "schemaNestedJsonProp", false, true), "{\"innerKey\" : \"innerValue\"}"); } } diff --git a/helper/tests/helper-tests-common/src/main/resources/RecordWithFieldProps.avsc b/helper/tests/helper-tests-common/src/main/resources/RecordWithFieldProps.avsc index 8b06010b5..000795ad7 100644 --- a/helper/tests/helper-tests-common/src/main/resources/RecordWithFieldProps.avsc +++ b/helper/tests/helper-tests-common/src/main/resources/RecordWithFieldProps.avsc @@ -15,7 +15,8 @@ "objectProp": { "a": "b", "c": "d" - } + }, + "nestedJsonProp": "{\"innerKey\" : \"innerValue\"}" }, { "name": "intField", @@ -30,5 +31,6 @@ "schemaObjectProp": { "e": "f", "g": "h" - } + }, + "schemaNestedJsonProp": "{\"innerKey\" : \"innerValue\"}" } \ No newline at end of file