Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for creating Custom Event Types with schemas validation #83

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions generator/src/main/resources/template/event-template.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ public class {{capitalizedSubject}}{{capitalizedPredicate}}CDEvent extends {{cap
return "{{schemaFileName}}";
}

/**
*
* @return custom schema URI
*/
@Override
public URI customSchemaUri() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: the field name is schemaUri - perhaps the method name should match it for clarity?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changing it to contextSchemaUri()

return getContext().getSchemaUri();
}


/**
* @param source
Expand All @@ -127,8 +136,8 @@ public class {{capitalizedSubject}}{{capitalizedPredicate}}CDEvent extends {{cap
* Sets the {@link Context} chainId value
*/

public void setChainId(URI chainId) {
getContext().setChainId(chainId.toString());
public void setChainId(String chainId) {
getContext().setChainId(chainId);
}

/**
Expand Down
15 changes: 15 additions & 0 deletions sdk/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,21 @@
</resources>
</configuration>
</execution>
<execution>
<id>copy-custom-resources</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/schemas/dev/cdevents/custom</outputDirectory>
<resources>
<resource>
<directory>../spec/custom</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
Expand Down
151 changes: 110 additions & 41 deletions sdk/src/main/java/dev/cdevents/CDEvents.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.networknt.schema.ValidationMessage;
import dev.cdevents.config.CustomObjectMapper;
import dev.cdevents.constants.CDEventConstants;
import static dev.cdevents.constants.CDEventConstants.CUSTOM_SCHEMA_CLASSPATH;
import dev.cdevents.exception.CDEventsException;
import dev.cdevents.models.CDEvent;
import io.cloudevents.CloudEvent;
Expand Down Expand Up @@ -63,18 +64,8 @@ public static CloudEvent cdEventAsCloudEvent(CDEvent cdEvent) {
log.error("CDEvent validation failed against schema URL - {}", cdEvent.schemaURL());
throw new CDEventsException("CDEvent validation failed against schema URL - " + cdEvent.schemaURL());
}
String cdEventJson = cdEventAsJson(cdEvent);
log.info("CDEvent with type {} as json - {}", cdEvent.currentCDEventType(), cdEventJson);
try {
CloudEvent ceToSend = new CloudEventBuilder()
.withId(UUID.randomUUID().toString())
.withSource(new URI(cdEvent.eventSource()))
.withType(cdEvent.currentCDEventType())
.withDataContentType("application/json")
.withData(cdEventJson.getBytes(StandardCharsets.UTF_8))
.withTime(OffsetDateTime.now())
.build();
return ceToSend;
return buildCloudEvent(cdEvent);
} catch (URISyntaxException e) {
throw new CDEventsException("Exception occurred while building CloudEvent from CDEvent ", e);
}
Expand All @@ -86,8 +77,10 @@ public static CloudEvent cdEventAsCloudEvent(CDEvent cdEvent) {
* @return valid cdEvent
*/
public static boolean validateCDEvent(CDEvent cdEvent) {
Set<ValidationMessage> errors = getJsonSchemaValidationMessages(cdEvent);

Map<String, String> schemaMap = new HashMap<>();
schemaMap.put(cdEvent.schemaURL(), SCHEMA_CLASSPATH + cdEvent.schemaFileName());
schemaMap.put(cdEvent.baseURI() + "links/embeddedlinksarray", SCHEMA_CLASSPATH + "links/embeddedlinksarray.json");
Set<ValidationMessage> errors = getJsonSchemaValidationMessages(cdEvent, schemaMap);
if (!errors.isEmpty()) {
log.error("CDEvent validation failed with errors {}", errors);
return false;
Expand All @@ -97,17 +90,17 @@ public static boolean validateCDEvent(CDEvent cdEvent) {

/**
* Creates cdEvent from cdEventJson string and validates against schema.
* @param cdEventJson
* @return CDEvent, needs type casting to specific CDEvent class
* @param cdEventJson json string of any CDEvent type
* @return CDEvent needs type casting to specific CDEvent class
*/
public static CDEvent cdEventFromJson(String cdEventJson) {
if (!validateCDEventJson(cdEventJson)) {
throw new CDEventsException("CDEvent Json validation failed against schema");
}
String eventType = getUnVersionedEventTypeFromJson(cdEventJson);
CDEventConstants.CDEventTypes cdEventType = getCDEventTypeEnum(eventType);
try {
CDEvent cdEvent = (CDEvent) new ObjectMapper().readValue(cdEventJson, cdEventType.getEventClass());
CDEvent cdEvent = new ObjectMapper().readValue(cdEventJson, cdEventType.getEventClass());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we constructing a new object mapper every time?
Is this method only ever called once? Even so we probably should move it to a field

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this method using the existing APIs

if (!validateCDEvent(cdEvent)) {
throw new CDEventsException("CDEvent Json validation failed against schema");
}
return cdEvent;
} catch (JsonProcessingException e) {
log.error("Exception occurred while creating CDEvent from json {}", cdEventJson);
Expand All @@ -116,35 +109,114 @@ public static CDEvent cdEventFromJson(String cdEventJson) {
}

/**
* Validates the cdEventJson against the Schema URL.
* @param cdEventJson
* @return true, If cdEventJson is valid
* Creates a CloudEvent from the custom cdEvent.
* @param <T> customCDEvent class
* @param customCDEvent custom CDEvent class object of type <T>
* @param validateContextSchema true If validation needed against context.schemaUri
* @return CloudEvent
*/
public static boolean validateCDEventJson(String cdEventJson) {
String eventType = getUnVersionedEventTypeFromJson(cdEventJson);
CDEventConstants.CDEventTypes cdEventType = getCDEventTypeEnum(eventType);
public static <T extends CDEvent> CloudEvent customCDEventAsCloudEvent(T customCDEvent, boolean validateContextSchema) {
if (!validateCustomCDEvent(customCDEvent, validateContextSchema)) {
throw new CDEventsException("Custom CDEvent validation failed.");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not say why something has failed in the exception? We should probably create an issue for that if that's the case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the method with in the condition logs the reason for failures before the exception is thrown.

}
try {
CDEvent cdEvent = (CDEvent) new ObjectMapper().readValue(cdEventJson, cdEventType.getEventClass());
Set<ValidationMessage> errors = getJsonSchemaValidationMessages(cdEvent);
return buildCloudEvent(customCDEvent);
} catch (URISyntaxException e) {
throw new CDEventsException("Exception occurred while building CloudEvent from custom CDEvent ", e);
}
}

if (!errors.isEmpty()) {
log.error("CDEvent Json validation failed against schema URL {}", cdEvent.schemaURL());
log.error("CDEvent Json validation failed with errors {}", errors);
return false;
/**
* Creates customCDEvent from Json string and validates against context and official schemas.
* @param customCDEventJson Json string of customCDEvent class type <T>
* @param <T> customCDEvent class
* @param eventClass custom CDEvent class of type <T>
* @param validateContextSchema true If validation needed against context.schemaUri
* @return CDEvent
*/
public static <T extends CDEvent> T customCDEventFromJson(String customCDEventJson, Class<T> eventClass, boolean validateContextSchema) {
try {
T cdEvent = new ObjectMapper().readValue(customCDEventJson, eventClass);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comment here with the object mapper. We can just move this to a static field

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this method now and using it from existing code

if (!validateCustomCDEvent(cdEvent, validateContextSchema)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment above with the reasons something has failed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using an existing method now, and added more logging for the failure reason

throw new CDEventsException("Custom CDEvent validation failed.");
}
return cdEvent;
} catch (JsonProcessingException e) {
throw new CDEventsException("Exception occurred while validating CDEvent json with schema file ", e);
throw new CDEventsException("Exception occurred while processing cdEventJson with the event class " + eventClass.getName(), e);
}
}

/**
* Validates the custom CDEvent against the official and context schemas.
* @param customCDEvent custom CDEvent to validate
* @param validateContextSchema true to validate custom CDEvent against context schema
* @return valid cdEvent
*/
public static boolean validateCustomCDEvent(CDEvent customCDEvent, boolean validateContextSchema) {
if (validateContextSchema) {
return validateWithContextSchemaUri(customCDEvent) && validateWithOfficialCustomSchema(customCDEvent);
} else {
return validateWithOfficialCustomSchema(customCDEvent);
}
}

/**
* Validates the custom CDEvent against the provided context Schema URI.
* @param customCDEvent custom CDEvent to validate
* @return valid cdEvent
*/
public static boolean validateWithContextSchemaUri(CDEvent customCDEvent) {
if (customCDEvent.customSchemaUri() == null) {
log.error("Context schemaUri does not exist, required for custom schema validation.");
throw new CDEventsException("Context schemaUri does not exist.");
}
log.info("Validate custom CDEvent against context.schemaUri - {}", customCDEvent.customSchemaUri());
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
JsonSchema jsonSchema = factory.getSchema(customCDEvent.customSchemaUri());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should not promote a model where for every message the SDK reaches out to some internet location to fetch the schema. The way I was thinking of implementing this for the sdk-go is to let user register a custom schema, so that it's loaded once and re-used from memory, and then give users an option to decide what to do if the custom schemaURI is not in the in-memory schema cache, i.e. fail or try to download the schema, with the former as default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes agree, will do the required changes in a separate PR. Removed this implementation from current PR and created issue to work on #84

JsonNode jsonNode = objectMapper.convertValue(customCDEvent, ObjectNode.class);
Set<ValidationMessage> errors = jsonSchema.validate(jsonNode);
if (!errors.isEmpty()) {
log.error("Custom CDEvent validation failed against context.schemaUri - {}, with errors {}", customCDEvent.customSchemaUri(), errors);
return false;
}
return true;
}

private static Set<ValidationMessage> getJsonSchemaValidationMessages(CDEvent cdEvent) {
/**
* Validates the custom CDEvent against the official spec/custom/schema.json.
* @param customCDEvent
* @return valid cdEvent
*/
public static boolean validateWithOfficialCustomSchema(CDEvent customCDEvent) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Map<String, String> schemaMap = new HashMap<>();
schemaMap.put(cdEvent.schemaURL(), SCHEMA_CLASSPATH + cdEvent.schemaFileName());
schemaMap.put(cdEvent.baseURI() + "links/embeddedlinksarray", SCHEMA_CLASSPATH + "links/embeddedlinksarray.json");
schemaMap.put(customCDEvent.schemaURL(), CUSTOM_SCHEMA_CLASSPATH + "schema.json");
schemaMap.put(customCDEvent.baseURI() + "links/embeddedlinksarray", SCHEMA_CLASSPATH + "links/embeddedlinksarray.json");
log.info("Validate custom CDEvent against official spec/custom/schema.json");
Set<ValidationMessage> errors = getJsonSchemaValidationMessages(customCDEvent, schemaMap);
if (!errors.isEmpty()) {
log.error("Custom CDEvent validation failed against official spec/custom/schema.json, with errors {}", errors);
return false;
}
return true;
}

private static CloudEvent buildCloudEvent(CDEvent cdEvent) throws URISyntaxException {
String cdEventJson = cdEventAsJson(cdEvent);
log.info("CDEvent with type {} as json - {}", cdEvent.currentCDEventType(), cdEventJson);
Copy link

@xibz xibz Jul 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I wonder if this should be a debug log with the raw json in there. It seems overkill with it for info. How do you feel about changing this to log.debug for the JSON and having the log.info just having the CDEvent with type {}

return new CloudEventBuilder()
.withId(UUID.randomUUID().toString())
.withSource(new URI(cdEvent.eventSource()))
.withType(cdEvent.currentCDEventType())
.withDataContentType("application/json")
.withData(cdEventJson.getBytes(StandardCharsets.UTF_8))
.withTime(OffsetDateTime.now())
.build();
}

private static Set<ValidationMessage> getJsonSchemaValidationMessages(CDEvent cdEvent, Map<String, String> schemaMap) {
JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012, builder ->
// This creates a mapping from $id which starts with https://cdevents.dev/0.4.0/schema to the retrieval URI classpath:schema/
builder.schemaMappers(schemaMappers -> schemaMappers.mappings(schemaMap))
// This creates a mapping from $id which starts with https://cdevents.dev/0.4.0/schema to the retrieval URI classpath:schema/
builder.schemaMappers(schemaMappers -> schemaMappers.mappings(schemaMap))
);
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setPathType(PathType.JSON_POINTER);
Expand All @@ -167,7 +239,6 @@ private static CDEventConstants.CDEventTypes getCDEventTypeEnum(String eventType
}

private static String getUnVersionedEventTypeFromJson(String cdEventJson) {
String unVersionedEventType = "";
try {
JsonNode rootNode = objectMapper.readTree(cdEventJson);
if (rootNode.get("context") != null && rootNode.get("context").get("type") != null) {
Expand All @@ -176,17 +247,15 @@ private static String getUnVersionedEventTypeFromJson(String cdEventJson) {
String[] type = versionedEventType.split("\\.");
String subject = type[CDEventConstants.EVENT_SUBJECT_INDEX];
String predicate = type[CDEventConstants.EVENT_PREDICATE_INDEX];
unVersionedEventType = CDEventConstants.EVENT_PREFIX + subject + "." + predicate + ".";
return CDEventConstants.EVENT_PREFIX + subject + "." + predicate + ".";
} else {
throw new CDEventsException("Invalid CDEvent type found in CDEvent Json " + versionedEventType);
}
} else {
throw new CDEventsException("Unable to find context and type in CDEvent Json");
}
return unVersionedEventType;
} catch (JsonProcessingException e) {
throw new CDEventsException("Exception occurred while reading CDEvent Json for eventType ", e);
}

}
}
16 changes: 11 additions & 5 deletions sdk/src/main/java/dev/cdevents/constants/CDEventConstants.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.cdevents.constants;

import dev.cdevents.events.*;
import dev.cdevents.models.CDEvent;

import java.io.File;

Expand All @@ -24,6 +25,11 @@ private CDEventConstants() {
*/
public static final String SCHEMA_CLASSPATH = "classpath:dev/cdevents/spec/schemas/";

/**
* Custom Schema classpath location.
*/
public static final String CUSTOM_SCHEMA_CLASSPATH = "classpath:dev/cdevents/custom/";

/**
* Event link schemas location.
*/
Expand Down Expand Up @@ -300,9 +306,9 @@ public enum CDEventTypes {
*/
private String eventType;

private Class eventClass;
private Class<? extends CDEvent> eventClass;

CDEventTypes(final String event, final Class eventClass) {
CDEventTypes(final String event, final Class<? extends CDEvent> eventClass) {
this.eventType = event;
this.eventClass = eventClass;
}
Expand All @@ -324,14 +330,14 @@ public void setEventType(final String event) {
/**
* @return class name of the event type
*/
public Class getEventClass() {
return eventClass;
public Class<? extends CDEvent> getEventClass() {
return this.eventClass;
}

/**
* @param eventClass class name to set
*/
public void setEventClass(Class eventClass) {
public void setEventClass(Class<? extends CDEvent> eventClass) {
this.eventClass = eventClass;
}
}
Expand Down
13 changes: 11 additions & 2 deletions sdk/src/main/java/dev/cdevents/events/ArtifactDeletedCDEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ public String schemaFileName() {
return "artifactdeleted.json";
}

/**
*
* @return custom schema URI
*/
@Override
public URI customSchemaUri() {
return getContext().getSchemaUri();
}


/**
* @param source
Expand All @@ -124,8 +133,8 @@ public void setSource(URI source) {
* Sets the {@link Context} chainId value
*/

public void setChainId(URI chainId) {
getContext().setChainId(chainId.toString());
public void setChainId(String chainId) {
getContext().setChainId(chainId);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ public String schemaFileName() {
return "artifactdownloaded.json";
}

/**
*
* @return custom schema URI
*/
@Override
public URI customSchemaUri() {
return getContext().getSchemaUri();
}


/**
* @param source
Expand All @@ -124,8 +133,8 @@ public void setSource(URI source) {
* Sets the {@link Context} chainId value
*/

public void setChainId(URI chainId) {
getContext().setChainId(chainId.toString());
public void setChainId(String chainId) {
getContext().setChainId(chainId);
}

/**
Expand Down
Loading