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 16 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
187 changes: 133 additions & 54 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 All @@ -27,6 +28,7 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

Expand Down Expand Up @@ -63,18 +65,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 +78,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 +91,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 +110,119 @@ 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) {
Optional.of(customCDEvent)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not so sure about the use of Optional here instead of a null and validation check

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah make sense

.filter(event -> validateCustomCDEvent(event, validateContextSchema))
.orElseThrow(() -> {
return new CDEventsException("Custom CDEvent validation failed.");
});
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);
}
return true;
}

private static Set<ValidationMessage> getJsonSchemaValidationMessages(CDEvent cdEvent) {
/**
* 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) {
return Optional.ofNullable(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.

Same as previous comment on Optional

.map(schemaUri -> {
log.info("Validate custom CDEvent against context.schemaUri - {}", customCDEvent.customSchemaUri());
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
JsonSchema jsonSchema = factory.getSchema(schemaUri);
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;
}).orElseThrow(() -> {
log.error("Context schemaUri does not exist, required for custom schema validation.");
return new CDEventsException("Context schemaUri does not exist.");
}
);
}

/**
* 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,26 +245,27 @@ 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) {
String versionedEventType = rootNode.get("context").get("type").asText();
if (versionedEventType.startsWith(CDEventConstants.EVENT_PREFIX)) {
String[] type = versionedEventType.split("\\.");
String subject = type[CDEventConstants.EVENT_SUBJECT_INDEX];
String predicate = type[CDEventConstants.EVENT_PREDICATE_INDEX];
unVersionedEventType = 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;
return Optional.ofNullable(rootNode.get("context"))
.flatMap(context -> Optional.ofNullable(context.get("type"))
.map(cType -> {
String vType = cType.asText();
if (vType.startsWith(CDEventConstants.EVENT_PREFIX)) {
String[] type = vType.split("\\.");
String subject = type[CDEventConstants.EVENT_SUBJECT_INDEX];
String predicate = type[CDEventConstants.EVENT_PREDICATE_INDEX];
return CDEventConstants.EVENT_PREFIX + subject + "." + predicate + ".";
} else {
throw new CDEventsException("Invalid CDEvent type found in CDEvent Json " + vType);
}
})).orElseThrow(() -> {
log.error("Unable to find context and type in CDEvent Json");
return new CDEventsException("Unable to find context and type in CDEvent Json");
});

} 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
Loading