Skip to content

Commit

Permalink
Merge pull request #4 from plantbreeding/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
daveneti authored Jun 12, 2024
2 parents 4a87c58 + c2b1e5f commit c428c70
Show file tree
Hide file tree
Showing 71 changed files with 1,502 additions and 1,311 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/publish-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ name: Publish package to the Maven Central Repository
on:
release:
types: [created]
push:
branches:
- main
workflow_dispatch:
jobs:
publish:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
}

group = 'org.brapi'
version = '0.0.1'
version = '0.2.0-SNAPSHOT'
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17

Expand Down
11 changes: 6 additions & 5 deletions java/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ publishing {
repositories {
maven {
name = "OSSRH"
url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
if(project.version.endsWith('-SNAPSHOT')) {
url = "https://s01.oss.sonatype.org/content/repositories/snapshots/"
} else {
url = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
}

credentials {
username = System.getenv("MAVEN_USERNAME")
password = System.getenv("MAVEN_PASSWORD")
Expand Down Expand Up @@ -78,10 +83,6 @@ project.plugins.withType(MavenPublishPlugin).all {
name = "${project.group}:${project.name}"
description = name
url = "https://github.com/plantbreeding/brapi-schema-tools"
autoReleaseAfterClose = true
properties = [
autoReleaseAfterClose: true
]
licenses {
license {
name = "MIT License"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@
import static java.util.Collections.singletonList;
import static org.brapi.schematools.core.response.Response.fail;
import static org.brapi.schematools.core.response.Response.success;
import static org.brapi.schematools.core.utils.StringUtils.toSingular;

/**
* Utility class for reading BrAPI JSON Schema.
*/
@AllArgsConstructor
public class BrAPISchemaReader {
private static final Pattern REF_PATTERN = Pattern.compile("((?:\\.{1,2}+/)*(?:[\\w-]+\\/)*(?:\\w+).json)?#\\/\\$defs\\/(\\w+)");
Expand All @@ -45,83 +49,148 @@ public class BrAPISchemaReader {
private final JsonSchemaFactory factory;
private final ObjectMapper objectMapper;

/**
* Creates schema reader with a basic {@link ObjectMapper} and V202012 JSonSchema version
*/
public BrAPISchemaReader() {
factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
objectMapper = new ObjectMapper();
}

/**
* Reads the schema module directories within a parent directory.
* Reads the schema module directories within a parent directory, and validates between schemas.
* Each directory in the parent directory is a module and the JSON schemas in the directories are object types
*
* @param schemaDirectory the parent directory that holds all the module directories
* @return a list of BrAPIClass with one type per JSON Schema
* @return a response containing a list of BrAPIClass with one type per JSON Schema or validation errors
* @throws BrAPISchemaReaderException if there is a problem reading the directories or JSON schemas
*/
public List<BrAPIClass> readDirectories(Path schemaDirectory) throws BrAPISchemaReaderException {
public Response<List<BrAPIClass>> readDirectories(Path schemaDirectory) throws BrAPISchemaReaderException {
try {
return dereferenceAllOfType(find(schemaDirectory, 3, this::schemaPathMatcher).flatMap(this::createBrAPISchemas).collect(Response.toList()).
getResultOrThrow(response -> new RuntimeException(response.getMessagesCombined(","))));
} catch (IOException | RuntimeException e) {
return dereferenceAndValidate(find(schemaDirectory, 3, this::schemaPathMatcher).flatMap(this::createBrAPISchemas).collect(Response.toList())) ;
} catch (RuntimeException | IOException e) {
throw new BrAPISchemaReaderException(e);
}
}

/**
* Reads a single object type from an JSON schema. If the JSON schema
* contain more than one type definition only the first is returned
* contain more than one type definition only the first is returned. There is
* no validation of referenced schemas
*
* @param schemaPath a JSON schema file
* @param module the module in which the object resides
* @return the BrAPIClass for this schema
* @return a response containing the BrAPIClass for this schema or validation errors
* @throws BrAPISchemaReaderException if there is a problem reading the JSON schema
*/
public BrAPIClass readSchema(Path schemaPath, String module) throws BrAPISchemaReaderException {
public Response<BrAPIClass> readSchema(Path schemaPath, String module) throws BrAPISchemaReaderException {
try {
return createBrAPISchemas(schemaPath, module).collect(Response.toList()).mapResult(list -> list.get(0)).
getResultOrThrow(response -> new RuntimeException(response.getMessagesCombined(",")));
return createBrAPISchemas(schemaPath, module).collect(Response.toList()).mapResult(list -> list.get(0)) ;
} catch (RuntimeException e) {
throw new BrAPISchemaReaderException(e);
}
}

/**
* Reads a single object type from an JSON schema string. If the JSON schema
* contain more than one type definition only the first is returned
* contain more than one type definition only the first is returned. There is
* no validation of referenced schemas
*
* @param path the path of the schema is used to check references, if not supplied then validation is not performed
* @param schema a JSON schema string
* @param module the module in which the object resides
* @return the BrAPIType for this schema
* @return a response containing the BrAPIClass for this schema or validation errors
* @throws BrAPISchemaReaderException if there is a problem reading the JSON schema
*/
public BrAPIClass readSchema(Path path, String schema, String module) throws BrAPISchemaReaderException {
public Response<BrAPIClass> readSchema(Path path, String schema, String module) throws BrAPISchemaReaderException {
try {
return createBrAPISchemas(path, objectMapper.readTree(schema), module).collect(Response.toList()).mapResult(list -> list.get(0)).
getResultOrThrow(response -> new RuntimeException(response.getMessagesCombined(",")));
} catch (RuntimeException | JsonProcessingException e) {
throw new BrAPISchemaReaderException(e);
return createBrAPISchemas(path, objectMapper.readTree(schema), module).collect(Response.toList()).mapResult(list -> list.get(0)) ;
} catch (RuntimeException| JsonProcessingException e) {
throw new BrAPISchemaReaderException(String.format("Can not read schema at '%s' in module '%s' from '%s', due to '%s'", path, module, schema, e.getMessage()), e);
}
}

private List<BrAPIClass> dereferenceAllOfType(List<BrAPIClass> types) {
private Response<List<BrAPIClass>> dereferenceAndValidate(Response<List<BrAPIClass>> types) {

return types.mapResult(this::dereference).mapResultToResponse(this::validate) ;
}

private List<BrAPIClass> dereference(List<BrAPIClass> types) {

Map<String, BrAPIType> typeMap = types.stream().collect(Collectors.toMap(BrAPIType::getName, Function.identity()));

List<BrAPIClass> objectTypes = new ArrayList<>() ;
List<BrAPIClass> brAPIClasses = new ArrayList<>() ;

types.forEach(type -> {
if (type instanceof BrAPIAllOfType brAPIAllOfType) {
objectTypes.add(BrAPIObjectType.builder().
brAPIClasses.add(BrAPIObjectType.builder().
name(brAPIAllOfType.getName()).
description(brAPIAllOfType.getDescription()).
module(brAPIAllOfType.getModule()).
properties(extractProperties(new ArrayList<>(), brAPIAllOfType, typeMap)).build()) ;
} else {
objectTypes.add(type) ;
brAPIClasses.add(type) ;
}
});

return objectTypes ;
return brAPIClasses ;
}

private Response<List<BrAPIClass>> validate(List<BrAPIClass> brAPIClasses) {
Map<String, BrAPIClass> classesMap = brAPIClasses.stream().collect(Collectors.toMap(BrAPIType::getName, Function.identity()));

return brAPIClasses.stream().map(brAPIClass -> validateType(classesMap, brAPIClass).mapResult(t -> (BrAPIClass)t)).collect(Response.toList()) ;
}

private Response<BrAPIType> validateType(final Map<String, BrAPIClass> classesMap, BrAPIType brAPIType) {

if (brAPIType instanceof BrAPIAllOfType brAPIAllOfType) {
return fail(Response.ErrorType.VALIDATION, String.format("Can not BrAPIAllOfType '%s' was not de-referenced", brAPIAllOfType.getName())) ;
} else if (brAPIType instanceof BrAPIOneOfType brAPIOneOfType) {
return brAPIOneOfType.getPossibleTypes().stream().map(possibleType -> validateType(classesMap, possibleType)).collect(Response.toList()).
merge(success(brAPIType)) ;
} else if (brAPIType instanceof BrAPIObjectType brAPIObjectType) {
return brAPIObjectType.getProperties().stream().map(property -> validateProperty(classesMap, brAPIObjectType, property)).collect(Response.toList()).
merge(success(brAPIType)) ;
} else {
return success(brAPIType) ;
}
}

private Response<BrAPIObjectProperty> validateProperty(Map<String, BrAPIClass> classesMap, BrAPIObjectType brAPIObjectType, BrAPIObjectProperty property) {
if (property.getReferencedAttribute() != null) {

BrAPIType type = unwrapType(property.getType());

BrAPIClass referencedType = classesMap.get(type.getName()) ;

if (referencedType == null) {
return Response.fail(Response.ErrorType.VALIDATION,
String.format("Property '%s' in type '%s' has a Referenced Attribute '%s', but the referenced type '%s' is not available",
property.getName(), brAPIObjectType.getName(), property.getReferencedAttribute(), property.getType().getName()));
}

if (referencedType instanceof BrAPIObjectType referencedObjectType) {
if (referencedObjectType.getProperties().stream().noneMatch(childProperty -> property.getReferencedAttribute().equals(childProperty.getName()))) {
return Response.fail(Response.ErrorType.VALIDATION, String.format("Property '%s' in type '%s' has a Referenced Attribute '%s', but the property does not exist in the referenced type '%s'",
property.getName(), brAPIObjectType.getName(), property.getReferencedAttribute(), referencedType.getName()));
}
} else {
return Response.fail(Response.ErrorType.VALIDATION,
String.format("Property '%s' in type '%s' has a Referenced Attribute '%s', but the referenced type '%s' is not a BrAPIObjectType",
property.getName(), brAPIObjectType.getName(), property.getReferencedAttribute(), referencedType.getName()));
}
}

return Response.success(property) ;
}

private BrAPIType unwrapType(BrAPIType type) {
if (type instanceof BrAPIArrayType brAPIArrayType) {
return unwrapType(brAPIArrayType.getItems()) ;
}

return type ;
}

private List<BrAPIObjectProperty> extractProperties(List<BrAPIObjectProperty> properties, BrAPIType brAPIType, Map<String, BrAPIType> typeMap) {
Expand All @@ -146,9 +215,9 @@ private Stream<Response<BrAPIClass>> createBrAPISchemas(Path path) {
}

private String findModule(Path path) {
String module = path.getParent().getFileName().toString();
String module = path != null ? path.getParent().getFileName().toString() : null;

return COMMON_MODULES.contains(module) ? null : module;
return module != null && COMMON_MODULES.contains(module) ? null : module;
}

private Stream<Response<BrAPIClass>> createBrAPISchemas(Path path, String module) {
Expand Down Expand Up @@ -205,7 +274,7 @@ private Response<BrAPIType> createType(Path path, JsonNode jsonNode, String fall
if (isEnum) {
return fail(Response.ErrorType.VALIDATION, String.format("Object Type '%s' can not be an enum!", fallbackName));
} else {
return createObjectType(path, jsonNode, findNameFromTitle(jsonNode).getResultIfPresentOrElseResult(fallbackName), module, isRequestPath(path));
return createObjectType(path, jsonNode, findNameFromTitle(jsonNode).getResultIfPresentOrElseResult(fallbackName), module);
}
}

Expand Down Expand Up @@ -255,10 +324,6 @@ private Response<BrAPIType> createType(Path path, JsonNode jsonNode, String fall

}

private boolean isRequestPath(Path path) {
return path.getParent().getFileName().endsWith("Requests") ;
}

private Response<String> findNameFromTitle(JsonNode jsonNode) {
return findString(jsonNode, "title", false).mapResult(name -> name != null ? name.replace(" ", "") : null);
}
Expand Down Expand Up @@ -296,16 +361,15 @@ private Response<BrAPIType> createArrayType(Path path, JsonNode jsonNode, String
BrAPIArrayType.BrAPIArrayTypeBuilder builder = BrAPIArrayType.builder().name(name);

return findChildNode(jsonNode, "items", true).
mapResultToResponse(childNode -> createType(path, childNode, String.format("%sItem", name), module).
mapResultToResponse(childNode -> createType(path, childNode, toSingular(name), module).
onSuccessDoWithResult(builder::items)).
map(() -> success(builder.build()));
}

private Response<BrAPIType> createObjectType(Path path, JsonNode jsonNode, String name, String module, boolean request) {
private Response<BrAPIType> createObjectType(Path path, JsonNode jsonNode, String name, String module) {

BrAPIObjectType.BrAPIObjectTypeBuilder builder = BrAPIObjectType.builder().
name(name).
request(request).
module(module);

findString(jsonNode, "description", false).
Expand Down Expand Up @@ -343,16 +407,24 @@ private Response<BrAPIObjectProperty> createProperty(Path path, JsonNode jsonNod
findString(jsonNode, "description", false).
onSuccessDoWithResult(builder::description);

findString(jsonNode, "referencedAttribute", false).
onSuccessDoWithResult(builder::referencedAttribute);

return createType(path, jsonNode, StringUtils.toSentenceCase(name), module).
onSuccessDoWithResult(builder::type).
mapOnCondition(jsonNode.has("relationshipType"), () -> findString(jsonNode, "relationshipType", true).
mapResultToResponse(BrAPIRelationshipType::fromNameOrLabel).
onSuccessDoWithResult(builder::relationshipType)).
map(() -> success(builder.build()));
}

private Response<BrAPIMetadata> parseMetadata(JsonNode metadata) {
BrAPIMetadata.BrAPIMetadataBuilder builder = BrAPIMetadata.builder();

return findBoolean(metadata, "primaryModel", false).
return findBoolean(metadata, "primaryModel", false, false).
onSuccessDoWithResult(builder::primaryModel).
merge(findBoolean(metadata, "request", false, false)).
onSuccessDoWithResult(builder::request).
map(() -> success(builder.build()));
}

Expand Down Expand Up @@ -452,15 +524,15 @@ private Response<String> findString(JsonNode parentNode, String fieldName, boole
});
}

private Response<Boolean> findBoolean(JsonNode parentNode, String fieldName, boolean required) {
private Response<Boolean> findBoolean(JsonNode parentNode, String fieldName, boolean required, boolean defaultValue) {
return findChildNode(parentNode, fieldName, required).mapResultToResponse(jsonNode -> {
if (jsonNode instanceof BooleanNode booleanNode) {
return success(booleanNode.asBoolean());
}
return required ?
fail(Response.ErrorType.VALIDATION,
String.format("Child node type '%s' was not BooleanNode with field name '%s' for parent node '%s'", jsonNode.getClass().getName(), parentNode, fieldName)) :
Response.empty();
Response.success(defaultValue);
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
package org.brapi.schematools.core.brapischema;

/**
* Exception thrown during the reading of BrAPI JSON Schema
*/
public class BrAPISchemaReaderException extends Exception {
public BrAPISchemaReaderException(Exception e) {
super(e);
/** Constructs a new exception with the specified cause and a detail message of
* (cause==null ? null : cause.toString()) (which typically contains the class and detail message of cause).
* This constructor is useful for exceptions that are little more than wrappers for other throwables
* (for example, java.security.PrivilegedActionException).
* @param cause the cause (which is saved for later retrieval by the {@link #getCause} method).
* (A null value is permitted, and indicates that the cause is nonexistent or unknown.)
*/
public BrAPISchemaReaderException(Exception cause) {
super(cause) ;
}

/** Constructs a new exception with the specified detail message and cause.
* Note that the detail message associated with cause is not automatically incorporated in this exception's detail message.
* @param message – the detail message (which is saved for later retrieval by the getMessage() method).
* @param cause – the cause (which is saved for later retrieval by the getCause() method).
* (A null value is permitted, and indicates that the cause is nonexistent or unknown.)
*/
public BrAPISchemaReaderException(String message, Exception cause) {
super(message, cause) ;
}
}
Loading

0 comments on commit c428c70

Please sign in to comment.