CAMEL-10744: Added utility APIs in Salesforce JsonUtils to generate JSON schema from SObjectDescription, added required modules to Karaf camel-salesforce feature
Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/e0fe1df7 Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/e0fe1df7 Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/e0fe1df7 Branch: refs/heads/camel-2.19.x Commit: e0fe1df75a383ebea78a40125d0d55654ce4f31e Parents: 6f9ed73 Author: Dhiraj Bokde <dhira...@yahoo.com> Authored: Thu Aug 3 00:12:19 2017 -0700 Committer: Dhiraj Bokde <dhira...@yahoo.com> Committed: Thu Aug 3 01:13:01 2017 -0700 ---------------------------------------------------------------------- .../camel-salesforce-component/pom.xml | 15 ++ .../salesforce/api/utils/JsonUtils.java | 225 +++++++++++++++++++ .../salesforce/api/utils/JsonUtilsTest.java | 56 +++++ parent/pom.xml | 6 + .../features/src/main/resources/features.xml | 2 + 5 files changed, 304 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/e0fe1df7/components/camel-salesforce/camel-salesforce-component/pom.xml ---------------------------------------------------------------------- diff --git a/components/camel-salesforce/camel-salesforce-component/pom.xml b/components/camel-salesforce/camel-salesforce-component/pom.xml index b3b0214..f258da5 100644 --- a/components/camel-salesforce/camel-salesforce-component/pom.xml +++ b/components/camel-salesforce/camel-salesforce-component/pom.xml @@ -88,6 +88,15 @@ <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> + <!-- json schema --> + <dependency> + <groupId>com.fasterxml.jackson.jaxrs</groupId> + <artifactId>jackson-jaxrs-json-provider</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.module</groupId> + <artifactId>jackson-module-jsonSchema</artifactId> + </dependency> <dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> @@ -169,6 +178,12 @@ <version>2.2</version> <scope>test</scope> </dependency> + <dependency> + <groupId>com.squareup.okhttp3</groupId> + <artifactId>mockwebserver</artifactId> + <version>${squareup-okhttp3-version}</version> + <scope>test</scope> + </dependency> </dependencies> <build> http://git-wip-us.apache.org/repos/asf/camel/blob/e0fe1df7/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/utils/JsonUtils.java ---------------------------------------------------------------------- diff --git a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/utils/JsonUtils.java b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/utils/JsonUtils.java index 2c3a04f..959e00b 100644 --- a/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/utils/JsonUtils.java +++ b/components/camel-salesforce/camel-salesforce-component/src/main/java/org/apache/camel/component/salesforce/api/utils/JsonUtils.java @@ -16,13 +16,50 @@ */ package org.apache.camel.component.salesforce.api.utils; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.joining; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator; +import com.fasterxml.jackson.module.jsonSchema.types.ArraySchema; +import com.fasterxml.jackson.module.jsonSchema.types.BooleanSchema; +import com.fasterxml.jackson.module.jsonSchema.types.IntegerSchema; +import com.fasterxml.jackson.module.jsonSchema.types.NullSchema; +import com.fasterxml.jackson.module.jsonSchema.types.NumberSchema; +import com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema; +import com.fasterxml.jackson.module.jsonSchema.types.SimpleTypeSchema; +import com.fasterxml.jackson.module.jsonSchema.types.StringSchema; + +import org.apache.camel.component.salesforce.api.dto.AbstractDTOBase; +import org.apache.camel.component.salesforce.api.dto.AbstractQueryRecordsBase; +import org.apache.camel.component.salesforce.api.dto.Address; +import org.apache.camel.component.salesforce.api.dto.GeoLocation; +import org.apache.camel.component.salesforce.api.dto.PickListValue; +import org.apache.camel.component.salesforce.api.dto.SObjectDescription; +import org.apache.camel.component.salesforce.api.dto.SObjectField; +import org.apache.camel.impl.DefaultPackageScanClassResolver; /** * Factory class for creating {@linkplain com.fasterxml.jackson.databind.ObjectMapper} */ public abstract class JsonUtils { + + public static final String SCHEMA4 = "http://json-schema.org/draft-04/schema#"; + public static final String DEFAULT_ID_PREFIX = "urn:jsonschema:org:apache:camel:component:salesforce:dto"; + + private static final String API_DTO_ID = "org:urn:jsonschema:org:apache:camel:component:salesforce:api:dto"; + public static ObjectMapper createObjectMapper() { // enable date time support including Java 1.8 ZonedDateTime ObjectMapper objectMapper = new ObjectMapper(); @@ -31,4 +68,192 @@ public abstract class JsonUtils { return objectMapper; } + public static String getBasicApiJsonSchema() throws JsonProcessingException { + ObjectMapper mapper = createSchemaObjectMapper(); + JsonSchemaGenerator schemaGen = new JsonSchemaGenerator(mapper); + + DefaultPackageScanClassResolver packageScanClassResolver = new DefaultPackageScanClassResolver(); + + Set<Class<?>> schemaClasses = new HashSet<>(); + + // get non-abstract extensions of AbstractDTOBase + schemaClasses.addAll(packageScanClassResolver.findByFilter( + type -> !Modifier.isAbstract(type.getModifiers()) + && AbstractDTOBase.class.isAssignableFrom(type), + "org.apache.camel.component.salesforce.api.dto")); + + // get non-abstract extensions of AbstractDTOBase + schemaClasses.addAll(packageScanClassResolver.findByFilter( + type -> !Modifier.isAbstract(type.getModifiers()) + && AbstractDTOBase.class.isAssignableFrom(type), + "org.apache.camel.component.salesforce.api.dto")); + + Set<Object> allSchemas = new HashSet<>(); + for (Class<?> aClass : schemaClasses) { + JsonSchema jsonSchema = schemaGen.generateSchema(aClass); + allSchemas.add(jsonSchema); + } + + return getJsonSchemaString(mapper, allSchemas, API_DTO_ID); + } + + public static String getJsonSchemaString(ObjectMapper mapper, Set<Object> allSchemas, String id) throws JsonProcessingException { + ObjectSchema rootSchema = new ObjectSchema(); + rootSchema.set$schema(SCHEMA4); + rootSchema.setId(id); + rootSchema.setOneOf(allSchemas); + + return mapper.writeValueAsString(rootSchema); + } + + public static String getSObjectJsonSchema(SObjectDescription description) throws JsonProcessingException { + return getSObjectJsonSchema(description, true); + } + + public static String getSObjectJsonSchema(SObjectDescription description, boolean addQuerySchema) throws JsonProcessingException { + ObjectMapper schemaObjectMapper = createSchemaObjectMapper(); + return getJsonSchemaString(schemaObjectMapper, getSObjectJsonSchema(schemaObjectMapper, description, DEFAULT_ID_PREFIX, addQuerySchema), DEFAULT_ID_PREFIX); + } + + public static Set<Object> getSObjectJsonSchema(ObjectMapper objectMapper, SObjectDescription description, String idPrefix, boolean addQuerySchema) throws JsonProcessingException { + Set<Object> allSchemas = new HashSet<>(); + + // generate SObject schema from description + ObjectSchema sobjectSchema = new ObjectSchema(); + sobjectSchema.setId(idPrefix + ":" + description.getName()); + sobjectSchema.setTitle(description.getLabel()); + + SimpleTypeSchema addressSchema = null; + SimpleTypeSchema geoLocationSchema = null; + + for (SObjectField field : description.getFields()) { + + SimpleTypeSchema fieldSchema = new NullSchema(); + String soapType = field.getSoapType(); + + switch (soapType.substring(soapType.indexOf(':') + 1)) { + case "ID": // mapping for tns:ID SOAP type + case "string": + case "base64Binary": + // Salesforce maps any types like string, picklist, reference, etc. to string + case "anyType": + fieldSchema = new StringSchema(); + break; + + case "integer": + case "int": + case "long": + case "short": + case "byte": + case "unsignedInt": + case "unsignedShort": + case "unsignedByte": + fieldSchema = new IntegerSchema(); + break; + + case "decimal": + case "float": + case "double": + fieldSchema = new NumberSchema(); + break; + + case "boolean": + fieldSchema = new BooleanSchema(); + break; + + case "dateTime": + case "time": + case "date": + case "g": + fieldSchema = new StringSchema(); + ((StringSchema) fieldSchema).setFormat(JsonValueFormat.DATE_TIME); + break; + + case "address": + if (addressSchema == null) { + addressSchema = getSchemaFromClass(objectMapper, Address.class); + } + fieldSchema = addressSchema; + break; + + case "location": + if (geoLocationSchema == null) { + geoLocationSchema = getSchemaFromClass(objectMapper, GeoLocation.class); + } + fieldSchema = geoLocationSchema; + break; + + default: + throw new IllegalArgumentException("Unsupported type " + soapType); + } + + List<PickListValue> picklistValues = field.getPicklistValues(); + switch (field.getType()) { + case "picklist": + fieldSchema.asStringSchema().setEnums( + picklistValues == null ? Collections.EMPTY_SET : picklistValues.stream() + .map(PickListValue::getValue) + .distinct() + .collect(Collectors.toSet())); + break; + + case "multipicklist": + // TODO regex needs more work to not allow values not separated by ',' + fieldSchema.asStringSchema().setPattern(picklistValues == null ? "" : picklistValues.stream() + .map(val -> "(,?(" + val.getValue() + "))") + .distinct() + .collect(joining("|", "(", ")"))); + break; + + default: + // nothing to fix + } + + // additional field properties + fieldSchema.setTitle(field.getLabel()); + fieldSchema.setDefault(field.getDefaultValue()); + if (field.isUpdateable() != null) { + fieldSchema.setReadonly(!field.isUpdateable()); + } + + // add property to sobject schema + if (field.isNillable()) { + sobjectSchema.putOptionalProperty(field.getName(), fieldSchema); + } else { + sobjectSchema.putProperty(field.getName(), fieldSchema); + } + } + + // add sobject schema to root schema + allSchemas.add(sobjectSchema); + + if (addQuerySchema) { + // add a simple query schema for this sobject for lookups, etc. + ObjectSchema queryRecords = getSchemaFromClass(objectMapper, AbstractQueryRecordsBase.class); + queryRecords.setId(idPrefix + ":QueryRecords" + description.getName()); + + ObjectSchema refSchema = new ObjectSchema(); + refSchema.set$ref(sobjectSchema.getId()); + + ArraySchema recordsProperty = new ArraySchema(); + recordsProperty.setItems(new ArraySchema.SingleItems(refSchema)); + queryRecords.putProperty("records", recordsProperty); + + allSchemas.add(queryRecords); + } + + return allSchemas; + } + + public static ObjectMapper createSchemaObjectMapper() { + ObjectMapper objectMapper = createObjectMapper(); + objectMapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); + objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); + return objectMapper; + } + + private static ObjectSchema getSchemaFromClass(ObjectMapper objectMapper, Class<?> type) throws JsonMappingException { + return new JsonSchemaGenerator(objectMapper).generateSchema(type).asObjectSchema(); + } + } http://git-wip-us.apache.org/repos/asf/camel/blob/e0fe1df7/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/utils/JsonUtilsTest.java ---------------------------------------------------------------------- diff --git a/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/utils/JsonUtilsTest.java b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/utils/JsonUtilsTest.java new file mode 100644 index 0000000..cd59a9e --- /dev/null +++ b/components/camel-salesforce/camel-salesforce-component/src/test/java/org/apache/camel/component/salesforce/api/utils/JsonUtilsTest.java @@ -0,0 +1,56 @@ +package org.apache.camel.component.salesforce.api.utils; + +import org.apache.camel.component.salesforce.api.dto.SObjectDescription; +import org.apache.camel.component.salesforce.dto.generated.Account; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.types.ObjectSchema; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Unit test for {@link JsonUtils} + */ +public class JsonUtilsTest { + + public static final Logger LOG = LoggerFactory.getLogger(JsonUtilsTest.class); + + @Test + public void getBasicApiJsonSchema() throws Exception { + + // create basic api dto schema + LOG.info("Basic Api Schema..."); + String basicApiJsonSchema = JsonUtils.getBasicApiJsonSchema(); + LOG.info(basicApiJsonSchema); + + // parse schema to validate + ObjectMapper objectMapper = JsonUtils.createObjectMapper(); + JsonSchema jsonSchema = objectMapper.readValue(basicApiJsonSchema, JsonSchema.class); + assertTrue(jsonSchema.isObjectSchema()); + assertFalse(((ObjectSchema)jsonSchema).getOneOf().isEmpty()); + } + + @Test + public void getSObjectJsonSchema() throws Exception { + + // create sobject dto schema + SObjectDescription description = new Account().description(); + + LOG.info("SObject Schema..."); + String sObjectJsonSchema = JsonUtils.getSObjectJsonSchema(description); + LOG.info(sObjectJsonSchema); + + // parse schema to validate + ObjectMapper objectMapper = JsonUtils.createObjectMapper(); + JsonSchema jsonSchema = objectMapper.readValue(sObjectJsonSchema, JsonSchema.class); + assertTrue(jsonSchema.isObjectSchema()); + assertEquals(2, ((ObjectSchema)jsonSchema).getOneOf().size()); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/camel/blob/e0fe1df7/parent/pom.xml ---------------------------------------------------------------------- diff --git a/parent/pom.xml b/parent/pom.xml index 6714d0d..cfbc5e4 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -622,6 +622,7 @@ <spymemcached-bundle-version>2.5_2</spymemcached-bundle-version> <!-- FIXME cmueller: not in sync! --> <spymemcached-version>2.12.0</spymemcached-version> <squareup-okhttp-version>2.7.5</squareup-okhttp-version> + <squareup-okhttp3-version>3.8.1</squareup-okhttp3-version> <squareup-okhttp-bundle-version>2.7.5_1</squareup-okhttp-bundle-version> <squareup-okio-version>1.11.0</squareup-okio-version> <squareup-okio-bundle-version>1.11.0_1</squareup-okio-bundle-version> @@ -3457,6 +3458,11 @@ <artifactId>jackson-module-jaxb-annotations</artifactId> <version>${jackson2-version}</version> </dependency> + <dependency> + <groupId>com.fasterxml.jackson.module</groupId> + <artifactId>jackson-module-jsonSchema</artifactId> + <version>${jackson2-version}</version> + </dependency> <dependency> <groupId>io.netty</groupId> http://git-wip-us.apache.org/repos/asf/camel/blob/e0fe1df7/platforms/karaf/features/src/main/resources/features.xml ---------------------------------------------------------------------- diff --git a/platforms/karaf/features/src/main/resources/features.xml b/platforms/karaf/features/src/main/resources/features.xml index a6e2213..1f23fda 100644 --- a/platforms/karaf/features/src/main/resources/features.xml +++ b/platforms/karaf/features/src/main/resources/features.xml @@ -1574,6 +1574,8 @@ <bundle dependency='true'>mvn:com.fasterxml.jackson.core/jackson-core/${jackson2-version}</bundle> <bundle dependency='true'>mvn:com.fasterxml.jackson.core/jackson-annotations/${jackson2-version}</bundle> <bundle dependency='true'>mvn:com.fasterxml.jackson.core/jackson-databind/${jackson2-version}</bundle> + <bundle dependency='true'>mvn:com.fasterxml.jackson.core/jackson-jaxrs-json-provider/${jackson2-version}</bundle> + <bundle dependency='true'>mvn:com.fasterxml.jackson.core/jackson-module-jsonSchema/${jackson2-version}</bundle> <bundle dependency='true'>mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.xstream/${xstream-bundle-version}</bundle> <bundle dependency='true'>mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.xpp3/${xpp3-bundle-version}</bundle> <bundle dependency='true'>mvn:javax.servlet/javax.servlet-api/${javax.servlet-api-version}</bundle>