Skip to content

Commit

Permalink
RMB-253: JSON schema and RAML multiple interface implementation (#326)
Browse files Browse the repository at this point in the history
  • Loading branch information
wwelling authored and adamdickmeiss committed Nov 21, 2018
1 parent eeac8df commit c2c4fa1
Show file tree
Hide file tree
Showing 11 changed files with 635 additions and 19 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ See the file ["LICENSE"](LICENSE) for more information.
* [Securing DB Configuration file](#securing-db-configuration-file)
* [Foreign keys constraint](#foreign-keys-constraint)
* [Tenant API](#tenant-api)
* [RAMLs API](#ramls-api)
* [JSON Schemas API](#json-schemas-api)
* [Query Syntax](#query-syntax)
* [Metadata](#metadata)
* [Facet Support](#facet-support)
Expand Down Expand Up @@ -1086,6 +1088,54 @@ The `Criteria` object which generates `where` clauses can also receive a JSON Sc
Criteria idCrit = new Criteria("ramls/schemas/userdata.json");
```
## RAMLs API
The RAMLs API is a multiple interface which affords RMB modules to expose their RAML files in a machine readable way. To enable the interface the module must add the following to the provides array of its module descriptor:
```JSON
{
"id": "_ramls",
"version": "1.0",
"interfaceType" : "multiple",
"handlers" : [
{
"methods" : [ "GET" ],
"pathPattern" : "/_/ramls"
}
]
}
```
The interface has a signle GET endpoint with an optional query parameter path. Without the path query parameter the response will be an application/json array of the available RAMLs. This will be the immediate RAMLs the module provides. If the query parameter path is provided it will return the RAML at the path if exists. The RAML will have HTTP resolvable references. These references are either to JSON Schemas or RAMLs the module provides or shared JSON Schemas and RAMLs. The shared JSON Schemas and RAMLs are included in each module via a git submodule under the path `raml_util`. These paths are resolvable using the path query parameter.
The RAML defining the API:
https://github.com/folio-org/raml/blob/eda76de6db681076212e20c7f988c3913764b9b0/ramls/ramls.raml
## JSON Schemas API
The JSON Schemas API is a multiple interface which affords RMB modules to expose their JSON Schema files in a machine readable way. To enable the interface the module must add the following to the provides array of its module descriptor:
```JSON
{
"id": "_jsonSchemas",
"version": "1.0",
"interfaceType" : "multiple",
"handlers" : [
{
"methods" : [ "GET" ],
"pathPattern" : "/_/jsonSchemas"
}
]
}
```
The interface has a signle GET endpoint with an optional query parameter path. Without the path query parameter the response will be an application/json array of the available JSON Schemas. This will be the immediate JSON Schemas the module provides. If the query parameter path is provided it will return the JSON Schema at the path if exists. The JSON Schema will have HTTP resolvable references. These references are either to JSON Schemas or RAMLs the module provides or shared JSON Schemas and RAMLs. The shared JSON Schemas and RAMLs are included in each module via a git submodule under the path `raml_util`. These paths are resolvable using the path query parameter.
The RAML defining the API:
https://github.com/folio-org/raml/blob/eda76de6db681076212e20c7f988c3913764b9b0/ramls/jsonSchemas.raml
## Query Syntax
The RMB can receive parameters of different types. Modules can declare a query parameter and receive it as a string parameter in the generated API functions.
Expand Down
15 changes: 15 additions & 0 deletions domain-models-interface-extensions/ramls/object.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"_id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"name"
]
}
2 changes: 1 addition & 1 deletion domain-models-interface-extensions/ramls/test.raml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#%RAML 1.0

title: Jobs API
title: Tests API
baseUri: http://localhost:8081/v1
version: v1

Expand Down
9 changes: 9 additions & 0 deletions domain-models-interface-extensions/ramls/test.schema
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
},
"name": {
"type": "string"
},
"objects": {
"description": "List of objects",
"type": "array",
"id": "objects",
"items": {
"type": "object",
"$ref": "object.json"
}
}
},
"required": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package org.folio.rest.tools;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.io.IOCase;
import org.folio.rest.tools.plugins.CustomTypeAnnotator;
import org.folio.rest.tools.utils.RamlDirCopier;
import org.jsonschema2pojo.AnnotationStyle;
Expand All @@ -23,14 +30,17 @@
*/
public class GenerateRunner {

public static final String SOURCES_DEFAULT = "ramls";
public static final String RAML_LIST = "raml.list";
public static final String JSON_SCHEMA_LIST = "json-schema.list";

static {
System.setProperty(LoggerFactory.LOGGER_DELEGATE_FACTORY_CLASS_NAME, "io.vertx.core.logging.Log4j2LogDelegateFactory");
}

static final Logger log = LoggerFactory.getLogger(GenerateRunner.class);

private static final String MODEL_PACKAGE_DEFAULT = "org.folio.rest.jaxrs.model";
private static final String SOURCES_DEFAULT = "ramls";
private static final String RESOURCE_DEFAULT = "target/classes";

private String outputDirectory = null;
Expand Down Expand Up @@ -98,31 +108,22 @@ public static void main(String[] args) throws Exception {
CustomTypeAnnotator.setCustomFields(System.getProperties().getProperty("jsonschema.customfield"));

String [] ramlFiles = System.getProperty("raml_files", SOURCES_DEFAULT).split(",");
String pre = ramlFiles[0];
File x0 = new File(pre);
File x1 = x0;
while (true) {
x1 = x1.getParentFile();
if (x1 == null) {
break;
} else {
if (x1.getName().equals(SOURCES_DEFAULT)) {
x0 = x1;
}
}
}
File input = rebase(ramlFiles[0]);
File output = new File(root + File.separator + RESOURCE_DEFAULT + File.separator + SOURCES_DEFAULT);
String input = x0.getAbsolutePath();

log.info("copying ramls from source directory at: " + input);
log.info("copying ramls to target directory at: " + output);
RamlDirCopier.copy(x0.toPath(), output.toPath());
RamlDirCopier.copy(input.toPath(), output.toPath());

for (String d : ramlFiles) {
File tmp = new File(d);
String a = tmp.getAbsolutePath();
a = a.replace(input, output.getAbsolutePath());
a = a.replace(input.getAbsolutePath(), output.getAbsolutePath());
generateRunner.generate(a);
}

createLookupList(output, RAML_LIST, ".raml");
createLookupList(output, JSON_SCHEMA_LIST, ".json", ".schema");
}

/**
Expand Down Expand Up @@ -175,4 +176,33 @@ public void generate(String inputDirectory) throws Exception {
log.info("processed: " + numMatches + " raml files");
}

public static void createLookupList(File directory, String name, String...suffixes) throws IOException {
File listFile = new File(directory.getAbsolutePath() + File.separator + name);
Path path = Paths.get(directory.getAbsolutePath(), name);
try (BufferedWriter bw = Files.newBufferedWriter(path)) {
for (File file: directory.listFiles((FileFilter) new SuffixFileFilter(suffixes, IOCase.INSENSITIVE))) {
log.info("lookup entry: " + file.getName());
bw.write(file.getName());
bw.newLine();
}
}
log.info("lookup list file created: " + listFile.getAbsolutePath());
}

private static File rebase(String path) {
File input = new File(path);
File temp = input;
while (true) {
temp = temp.getParentFile();
if (temp == null) {
break;
} else {
if (temp.getName().equals(SOURCES_DEFAULT)) {
input = temp;
}
}
}
return input;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.folio.util.IoUtil;
import org.folio.util.ResourceUtil;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
Expand Down Expand Up @@ -40,6 +48,7 @@ public static void restoreJaxrs() throws IOException {
jaxrsBak.renameTo(jaxrs); // ignore any error
}

@After
@Before
public void cleanDir() throws IOException {
ClientGenerator.makeCleanDir(baseDir);
Expand Down Expand Up @@ -126,4 +135,25 @@ public void defaultRamlFilesDir() throws Exception {
assertTest();
}

@Test
public void testCreateRamlsLookupList() throws Exception {
List<String> actualRamls = testCreateLookupList(GenerateRunner.RAML_LIST, ".raml");
Assert.assertThat(actualRamls, containsInAnyOrder("test.raml"));
}

@Test
public void testCreateJsonSchemasLookupList() throws Exception {
List<String> actualJsonSchemas = testCreateLookupList(GenerateRunner.JSON_SCHEMA_LIST, ".json", ".schema");
Assert.assertThat(actualJsonSchemas, containsInAnyOrder("test.schema", "object.json"));
}

private List<String> testCreateLookupList(String filename, String...exts) throws IOException {
File src = new File(userDir + "/ramls/");
assertTrue(src.exists() && src.isDirectory());
File dest = new File(userDir + "/target/ramls/");
FileUtils.copyDirectory(src, dest);
GenerateRunner.createLookupList(dest, filename, exts);
return Arrays.asList(ResourceUtil.asString("ramls/" + filename).split("\\r?\\n"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package org.folio.rest.impl;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.ws.rs.core.Response;

import org.folio.rest.annotations.Validate;
import org.folio.rest.jaxrs.resource.JsonSchemas;
import org.folio.rest.tools.GenerateRunner;
import org.folio.util.ResourceUtil;

import io.vertx.core.AsyncResult;
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;

public class JsonSchemasAPI implements JsonSchemas {

private static final Logger log = LoggerFactory.getLogger(JsonSchemasAPI.class);

private static final Pattern REF_MATCH_PATTERN = Pattern.compile("\\\"\\$ref\\\"\\s*:\\s*\\\"(.*?)\\\"");

private static final String OKAPI_URL_HEADER = "x-okapi-url";
private static final String RAMLS_PATH = System.getProperty("raml_files", GenerateRunner.SOURCES_DEFAULT) + File.separator;
private static final String HASH_TAG = "#";

private static final List<String> JSON_SCHEMAS = getJsonSchemasList();

@Validate
@Override
public void getJsonSchemas(
String path,
Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> asyncResultHandler,
Context vertxContext
) {
vertxContext.runOnContext(v -> {
try {
if (path == null) {
List<String> schemas = getSchemas();
asyncResultHandler.handle(
Future.succeededFuture(
GetJsonSchemasResponse.respond200WithApplicationJson(schemas)
)
);
} else {
String okapiUrl = okapiHeaders.get(OKAPI_URL_HEADER);
String schema = getJsonSchemaByPath(path, okapiUrl);
if (schema != null) {
asyncResultHandler.handle(
Future.succeededFuture(
GetJsonSchemasResponse.respond200WithApplicationSchemaJson(schema)
)
);
} else {
String notFoundMessage = "Schema " + path + " not found";
asyncResultHandler.handle(
Future.succeededFuture(
GetJsonSchemasResponse.respond404WithTextPlain(notFoundMessage)
)
);
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
asyncResultHandler.handle(
Future.succeededFuture(
GetJsonSchemasResponse.respond500WithTextPlain(e.getMessage())
)
);
}
});
}

private static List<String> getJsonSchemasList() {
try {
return Arrays.asList(ResourceUtil.asString(RAMLS_PATH + GenerateRunner.JSON_SCHEMA_LIST).split("\\r?\\n"));
} catch (IOException e) {
log.warn("Unable to get JSON Schemas list!", e);
return new ArrayList<>();
}
}

private List<String> getSchemas() {
return JSON_SCHEMAS;
}

private String getJsonSchemaByPath(String path, String okapiUrl) {
try {
return replaceReferences(ResourceUtil.asString(RAMLS_PATH + path), okapiUrl);
} catch (IOException e) {
return null;
}
}

String replaceReferences(String schema, String okapiUrl) {
Matcher matcher = REF_MATCH_PATTERN.matcher(schema);
StringBuffer sb = new StringBuffer(schema.length());
while (matcher.find()) {
String path = matcher.group(1);
if (!path.startsWith(HASH_TAG)) {
if (path.contains(RAMLS_PATH)) {
path = path.substring(path.lastIndexOf(RAMLS_PATH) + RAMLS_PATH.length());
}
matcher.appendReplacement(sb, Matcher.quoteReplacement("\"$ref\":\"" + okapiUrl + "/_/jsonSchemas?path=" + path + "\""));
}
}
matcher.appendTail(sb);
return sb.toString();
}

}
Loading

0 comments on commit c2c4fa1

Please sign in to comment.