diff --git a/globalization/Api.de.xml b/globalization/Api.de.xml
new file mode 100644
index 00000000..b188fa51
--- /dev/null
+++ b/globalization/Api.de.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/globalization/Api.en.xml b/globalization/Api.en.xml
new file mode 100644
index 00000000..3becbd6e
--- /dev/null
+++ b/globalization/Api.en.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/globalization/BlocksResource.de.xml b/globalization/BlocksResource.de.xml
new file mode 100644
index 00000000..b9610a74
--- /dev/null
+++ b/globalization/BlocksResource.de.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/globalization/BlocksResource.en.xml b/globalization/BlocksResource.en.xml
new file mode 100644
index 00000000..b87b35af
--- /dev/null
+++ b/globalization/BlocksResource.en.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
index 72da220b..b20f47ad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,6 +4,9 @@
org.qora
qora-core
2.0.0-SNAPSHOT
+
+ 3.19.0
+
src
@@ -15,6 +18,74 @@
1.8
+
+
+ maven-dependency-plugin
+
+
+ swagger ui
+ generate-resources
+
+ unpack
+
+
+
+
+ org.webjars
+ swagger-ui
+ ${swagger-ui.version}
+
+
+ ${project.build.directory}/swagger-ui.unpacked
+
+
+
+
+
+
+ com.google.code.maven-replacer-plugin
+ replacer
+ 1.5.3
+
+
+ generate-resources
+
+ replace
+
+
+
+
+ ${project.build.directory}/swagger-ui.unpacked/META-INF/resources/webjars/swagger-ui/${swagger-ui.version}/index.html
+
+
+ https://petstore.swagger.io/v2/swagger.json
+ /openapi.json
+
+
+
+
+
+
+ maven-resources-plugin
+ 3.1.0
+
+
+ copy-resources
+ generate-resources
+
+ copy-resources
+
+
+ ${project.build.directory}/classes/resources/swagger-ui
+
+
+ ${project.build.directory}/swagger-ui.unpacked/META-INF/resources/webjars/swagger-ui/${swagger-ui.version}
+
+
+
+
+
+
@@ -118,5 +189,35 @@
2.4.1
test
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.3.1
+
+
+ org.hamcrest
+ hamcrest-library
+ 1.3
+
+
+ org.glassfish.jersey.media
+ jersey-media-multipart
+ 2.27
+
+
+ javax.mail
+ mail
+ 1.5.0-b01
+
+
+ org.webjars
+ swagger-ui
+ ${swagger-ui.version}
+
+
+ org.eclipse.jetty
+ jetty-rewrite
+ 9.4.11.v20180605
+
\ No newline at end of file
diff --git a/src/Start.java b/src/Start.java
index 585f65c6..cbab3830 100644
--- a/src/Start.java
+++ b/src/Start.java
@@ -8,7 +8,7 @@ import repository.hsqldb.HSQLDBRepositoryFactory;
public class Start {
- private static final String connectionUrl = "jdbc:hsqldb:mem:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true";
+ private static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
public static void main(String args[]) throws DataException {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
@@ -19,7 +19,7 @@ public class Start {
//// testing the API client
//ApiClient client = ApiClient.getInstance();
- //String test = client.executeCommand("GET blocks/height");
+ //String test = client.executeCommand("GET blocks/first");
//System.out.println(test);
}
}
diff --git a/src/api/AddressesResource.java b/src/api/AddressesResource.java
new file mode 100644
index 00000000..df4570b8
--- /dev/null
+++ b/src/api/AddressesResource.java
@@ -0,0 +1,382 @@
+package api;
+
+import data.account.AccountData;
+import data.block.BlockData;
+import globalization.Translator;
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import java.math.BigDecimal;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import qora.account.Account;
+import qora.assets.Asset;
+import qora.crypto.Crypto;
+import repository.Repository;
+import repository.RepositoryManager;
+import utils.Base58;
+
+@Path("addresses")
+@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
+@OpenAPIDefinition(
+ extensions = @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="/Api/AddressesResource")
+ })
+)
+public class AddressesResource {
+
+ @Context
+ HttpServletRequest request;
+
+ private ApiErrorFactory apiErrorFactory;
+
+ public AddressesResource() {
+ this(new ApiErrorFactory(Translator.getInstance()));
+ }
+
+ public AddressesResource(ApiErrorFactory apiErrorFactory) {
+ this.apiErrorFactory = apiErrorFactory;
+ }
+
+ @GET
+ @Path("/lastreference/{address}")
+ @Operation(
+ description = "Returns the 64-byte long base58-encoded signature of last transaction where the address is delivered as creator. Or the first incoming transaction. Returns \"false\" if there is no transactions.",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET lastreference:address"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
+ })
+ },
+ responses = {
+ @ApiResponse(
+ description = "the base58-encoded transaction signature or \"false\"",
+ content = @Content(schema = @Schema(implementation = String.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
+ )
+ }
+ )
+ public String getLastReference(
+ @Parameter(description = "a base58-encoded address", required = true) @PathParam("address") String address
+ ) {
+ Security.checkApiCallAllowed("GET addresses/lastreference", request);
+
+ if (!Crypto.isValidAddress(address))
+ throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
+
+ byte[] lastReference = null;
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ Account account = new Account(repository, address);
+ account.getLastReference();
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
+
+ if(lastReference == null || lastReference.length == 0) {
+ return "false";
+ } else {
+ return Base58.encode(lastReference);
+ }
+ }
+
+ @GET
+ @Path("/lastreference/{address}/unconfirmed")
+ @Operation(
+ description = "Returns the 64-byte long base58-encoded signature of last transaction including unconfirmed where the address is delivered as creator. Or the first incoming transaction. Returns \\\"false\\\" if there is no transactions.",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET lastreference:address:unconfirmed"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
+ })
+ },
+ responses = {
+ @ApiResponse(
+ description = "the base58-encoded transaction signature",
+ content = @Content(schema = @Schema(implementation = String.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
+ )
+ }
+ )
+ public String getLastReferenceUnconfirmed(@PathParam("address") String address) {
+ Security.checkApiCallAllowed("GET addresses/lastreference", request);
+
+ // XXX: is this method needed?
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/validate/{address}")
+ @Operation(
+ description = "Validates the given address. Returns true/false.",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET validate:address"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ })
+ },
+ responses = {
+ @ApiResponse(
+ //description = "",
+ content = @Content(schema = @Schema(implementation = Boolean.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
+ )
+ }
+ )
+ public boolean validate(@PathParam("address") String address) {
+ Security.checkApiCallAllowed("GET addresses/validate", request);
+
+ return Crypto.isValidAddress(address);
+ }
+
+ @GET
+ @Path("/generatingbalance/{address}")
+ @Operation(
+ description = "Return the generating balance of the given address.",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET generatingbalance:address"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
+ })
+ },
+ responses = {
+ @ApiResponse(
+ description = "the generating balance",
+ content = @Content(schema = @Schema(implementation = BigDecimal.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
+ )
+ }
+ )
+ public BigDecimal getGeneratingBalanceOfAddress(@PathParam("address") String address) {
+ Security.checkApiCallAllowed("GET addresses/generatingbalance", request);
+
+ if (!Crypto.isValidAddress(address))
+ throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ Account account = new Account(repository, address);
+ return account.getGeneratingBalance();
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
+ }
+
+ @GET
+ @Path("balance/{address}")
+ @Operation(
+ description = "Returns the confirmed balance of the given address.",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET balance:address"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
+ })
+ },
+ responses = {
+ @ApiResponse(
+ description = "the balance",
+ content = @Content(schema = @Schema(implementation = BigDecimal.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
+ )
+ }
+ )
+ public BigDecimal getGeneratingBalance(@PathParam("address") String address) {
+ Security.checkApiCallAllowed("GET addresses/balance", request);
+
+ if (!Crypto.isValidAddress(address))
+ throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ Account account = new Account(repository, address);
+ return account.getConfirmedBalance(Asset.QORA);
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
+ }
+
+ @GET
+ @Path("assetbalance/{assetid}/{address}")
+ @Operation(
+ description = "Returns the confirmed balance of the given address for the given asset key.",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET assetbalance:assetid:address"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\", \"INVALID_ASSET_ID\"]", parseValue = true),
+ })
+ },
+ responses = {
+ @ApiResponse(
+ description = "the balance",
+ content = @Content(schema = @Schema(implementation = BigDecimal.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
+ )
+ }
+ )
+ public BigDecimal getAssetBalance(@PathParam("assetid") long assetid, @PathParam("address") String address) {
+ Security.checkApiCallAllowed("GET addresses/assetbalance", request);
+
+ if (!Crypto.isValidAddress(address))
+ throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ Account account = new Account(repository, address);
+ return account.getConfirmedBalance(assetid);
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
+ }
+
+ @GET
+ @Path("assets/{address}")
+ @Operation(
+ description = "Returns the list of assets for this address with balances.",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET assets:address"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
+ })
+ },
+ responses = {
+ @ApiResponse(
+ description = "the list of assets",
+ content = @Content(schema = @Schema(implementation = String.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
+ )
+ }
+ )
+ public String getAssetBalance(@PathParam("address") String address) {
+ Security.checkApiCallAllowed("GET addresses/assets", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("balance/{address}/{confirmations}")
+ @Operation(
+ description = "Calculates the balance of the given address after the given confirmations.",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET balance:address:confirmations"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
+ })
+ },
+ responses = {
+ @ApiResponse(
+ description = "the balance",
+ content = @Content(schema = @Schema(implementation = String.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
+ )
+ }
+ )
+ public String getGeneratingBalance(@PathParam("address") String address, @PathParam("confirmations") int confirmations) {
+ Security.checkApiCallAllowed("GET addresses/balance", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/publickey/{address}")
+ @Operation(
+ description = "Returns the 32-byte long base58-encoded account publickey of the given address.",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET publickey:address"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
+ })
+ },
+ responses = {
+ @ApiResponse(
+ description = "the publickey",
+ content = @Content(schema = @Schema(implementation = String.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
+ )
+ }
+ )
+ public String getPublicKey(@PathParam("address") String address) {
+ Security.checkApiCallAllowed("GET addresses/publickey", request);
+
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/src/api/AnnotationPostProcessor.java b/src/api/AnnotationPostProcessor.java
new file mode 100644
index 00000000..224c7a8e
--- /dev/null
+++ b/src/api/AnnotationPostProcessor.java
@@ -0,0 +1,221 @@
+
+package api;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import globalization.ContextPaths;
+import globalization.Translator;
+import io.swagger.v3.core.converter.ModelConverters;
+import io.swagger.v3.jaxrs2.Reader;
+import io.swagger.v3.jaxrs2.ReaderListener;
+import io.swagger.v3.oas.models.media.Content;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.examples.Example;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.media.MediaType;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.responses.ApiResponse;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+public class AnnotationPostProcessor implements ReaderListener {
+
+ private class ContextInformation {
+ public String path;
+ public Map keys;
+ }
+
+ private final Translator translator;
+ private final ApiErrorFactory apiErrorFactory;
+
+ public AnnotationPostProcessor() {
+ this(Translator.getInstance(), ApiErrorFactory.getInstance());
+ }
+
+ public AnnotationPostProcessor(Translator translator, ApiErrorFactory apiErrorFactory) {
+ this.translator = translator;
+ this.apiErrorFactory = apiErrorFactory;
+ }
+
+ @Override
+ public void beforeScan(Reader reader, OpenAPI openAPI) {}
+
+ @Override
+ public void afterScan(Reader reader, OpenAPI openAPI) {
+ // use context path and keys from "x-translation" extension annotations
+ // to translate supported annotations and finally remove "x-translation" extensions
+ Info resourceInfo = openAPI.getInfo();
+ ContextInformation resourceContext = getContextInformation(openAPI.getExtensions());
+ removeTranslationAnnotations(openAPI.getExtensions());
+ TranslateProperties(Constants.TRANSLATABLE_INFO_PROPERTIES, resourceContext, resourceInfo);
+
+ for (Map.Entry pathEntry : openAPI.getPaths().entrySet())
+ {
+ PathItem pathItem = pathEntry.getValue();
+ ContextInformation pathContext = getContextInformation(pathItem.getExtensions(), resourceContext);
+ removeTranslationAnnotations(pathItem.getExtensions());
+ TranslateProperties(Constants.TRANSLATABLE_PATH_ITEM_PROPERTIES, pathContext, pathItem);
+
+ for (Operation operation : pathItem.readOperations()) {
+ ContextInformation operationContext = getContextInformation(operation.getExtensions(), pathContext);
+ removeTranslationAnnotations(operation.getExtensions());
+ TranslateProperties(Constants.TRANSLATABLE_OPERATION_PROPERTIES, operationContext, operation);
+
+ addApiErrorResponses(operation);
+ removeApiErrorsAnnotations(operation.getExtensions());
+
+ for (Map.Entry responseEntry : operation.getResponses().entrySet()) {
+ ApiResponse response = responseEntry.getValue();
+ ContextInformation responseContext = getContextInformation(response.getExtensions(), operationContext);
+ removeTranslationAnnotations(response.getExtensions());
+ TranslateProperties(Constants.TRANSLATABLE_API_RESPONSE_PROPERTIES, responseContext, response);
+ }
+ }
+ }
+ }
+
+ private void addApiErrorResponses(Operation operation) {
+ List apiErrors = getApiErrors(operation.getExtensions());
+ if(apiErrors != null) {
+ for(ApiError apiError : apiErrors) {
+ String statusCode = Integer.toString(apiError.getStatus());
+ ApiResponse apiResponse = operation.getResponses().get(statusCode);
+ if(apiResponse == null) {
+ Schema errorMessageSchema = ModelConverters.getInstance().readAllAsResolvedSchema(ApiErrorMessage.class).schema;
+ MediaType mediaType = new MediaType().schema(errorMessageSchema);
+ Content content = new Content().addMediaType(javax.ws.rs.core.MediaType.APPLICATION_JSON, mediaType);
+ apiResponse = new ApiResponse().content(content);
+ operation.getResponses().addApiResponse(statusCode, apiResponse);
+ }
+
+ int apiErrorCode = apiError.getCode();
+ ApiErrorMessage apiErrorMessage = new ApiErrorMessage(apiErrorCode, this.apiErrorFactory.getErrorMessage(apiError));
+ Example example = new Example().value(apiErrorMessage);
+
+ // XXX: addExamples(..) is not working in Swagger 2.0.4. This bug is referenced in https://github.com/swagger-api/swagger-ui/issues/2651
+ // Replace the call to .setExample(..) by .addExamples(..) when the bug is fixed.
+ apiResponse.getContent().get(javax.ws.rs.core.MediaType.APPLICATION_JSON).setExample(example);
+ //apiResponse.getContent().get(javax.ws.rs.core.MediaType.APPLICATION_JSON).addExamples(Integer.toString(apiErrorCode), example);
+ }
+ }
+ }
+
+ private void TranslateProperties(List> translatableProperties, ContextInformation context, T item) {
+ if(context.keys != null) {
+ Map keys = context.keys;
+ for(TranslatableProperty prop : translatableProperties) {
+ String key = keys.get(prop.keyName());
+ if(key != null) {
+ String originalValue = prop.getValue(item);
+ // XXX: use browser locale instead default?
+ String translation = translator.translate(context.path, key, originalValue);
+ prop.setValue(item, translation);
+ }
+ }
+ }
+ }
+
+ private List getApiErrors(Map extensions) {
+ if(extensions == null)
+ return null;
+
+ List apiErrorStrings = new ArrayList();
+ try {
+ ArrayNode apiErrorsNode = (ArrayNode)extensions.get("x-" + Constants.API_ERRORS_EXTENSION_NAME);
+ if(apiErrorsNode == null)
+ return null;
+
+ for(int i = 0; i < apiErrorsNode.size(); i++) {
+ String errorString = apiErrorsNode.get(i).asText();
+ apiErrorStrings.add(errorString);
+ }
+ } catch(Exception e) {
+ // TODO: error logging
+ return null;
+ }
+
+ List result = new ArrayList<>();
+ for(String apiErrorString : apiErrorStrings) {
+ ApiError apiError = null;
+ try {
+ apiError = ApiError.valueOf(apiErrorString);
+ } catch(IllegalArgumentException e) {
+ try {
+ int errorCodeInt = Integer.parseInt(apiErrorString);
+ apiError = ApiError.fromCode(errorCodeInt);
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ }
+
+ if(apiError == null)
+ return null;
+
+ result.add(apiError);
+ }
+
+ return result;
+ }
+
+ private ContextInformation getContextInformation(Map extensions) {
+ return getContextInformation(extensions, null);
+ }
+
+ private ContextInformation getContextInformation(Map extensions, ContextInformation base) {
+ if(extensions != null) {
+ Map translationDefinitions = (Map)extensions.get("x-" + Constants.TRANSLATION_EXTENSION_NAME);
+ if(translationDefinitions != null) {
+ ContextInformation result = new ContextInformation();
+ result.path = combinePaths(base, (String)translationDefinitions.get(Constants.TRANSLATION_PATH_EXTENSION_NAME));
+ result.keys = getTranslationKeys(translationDefinitions);
+ return result;
+ }
+ }
+
+ if(base != null) {
+ ContextInformation result = new ContextInformation();
+ result.path = base.path;
+ return result;
+ }
+
+ return null;
+ }
+
+ private void removeApiErrorsAnnotations(Map extensions) {
+ String extensionName = Constants.API_ERRORS_EXTENSION_NAME;
+ removeExtension(extensions, extensionName);
+ }
+
+ private void removeTranslationAnnotations(Map extensions) {
+ String extensionName = Constants.TRANSLATION_EXTENSION_NAME;
+ removeExtension(extensions, extensionName);
+ }
+
+ private void removeExtension(Map extensions, String extensionName) {
+ if(extensions == null)
+ return;
+
+ extensions.remove("x-" + extensionName);
+ }
+
+ private Map getTranslationKeys(Map translationDefinitions) {
+ Map result = new HashMap<>();
+
+ for(TranslatableProperty prop : Constants.TRANSLATABLE_INFO_PROPERTIES) {
+ String key = (String)translationDefinitions.get(prop.keyName());
+ if(key != null)
+ result.put(prop.keyName(), key);
+ }
+
+ return result;
+ }
+
+ private String combinePaths(ContextInformation base, String path) {
+ String basePath = (base != null) ? base.path : null;
+ return ContextPaths.combinePaths(basePath, path);
+ }
+}
diff --git a/src/api/ApiClient.java b/src/api/ApiClient.java
index d43a63f2..9421a23b 100644
--- a/src/api/ApiClient.java
+++ b/src/api/ApiClient.java
@@ -1,16 +1,20 @@
package api;
+import globalization.ContextPaths;
import globalization.Translator;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
+import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.Path;
@@ -49,6 +53,8 @@ public class ApiClient {
}
}
+ private static final String TRANSLATION_CONTEXT_PATH = "/Api/ApiClient";
+
private static final Pattern COMMAND_PATTERN = Pattern.compile("^ *(?GET|POST|PUT|PATCH|DELETE) *(?.*)$");
private static final Pattern HELP_COMMAND_PATTERN = Pattern.compile("^ *help *(?.*)$", Pattern.CASE_INSENSITIVE);
private static final List> HTTP_METHOD_ANNOTATIONS = Arrays.asList(
@@ -59,8 +65,8 @@ public class ApiClient {
DELETE.class
);
+ private final Translator translator;
ApiService apiService;
- private Translator translator;
List helpInfos;
public ApiClient(ApiService apiService, Translator translator) {
@@ -83,8 +89,6 @@ public class ApiClient {
private List getHelpInfos(Iterable> resources) {
List result = new ArrayList<>();
- // TODO: need some way to realize translation from resource annotations
-
// scan each resource class
for (Class> resource : resources) {
if (OpenApiResource.class.isAssignableFrom(resource)) {
@@ -94,18 +98,28 @@ public class ApiClient {
if (resourcePath == null) {
continue;
}
-
String resourcePathString = resourcePath.value();
+ // get translation context path for resource
+ String resourceContextPath = "/";
+ OpenAPIDefinition openAPIDefinition = resource.getDeclaredAnnotation(OpenAPIDefinition.class);
+ if(openAPIDefinition != null)
+ resourceContextPath = getContextPath(openAPIDefinition.extensions());
+
// scan each method
for (Method method : resource.getDeclaredMethods()) {
Operation operationAnnotation = method.getAnnotation(Operation.class);
- if (operationAnnotation == null) {
+ if (operationAnnotation == null)
continue;
- }
String description = operationAnnotation.description();
+ // translate
+ String operationContextPath = ContextPaths.combinePaths(resourceContextPath, getContextPath(operationAnnotation.extensions()));
+ String operationDescriptionKey = getDescriptionTranslationKey(operationAnnotation.extensions());
+ if(operationDescriptionKey != null)
+ description = translator.translate(operationContextPath, operationDescriptionKey, description);
+
// extract responses
ArrayList success = new ArrayList();
ArrayList errors = new ArrayList();
@@ -114,6 +128,20 @@ public class ApiClient {
if(StringUtils.isBlank(responseDescription))
continue; // ignore responses without description
+ // translate
+ String responseContextPath = ContextPaths.combinePaths(operationContextPath, getContextPath(response.extensions()));
+ String responseDescriptionKey = getDescriptionTranslationKey(response.extensions());
+ if(responseDescriptionKey != null)
+ responseDescription = translator.translate(responseContextPath, responseDescriptionKey, responseDescription);
+
+ String apiErrorCode = getApiErrorCode(response.extensions());
+ if(apiErrorCode != null) {
+ responseDescription = translator.translate(TRANSLATION_CONTEXT_PATH, "API error response", "(API error: ${ERROR_CODE}) ${DESCRIPTION}",
+ new AbstractMap.SimpleEntry<>("ERROR_CODE", apiErrorCode),
+ new AbstractMap.SimpleEntry<>("DESCRIPTION", responseDescription)
+ );
+ }
+
try {
// try to identify response type by status code
int responseCode = Integer.parseInt(response.responseCode());
@@ -164,6 +192,50 @@ public class ApiClient {
return result;
}
+
+ private String getApiErrorCode(Extension[] extensions) {
+ if(extensions == null)
+ return null;
+
+ for(Extension extension : extensions) {
+ if(extension.name() != null && !extension.name().isEmpty())
+ continue;
+
+ for(ExtensionProperty prop : extension.properties()) {
+ if(Constants.API_ERROR_CODE_EXTENSION_NAME.equals(prop.name())) {
+ return prop.value();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private String getContextPath(Extension[] extensions) {
+ return getTranslationExtensionValue(extensions, Constants.TRANSLATION_PATH_EXTENSION_NAME);
+ }
+
+ private String getDescriptionTranslationKey(Extension[] extensions) {
+ return getTranslationExtensionValue(extensions, Constants.TRANSLATION_ANNOTATION_DESCRIPTION_KEY);
+ }
+
+ private String getTranslationExtensionValue(Extension[] extensions, String key) {
+ if(extensions == null)
+ return null;
+
+ for(Extension extension : extensions) {
+ if(!Constants.TRANSLATION_EXTENSION_NAME.equals(extension.name()))
+ continue;
+
+ for(ExtensionProperty prop : extension.properties()) {
+ if(key.equals(prop.name())) {
+ return prop.value();
+ }
+ }
+ }
+
+ return null;
+ }
private String getHelpPatternForPath(String path) {
path = path
@@ -205,7 +277,7 @@ public class ApiClient {
match = COMMAND_PATTERN.matcher(command);
if(!match.matches())
- return this.translator.translate(Locale.getDefault(), "ApiClient: INVALID_COMMAND", "Invalid command! \nType help to get a list of commands.");
+ return this.translator.translate(TRANSLATION_CONTEXT_PATH, "invalid command", "Invalid command! \nType 'help all' to get a list of commands.");
// send the command to the API service
String method = match.group("method");
@@ -223,13 +295,22 @@ public class ApiClient {
final int status = response.getStatus();
StringBuilder result = new StringBuilder();
if(status >= 400) {
- result.append("HTTP Status ");
- result.append(status);
- if(!StringUtils.isBlank(body)) {
- result.append(": ");
- result.append(body);
+ if(StringUtils.isBlank(body)) {
+ result.append(
+ this.translator.translate(TRANSLATION_CONTEXT_PATH, "error without body", "HTTP Status ${STATUS}",
+ new AbstractMap.SimpleEntry<>("STATUS", status)
+ )
+ );
+ }else{
+ result.append(
+ this.translator.translate(TRANSLATION_CONTEXT_PATH, "error with body", "HTTP Status ${STATUS}: ${BODY}",
+ new AbstractMap.SimpleEntry<>("STATUS", status),
+ new AbstractMap.SimpleEntry<>("BODY", body)
+ )
+ );
}
- result.append("\nType help to get a list of commands.");
+ result.append("\n");
+ result.append(this.translator.translate(TRANSLATION_CONTEXT_PATH, "error footer", "Type 'help all' to get a list of commands."));
} else {
result.append(body);
}
@@ -240,13 +321,17 @@ public class ApiClient {
builder.append(help.fullPath + "\n");
builder.append(" " + help.description + "\n");
if(help.success != null && help.success.size() > 0) {
- builder.append(" On success returns:\n");
+ builder.append(" ");
+ builder.append(this.translator.translate(TRANSLATION_CONTEXT_PATH, "help: success responses", "On success returns:"));
+ builder.append("\n");
for(String content : help.success) {
builder.append(" " + content + "\n");
}
}
if(help.errors != null && help.errors.size() > 0) {
- builder.append(" On failure returns:\n");
+ builder.append(" ");
+ builder.append(this.translator.translate(TRANSLATION_CONTEXT_PATH, "help: failure responses", "On failure returns:"));
+ builder.append("\n");
for(String content : help.errors) {
builder.append(" " + content + "\n");
}
diff --git a/src/api/ApiError.java b/src/api/ApiError.java
index 471fa9f2..3291fbdc 100644
--- a/src/api/ApiError.java
+++ b/src/api/ApiError.java
@@ -110,6 +110,15 @@ public enum ApiError {
this.status = status;
}
+ public static ApiError fromCode(int code) {
+ for(ApiError apiError : ApiError.values()) {
+ if(apiError.code == code)
+ return apiError;
+ }
+
+ return null;
+ }
+
int getCode() {
return this.code;
}
@@ -117,4 +126,5 @@ public enum ApiError {
int getStatus() {
return this.status;
}
+
}
\ No newline at end of file
diff --git a/src/api/ApiErrorFactory.java b/src/api/ApiErrorFactory.java
index 784a3476..32b1e40f 100644
--- a/src/api/ApiErrorFactory.java
+++ b/src/api/ApiErrorFactory.java
@@ -101,16 +101,16 @@ public class ApiErrorFactory {
// this.errorMessages.put(ApiError.NAME_FOR_SALE, createErrorMessageEntry(ApiError.NAME_FOR_SALE, NameResult.NAME_FOR_SALE.getStatusMessage()));
// this.errorMessages.put(ApiError.NAME_WITH_SPACE, createErrorMessageEntry(ApiError.NAME_WITH_SPACE, NameResult.NAME_WITH_SPACE.getStatusMessage()));
//AT
- this.errorMessages.put(ApiError.INVALID_CREATION_BYTES, createErrorMessageEntry(ApiError.INVALID_CREATION_BYTES, "error in creation bytes"));
// TODO
// this.errorMessages.put(ApiError.INVALID_DESC_LENGTH, createErrorMessageEntry(ApiError.INVALID_DESC_LENGTH,
// "invalid description length. max length ${MAX_LENGTH}",
// new AbstractMap.SimpleEntry("MAX_LENGTH", AT_Constants.DESC_MAX_LENGTH));
this.errorMessages.put(ApiError.EMPTY_CODE, createErrorMessageEntry(ApiError.EMPTY_CODE, "code is empty"));
this.errorMessages.put(ApiError.DATA_SIZE, createErrorMessageEntry(ApiError.DATA_SIZE, "invalid data length"));
+ this.errorMessages.put(ApiError.NULL_PAGES, createErrorMessageEntry(ApiError.NULL_PAGES, "invalid pages"));
this.errorMessages.put(ApiError.INVALID_TYPE_LENGTH, createErrorMessageEntry(ApiError.INVALID_TYPE_LENGTH, "invalid type length"));
this.errorMessages.put(ApiError.INVALID_TAGS_LENGTH, createErrorMessageEntry(ApiError.INVALID_TAGS_LENGTH, "invalid tags length"));
- this.errorMessages.put(ApiError.NULL_PAGES, createErrorMessageEntry(ApiError.NULL_PAGES, "invalid pages"));
+ this.errorMessages.put(ApiError.INVALID_CREATION_BYTES, createErrorMessageEntry(ApiError.INVALID_CREATION_BYTES, "error in creation bytes"));
//BLOG
this.errorMessages.put(ApiError.BODY_EMPTY, createErrorMessageEntry(ApiError.BODY_EMPTY, "invalid body it must not be empty"));
@@ -147,13 +147,17 @@ public class ApiErrorFactory {
}
private ErrorMessageEntry createErrorMessageEntry(ApiError errorCode, String defaultTemplate, AbstractMap.SimpleEntry... templateValues) {
- String templateKey = String.format("%s: ApiError.%s message", ApiErrorFactory.class.getSimpleName(), errorCode.name());
+ String templateKey = String.format(Constants.APIERROR_KEY, errorCode.name());
return new ErrorMessageEntry(templateKey, defaultTemplate, templateValues);
}
+
+ public String getErrorMessage(ApiError error) {
+ return getErrorMessage(null, error);
+ }
public String getErrorMessage(Locale locale, ApiError error) {
ErrorMessageEntry errorMessage = this.errorMessages.get(error);
- String message = this.translator.translate(locale, errorMessage.templateKey, errorMessage.defaultTemplate, errorMessage.templateValues);
+ String message = this.translator.translate(locale, Constants.APIERROR_CONTEXT_PATH, errorMessage.templateKey, errorMessage.defaultTemplate, errorMessage.templateValues);
return message;
}
@@ -166,8 +170,7 @@ public class ApiErrorFactory {
}
public ApiException createError(ApiError error, Throwable throwable) {
- Locale locale = Locale.ENGLISH; // default locale
- return createError(locale, error, throwable);
+ return createError(null, error, throwable);
}
public ApiException createError(Locale locale, ApiError error, Throwable throwable) {
diff --git a/src/api/ApiService.java b/src/api/ApiService.java
index afa236ec..d5eae3e3 100644
--- a/src/api/ApiService.java
+++ b/src/api/ApiService.java
@@ -1,11 +1,15 @@
package api;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
+import java.io.File;
import java.util.HashSet;
import java.util.Set;
+import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
+import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.InetAccessHandler;
+import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.glassfish.jersey.server.ResourceConfig;
@@ -21,10 +25,12 @@ public class ApiService {
public ApiService() {
// resources to register
this.resources = new HashSet>();
+ this.resources.add(AddressesResource.class);
this.resources.add(BlocksResource.class);
this.resources.add(OpenApiResource.class); // swagger
+ this.resources.add(AnnotationPostProcessor.class); // for API resource annotations
ResourceConfig config = new ResourceConfig(this.resources);
-
+
// create RPC server
this.server = new Server(Settings.getInstance().getRpcPort());
@@ -35,16 +41,30 @@ public class ApiService {
}
this.server.setHandler(accessHandler);
+ // url rewriting
+ RewriteHandler rewriteHandler = new RewriteHandler();
+ accessHandler.setHandler(rewriteHandler);
+
// context
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
context.setContextPath("/");
- accessHandler.setHandler(context);
-
+ rewriteHandler.setHandler(context);
+
// API servlet
ServletContainer container = new ServletContainer(config);
ServletHolder apiServlet = new ServletHolder(container);
apiServlet.setInitOrder(1);
context.addServlet(apiServlet, "/*");
+
+ // Swagger-UI static content
+ ClassLoader loader = this.getClass().getClassLoader();
+ File swaggerUIResourceLocation = new File(loader.getResource("resources/swagger-ui/").getFile());
+ ServletHolder swaggerUIServlet = new ServletHolder("static-swagger-ui", DefaultServlet.class);
+ swaggerUIServlet.setInitParameter("resourceBase", swaggerUIResourceLocation.getAbsolutePath());
+ swaggerUIServlet.setInitParameter("dirAllowed","true");
+ swaggerUIServlet.setInitParameter("pathInfoOnly","true");
+ context.addServlet(swaggerUIServlet,"/api-documentation/*");
+ rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/index.html")); // redirect to swagger ui start page
}
//XXX: replace singleton pattern by dependency injection?
diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java
index e4f7f09b..17fa73da 100644
--- a/src/api/BlocksResource.java
+++ b/src/api/BlocksResource.java
@@ -1,10 +1,15 @@
package api;
+import data.block.BlockData;
import globalization.Translator;
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import java.math.BigDecimal;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
@@ -13,12 +18,19 @@ import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
+import qora.block.Block;
import repository.Repository;
import repository.RepositoryManager;
+import utils.Base58;
@Path("blocks")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
+@OpenAPIDefinition(
+ extensions = @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="/Api/BlocksResource")
+ })
+)
public class BlocksResource {
@Context
@@ -27,240 +39,366 @@ public class BlocksResource {
private ApiErrorFactory apiErrorFactory;
public BlocksResource() {
- this(new ApiErrorFactory(new Translator()));
+ this(new ApiErrorFactory(Translator.getInstance()));
}
public BlocksResource(ApiErrorFactory apiErrorFactory) {
this.apiErrorFactory = apiErrorFactory;
}
- @GET
- @Operation(
- description = "Returns an array of the 50 last blocks generated by your accounts",
- responses = {
- @ApiResponse(
- description = "The blocks"
- //content = @Content(schema = @Schema(implementation = ???))
- ),
- @ApiResponse(
- responseCode = "422",
- description = "Error: 201 - Wallet does not exist",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
- )
- }
- )
- public String getBlocks() {
- Security.checkApiCallAllowed("GET blocks", request);
-
- throw new UnsupportedOperationException();
- }
-
- @GET
- @Path("/address/{address}")
- @Operation(
- description = "Returns an array of the 50 last blocks generated by a specific address in your wallet",
- responses = {
- @ApiResponse(
- description = "The blocks"
- //content = @Content(schema = @Schema(implementation = ???))
- ),
- @ApiResponse(
- responseCode = "400",
- description = "102 - Invalid address",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
- ),
- @ApiResponse(
- responseCode = "422",
- description = "201 - Wallet does not exist",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
- ),
- @ApiResponse(
- responseCode = "422",
- description = "202 - Address does not exist in wallet",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
- )
- }
- )
- public String getBlocks(@PathParam("address") String address) {
- Security.checkApiCallAllowed("GET blocks/address/" + address, request);
-
- throw new UnsupportedOperationException();
- }
-
@GET
@Path("/{signature}")
@Operation(
- description = "Returns the block that matches the given signature",
+ description = "returns the block that matches the given signature",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET signature"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]", parseValue = true),
+ })
+ },
responses = {
@ApiResponse(
- description = "The block"
- //content = @Content(schema = @Schema(implementation = ???))
- ),
- @ApiResponse(
- responseCode = "400",
- description = "101 - Invalid signature",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
- ),
- @ApiResponse(
- responseCode = "422",
- description = "301 - Block does not exist",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
+ description = "the block",
+ content = @Content(schema = @Schema(implementation = BlockData.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
)
}
)
- public String getBlock(@PathParam("signature") String signature) {
+ public BlockData getBlock(@PathParam("signature") String signature) {
Security.checkApiCallAllowed("GET blocks", request);
- throw new UnsupportedOperationException();
+ // decode signature
+ byte[] signatureBytes;
+ try
+ {
+ signatureBytes = Base58.decode(signature);
+ }
+ catch(Exception e)
+ {
+ throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
+ }
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
+
+ // check if block exists
+ if(blockData == null)
+ throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
+
+ return blockData;
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
}
@GET
@Path("/first")
@Operation(
- description = "Returns the genesis block",
+ description = "returns the genesis block",
+ extensions = @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET first"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
responses = {
@ApiResponse(
- description = "The block"
- //content = @Content(schema = @Schema(implementation = ???))
+ description = "the block",
+ content = @Content(schema = @Schema(implementation = BlockData.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
)
}
)
- public String getFirstBlock() {
+ public BlockData getFirstBlock() {
Security.checkApiCallAllowed("GET blocks/first", request);
- throw new UnsupportedOperationException();
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().fromHeight(1);
+ return blockData;
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
}
@GET
@Path("/last")
@Operation(
- description = "Returns the last valid block",
+ description = "returns the last valid block",
+ extensions = @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET last"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
responses = {
@ApiResponse(
- description = "The block"
- //content = @Content(schema = @Schema(implementation = ???))
+ description = "the block",
+ content = @Content(schema = @Schema(implementation = BlockData.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
)
}
)
- public String getLastBlock() {
+ public BlockData getLastBlock() {
Security.checkApiCallAllowed("GET blocks/last", request);
- throw new UnsupportedOperationException();
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().getLastBlock();
+ return blockData;
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
}
@GET
@Path("/child/{signature}")
@Operation(
- description = "Returns the child block of the block that matches the given signature",
+ description = "returns the child block of the block that matches the given signature",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET child:signature"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]", parseValue = true),
+ })
+ },
responses = {
@ApiResponse(
- description = "The block"
- //content = @Content(schema = @Schema(implementation = ???))
- ),
- @ApiResponse(
- responseCode = "400",
- description = "101 - Invalid signature",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
- ),
- @ApiResponse(
- responseCode = "422",
- description = "301 - Block does not exist",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
+ description = "the block",
+ content = @Content(schema = @Schema(implementation = BlockData.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
)
}
)
- public String getChild(@PathParam("signature") String signature) {
+ public BlockData getChild(@PathParam("signature") String signature) {
Security.checkApiCallAllowed("GET blocks/child", request);
- throw new UnsupportedOperationException();
+ // decode signature
+ byte[] signatureBytes;
+ try
+ {
+ signatureBytes = Base58.decode(signature);
+ }
+ catch(Exception e)
+ {
+ throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
+ }
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
+
+ // check if block exists
+ if(blockData == null)
+ throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
+
+ int height = blockData.getHeight();
+ BlockData childBlockData = repository.getBlockRepository().fromHeight(height + 1);
+
+ // check if child exists
+ if(childBlockData == null)
+ throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
+
+ return childBlockData;
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
}
@GET
@Path("/generatingbalance")
@Operation(
- description = "Calculates the generating balance of the block that will follow the last block",
+ description = "calculates the generating balance of the block that will follow the last block",
+ extensions = @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET generatingbalance"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
responses = {
@ApiResponse(
- description = "The generating balance",
- content = @Content(schema = @Schema(implementation = long.class))
+ description = "the generating balance",
+ content = @Content(schema = @Schema(implementation = BigDecimal.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
)
}
)
- public long getGeneratingBalance() {
+ public BigDecimal getGeneratingBalance() {
Security.checkApiCallAllowed("GET blocks/generatingbalance", request);
- throw new UnsupportedOperationException();
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().getLastBlock();
+ Block block = new Block(repository, blockData);
+ return block.calcNextBlockGeneratingBalance();
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
}
@GET
@Path("/generatingbalance/{signature}")
@Operation(
- description = "Calculates the generating balance of the block that will follow the block that matches the signature",
+ description = "calculates the generating balance of the block that will follow the block that matches the signature",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET generatingbalance:signature"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]", parseValue = true),
+ })
+ },
responses = {
@ApiResponse(
- description = "The block",
- content = @Content(schema = @Schema(implementation = long.class))
- ),
- @ApiResponse(
- responseCode = "400",
- description = "101 - Invalid signature",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
- ),
- @ApiResponse(
- responseCode = "422",
- description = "301 - Block does not exist",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
+ description = "the block",
+ content = @Content(schema = @Schema(implementation = BigDecimal.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
)
}
)
- public long getGeneratingBalance(@PathParam("signature") String signature) {
+ public BigDecimal getGeneratingBalance(@PathParam("signature") String signature) {
Security.checkApiCallAllowed("GET blocks/generatingbalance", request);
- throw new UnsupportedOperationException();
+ // decode signature
+ byte[] signatureBytes;
+ try
+ {
+ signatureBytes = Base58.decode(signature);
+ }
+ catch(Exception e)
+ {
+ throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
+ }
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
+
+ // check if block exists
+ if(blockData == null)
+ throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
+
+ Block block = new Block(repository, blockData);
+ return block.calcNextBlockGeneratingBalance();
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
}
@GET
@Path("/time")
@Operation(
- description = "Calculates the time it should take for the network to generate the next block",
+ description = "calculates the time it should take for the network to generate the next block",
+ extensions = @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET time"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
responses = {
@ApiResponse(
- description = "The time", // in seconds?
- content = @Content(schema = @Schema(implementation = long.class))
+ description = "the time in seconds", // in seconds?
+ content = @Content(schema = @Schema(implementation = long.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
)
}
)
public long getTimePerBlock() {
Security.checkApiCallAllowed("GET blocks/time", request);
- throw new UnsupportedOperationException();
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().getLastBlock();
+ return Block.calcForgingDelay(blockData.getGeneratingBalance());
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
}
@GET
@Path("/time/{generatingbalance}")
@Operation(
- description = "Calculates the time it should take for the network to generate blocks when the current generating balance in the network is the specified generating balance",
+ description = "calculates the time it should take for the network to generate blocks when the current generating balance in the network is the specified generating balance",
+ extensions = @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET time:generatingbalance"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
responses = {
@ApiResponse(
- description = "The time", // in seconds?
- content = @Content(schema = @Schema(implementation = long.class))
+ description = "the time", // in seconds?
+ content = @Content(schema = @Schema(implementation = long.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
)
}
)
- public String getTimePerBlock(@PathParam("generating") long generatingbalance) {
+ public long getTimePerBlock(@PathParam("generating") BigDecimal generatingbalance) {
Security.checkApiCallAllowed("GET blocks/time", request);
- throw new UnsupportedOperationException();
+ return Block.calcForgingDelay(generatingbalance);
}
@GET
@Path("/height")
@Operation(
- description = "Returns the block height of the last block.",
+ description = "returns the block height of the last block.",
+ extensions = @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET height"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
responses = {
@ApiResponse(
- description = "The height",
- content = @Content(schema = @Schema(implementation = int.class))
+ description = "the height",
+ content = @Content(schema = @Schema(implementation = int.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
)
}
)
@@ -269,7 +407,10 @@ public class BlocksResource {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockchainHeight();
- } catch (Exception e) {
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
}
}
@@ -277,49 +418,99 @@ public class BlocksResource {
@GET
@Path("/height/{signature}")
@Operation(
- description = "Returns the block height of the block that matches the given signature",
+ description = "returns the block height of the block that matches the given signature",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET height:signature"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]", parseValue = true),
+ })
+ },
responses = {
@ApiResponse(
- description = "The height",
- content = @Content(schema = @Schema(implementation = int.class))
- ),
- @ApiResponse(
- responseCode = "400",
- description = "101 - Invalid signature",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
- ),
- @ApiResponse(
- responseCode = "422",
- description = "301 - Block does not exist",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
+ description = "the height",
+ content = @Content(schema = @Schema(implementation = int.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
)
}
)
public int getHeight(@PathParam("signature") String signature) {
Security.checkApiCallAllowed("GET blocks/height", request);
- throw new UnsupportedOperationException();
+ // decode signature
+ byte[] signatureBytes;
+ try
+ {
+ signatureBytes = Base58.decode(signature);
+ }
+ catch(Exception e)
+ {
+ throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
+ }
+
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
+
+ // check if block exists
+ if(blockData == null)
+ throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
+
+ return blockData.getHeight();
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
}
@GET
@Path("/byheight/{height}")
@Operation(
- description = "Returns the block whith given height",
+ description = "returns the block whith given height",
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="path", value="GET byheight:height"),
+ @ExtensionProperty(name="description.key", value="operation:description")
+ }),
+ @Extension(properties = {
+ @ExtensionProperty(name="apiErrors", value="[\"BLOCK_NO_EXISTS\"]", parseValue = true),
+ })
+ },
responses = {
@ApiResponse(
- description = "The block"
- //content = @Content(schema = @Schema(implementation = ???))
- ),
- @ApiResponse(
- responseCode = "422",
- description = "301 - Block does not exist",
- content = @Content(schema = @Schema(implementation = ApiErrorMessage.class))
+ description = "the block",
+ content = @Content(schema = @Schema(implementation = BlockData.class)),
+ extensions = {
+ @Extension(name = "translation", properties = {
+ @ExtensionProperty(name="description.key", value="success_response:description")
+ })
+ }
)
}
)
- public String getbyHeight(@PathParam("height") int height) {
+ public BlockData getbyHeight(@PathParam("height") int height) {
Security.checkApiCallAllowed("GET blocks/byheight", request);
- throw new UnsupportedOperationException();
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ BlockData blockData = repository.getBlockRepository().fromHeight(height);
+
+ // check if block exists
+ if(blockData == null)
+ throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
+
+ return blockData;
+
+ } catch (ApiException e) {
+ throw e;
+ } catch (Exception e) {
+ throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
+ }
}
}
diff --git a/src/api/Constants.java b/src/api/Constants.java
new file mode 100644
index 00000000..60886dfd
--- /dev/null
+++ b/src/api/Constants.java
@@ -0,0 +1,76 @@
+package api;
+
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.responses.ApiResponse;
+import static java.util.Arrays.asList;
+import java.util.List;
+
+class Constants {
+ public static final String APIERROR_CONTEXT_PATH = "/Api";
+ public static final String APIERROR_KEY = "ApiError/%s";
+
+ public static final String TRANSLATION_EXTENSION_NAME = "translation";
+ public static final String TRANSLATION_PATH_EXTENSION_NAME = "path";
+
+ public static final String TRANSLATION_ANNOTATION_DESCRIPTION_KEY = "description.key";
+ public static final String TRANSLATION_ANNOTATION_SUMMARY_KEY = "summary.key";
+ public static final String TRANSLATION_ANNOTATION_TITLE_KEY = "title.key";
+ public static final String TRANSLATION_ANNOTATION_TERMS_OF_SERVICE_KEY = "termsOfService.key";
+
+ public static final String API_ERRORS_EXTENSION_NAME = "apiErrors";
+ public static final String API_ERROR_CODE_EXTENSION_NAME = "apiErrorCode";
+
+ public static final List> TRANSLATABLE_INFO_PROPERTIES = asList(
+ new TranslatableProperty() {
+ @Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; }
+ @Override public void setValue(Info item, String translation) { item.setDescription(translation); }
+ @Override public String getValue(Info item) { return item.getDescription(); }
+ },
+ new TranslatableProperty() {
+ @Override public String keyName() { return TRANSLATION_ANNOTATION_TITLE_KEY; }
+ @Override public void setValue(Info item, String translation) { item.setTitle(translation); }
+ @Override public String getValue(Info item) { return item.getTitle(); }
+ },
+ new TranslatableProperty() {
+ @Override public String keyName() { return TRANSLATION_ANNOTATION_TERMS_OF_SERVICE_KEY; }
+ @Override public void setValue(Info item, String translation) { item.setTermsOfService(translation); }
+ @Override public String getValue(Info item) { return item.getTermsOfService(); }
+ }
+ );
+
+ public static final List> TRANSLATABLE_PATH_ITEM_PROPERTIES = asList(
+ new TranslatableProperty() {
+ @Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; }
+ @Override public void setValue(PathItem item, String translation) { item.setDescription(translation); }
+ @Override public String getValue(PathItem item) { return item.getDescription(); }
+ },
+ new TranslatableProperty() {
+ @Override public String keyName() { return TRANSLATION_ANNOTATION_SUMMARY_KEY; }
+ @Override public void setValue(PathItem item, String translation) { item.setSummary(translation); }
+ @Override public String getValue(PathItem item) { return item.getSummary(); }
+ }
+ );
+
+ public static final List> TRANSLATABLE_OPERATION_PROPERTIES = asList(
+ new TranslatableProperty() {
+ @Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; }
+ @Override public void setValue(Operation item, String translation) { item.setDescription(translation); }
+ @Override public String getValue(Operation item) { return item.getDescription(); }
+ },
+ new TranslatableProperty() {
+ @Override public String keyName() { return TRANSLATION_ANNOTATION_SUMMARY_KEY; }
+ @Override public void setValue(Operation item, String translation) { item.setSummary(translation); }
+ @Override public String getValue(Operation item) { return item.getSummary(); }
+ }
+ );
+
+ public static final List> TRANSLATABLE_API_RESPONSE_PROPERTIES = asList(
+ new TranslatableProperty() {
+ @Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; }
+ @Override public void setValue(ApiResponse item, String translation) { item.setDescription(translation); }
+ @Override public String getValue(ApiResponse item) { return item.getDescription(); }
+ }
+ );
+}
diff --git a/src/api/TranslatableProperty.java b/src/api/TranslatableProperty.java
new file mode 100644
index 00000000..8dd90e9a
--- /dev/null
+++ b/src/api/TranslatableProperty.java
@@ -0,0 +1,7 @@
+package api;
+
+interface TranslatableProperty {
+ public String keyName();
+ public void setValue(T item, String translation);
+ public String getValue(T item);
+}
diff --git a/src/data/block/BlockData.java b/src/data/block/BlockData.java
index 838b347e..a7255f43 100644
--- a/src/data/block/BlockData.java
+++ b/src/data/block/BlockData.java
@@ -3,8 +3,9 @@ package data.block;
import java.math.BigDecimal;
import com.google.common.primitives.Bytes;
+import java.io.Serializable;
-public class BlockData {
+public class BlockData implements Serializable {
private byte[] signature;
private int version;
@@ -20,6 +21,8 @@ public class BlockData {
private int atCount;
private BigDecimal atFees;
+ private BlockData() {} // necessary for JAX-RS serialization
+
public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp,
BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, int atCount, BigDecimal atFees) {
this.version = version;
diff --git a/src/globalization/ContextPaths.java b/src/globalization/ContextPaths.java
new file mode 100644
index 00000000..8743d438
--- /dev/null
+++ b/src/globalization/ContextPaths.java
@@ -0,0 +1,33 @@
+package globalization;
+
+import java.nio.file.Paths;
+
+public class ContextPaths {
+
+ public static boolean isValidKey(String value) {
+ return !value.contains("/") && !ContextPaths.containsParentReference(value);
+ }
+
+ public static boolean containsParentReference(String value) {
+ for(String part : value.split("/")) {
+ if(part.equalsIgnoreCase(".."))
+ return true;
+ }
+ return false;
+ }
+
+ public static String combinePaths(String left, String right) {
+ left = (left != null) ? left : "";
+ right = (right != null) ? right : "";
+ return Paths.get("/", left, right).normalize().toString();
+ }
+
+ public static String getParent(String path) {
+ return combinePaths(path, "..");
+ }
+
+ public static boolean isRoot(String path) {
+ return path.equals("/");
+ }
+
+}
diff --git a/src/globalization/TranslationEntry.java b/src/globalization/TranslationEntry.java
new file mode 100644
index 00000000..91a5114d
--- /dev/null
+++ b/src/globalization/TranslationEntry.java
@@ -0,0 +1,32 @@
+package globalization;
+
+import java.util.Locale;
+
+public class TranslationEntry {
+ private Locale locale;
+ private String path;
+ private String template;
+
+ public TranslationEntry(Locale locale, String path, String template) {
+ this.locale = locale;
+ this.path = path;
+ this.template = template;
+ }
+
+ public Locale locale() {
+ return this.locale;
+ }
+
+ public String path() {
+ return this.path;
+ }
+
+ public String template() {
+ return this.template;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{locale: '%s', path: '%s', template: '%s'}", this.locale, this.path, this.template);
+ }
+}
diff --git a/src/globalization/TranslationXmlStreamReader.java b/src/globalization/TranslationXmlStreamReader.java
new file mode 100644
index 00000000..1712a9f6
--- /dev/null
+++ b/src/globalization/TranslationXmlStreamReader.java
@@ -0,0 +1,252 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package globalization;
+
+import java.io.InputStream;
+import java.nio.file.Paths;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import javax.xml.namespace.QName;
+
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.*;
+import org.apache.commons.text.StringEscapeUtils;
+
+public class TranslationXmlStreamReader {
+
+ private class State {
+
+ public final Locale locale;
+ public final String path;
+
+ public State(Locale locale, String path) {
+ this.locale = locale;
+ this.path = path;
+ }
+ }
+
+ private static final String LOCALIZATION_TAG_NAME = "localization";
+
+ private static final String CONTEXT_TAG_NAME = "context";
+ private static final String CONTEXT_LOCALE_ATTRIBUTE_NAME = "locale";
+ private static final String CONTEXT_PATH_ATTRIBUTE_NAME = "path";
+
+ private static final String TRANSLATION_TAG_NAME = "translation";
+ private static final String TRANSLATION_KEY_ATTRIBUTE_NAME = "key";
+ private static final String TRANSLATION_TEMPLATE_ATTRIBUTE_NAME = "template";
+
+ public Iterable ReadFrom(InputStream stream) throws XMLStreamException {
+ XMLInputFactory inputFactory = XMLInputFactory.newInstance();
+ XMLEventReader eventReader = inputFactory.createXMLEventReader(stream);
+
+ XMLEvent element = eventReader.nextEvent();
+ if(!element.isStartDocument())
+ throw new javax.xml.stream.XMLStreamException("XML declaration must be first in the document");
+
+ State state = new State(Locale.forLanguageTag("default"), "/");
+
+ List result = new ArrayList<>();
+ if (eventReader.hasNext())
+ {
+ XMLEvent event = eventReader.nextTag();
+ if (isStartElement(event, LOCALIZATION_TAG_NAME))
+ {
+ processLocalization(eventReader, (StartElement)event, state, result);
+ } else {
+ throw new javax.xml.stream.XMLStreamException("Unexpected element: " + event.toString());
+ }
+ }
+
+ while (eventReader.hasNext())
+ {
+ XMLEvent event = eventReader.nextEvent();
+ switch(event.getEventType()) {
+ case XMLEvent.COMMENT:
+ break;
+ case XMLEvent.CHARACTERS:
+ if(!event.asCharacters().isIgnorableWhiteSpace())
+ throw new javax.xml.stream.XMLStreamException("Unexpected content after end of root element: " + event.toString());
+ break;
+ case XMLEvent.END_DOCUMENT:
+ return result;
+ default:
+ throw new javax.xml.stream.XMLStreamException("Unexpected content after end of root element: " + event.toString());
+ }
+ }
+
+ throw new javax.xml.stream.XMLStreamException("End of document not found");
+ }
+
+ private void processLocalization(XMLEventReader eventReader, StartElement element, State state, List result) throws XMLStreamException {
+ assureStartElement(element, LOCALIZATION_TAG_NAME);
+
+ Iterator attributes = element.getAttributes();
+ while (attributes.hasNext())
+ {
+ Attribute attribute = attributes.next();
+ QName name = attribute.getName();
+ throw new javax.xml.stream.XMLStreamException("Unexpected attribute: " + name);
+ }
+
+ XMLEvent event;
+ while(!(event = eventReader.nextTag()).isEndElement()) {
+ if(event.isStartElement()) {
+ StartElement childElement = (StartElement)event;
+ switch(childElement.getName().toString()) {
+ case CONTEXT_TAG_NAME:
+ processContext(eventReader, childElement, state, result);
+ break;
+ case TRANSLATION_TAG_NAME:
+ processTranslation(eventReader, childElement, state, result);
+ break;
+ default:
+ throw new javax.xml.stream.XMLStreamException("Unexpected element: " + event.toString());
+ }
+ } else {
+ throw new javax.xml.stream.XMLStreamException("Unexpected content: " + event.toString());
+ }
+ }
+ assureEndElement(event, LOCALIZATION_TAG_NAME);
+ }
+
+ private void processContext(XMLEventReader eventReader, StartElement element, State state, List result) throws XMLStreamException {
+ assureStartElement(element, CONTEXT_TAG_NAME);
+
+ Locale locale = state.locale;
+ String contextPath = state.path;
+
+ Iterator attributes = element.getAttributes();
+ while (attributes.hasNext())
+ {
+ Attribute attribute = attributes.next();
+ QName name = attribute.getName();
+ String value = attribute.getValue();
+ switch(name.toString()) {
+ case CONTEXT_LOCALE_ATTRIBUTE_NAME:
+ locale = Locale.forLanguageTag(value);
+ break;
+ case CONTEXT_PATH_ATTRIBUTE_NAME:
+ assureIsValidPathExtension(value);
+ contextPath = ContextPaths.combinePaths(contextPath, value);
+ break;
+ default:
+ throw new javax.xml.stream.XMLStreamException("Unexpected attribute: " + name);
+ }
+ }
+
+ state = new State(locale, contextPath);
+
+ XMLEvent event;
+ while(!(event = eventReader.nextTag()).isEndElement()) {
+ if(event.isStartElement()) {
+ StartElement childElement = (StartElement)event;
+ switch(childElement.getName().toString()) {
+ case CONTEXT_TAG_NAME:
+ processContext(eventReader, childElement, state, result);
+ break;
+ case TRANSLATION_TAG_NAME:
+ processTranslation(eventReader, childElement, state, result);
+ break;
+ default:
+ throw new javax.xml.stream.XMLStreamException("Unexpected element: " + event.toString());
+ }
+ } else {
+ throw new javax.xml.stream.XMLStreamException("Unexpected content: " + event.toString());
+ }
+ }
+ assureEndElement(event, CONTEXT_TAG_NAME);
+ }
+
+
+ private void processTranslation(XMLEventReader eventReader, StartElement element, State state, List result) throws XMLStreamException {
+ assureStartElement(element, TRANSLATION_TAG_NAME);
+
+ String path = null;
+ String template = null;
+
+ Iterator attributes = element.getAttributes();
+ while (attributes.hasNext())
+ {
+ Attribute attribute = attributes.next();
+ QName name = attribute.getName();
+ String value = attribute.getValue();
+ switch(name.toString()) {
+ case TRANSLATION_KEY_ATTRIBUTE_NAME:
+ assureIsValidKey(value);
+ path = ContextPaths.combinePaths(state.path, value);
+ break;
+ case TRANSLATION_TEMPLATE_ATTRIBUTE_NAME:
+ template = unescape(value);
+ break;
+ default:
+ throw new javax.xml.stream.XMLStreamException("Unexpected attribute: " + name);
+ }
+ }
+
+ XMLEvent event;
+ while(!(event = eventReader.nextTag()).isEndElement()) {
+ if(event.isStartElement()) {
+ throw new javax.xml.stream.XMLStreamException("Unexpected element: " + event.toString());
+ } else if(event.isCharacters()) {
+ if(template != null)
+ throw new javax.xml.stream.XMLStreamException("Content must be empty if 'template' attribute is used");
+ template = event.asCharacters().getData();
+ }
+ }
+ assureEndElement(event, TRANSLATION_TAG_NAME);
+
+ if(path == null)
+ throw new javax.xml.stream.XMLStreamException("Missing attribute: " + TRANSLATION_KEY_ATTRIBUTE_NAME);
+
+ if(template == null)
+ throw new javax.xml.stream.XMLStreamException("Missing attribute: " + TRANSLATION_TEMPLATE_ATTRIBUTE_NAME);
+
+ result.add(new TranslationEntry(state.locale, path, template));
+ }
+
+ private String unescape(String value) {
+ return StringEscapeUtils.unescapeJava(value);
+ }
+
+ private void assureIsValidPathExtension(String value) throws XMLStreamException {
+ if(ContextPaths.containsParentReference(value))
+ throw new javax.xml.stream.XMLStreamException("Parent reference .. is not allowed");
+ }
+
+ private void assureIsValidKey(String value) throws XMLStreamException {
+ if(!ContextPaths.isValidKey(value))
+ throw new javax.xml.stream.XMLStreamException("Key is not valid");
+ }
+
+ private void assureStartElement(XMLEvent event, String name) throws XMLStreamException {
+ if(!isStartElement(event, name))
+ throw new javax.xml.stream.XMLStreamException("Unexpected start element: " + event.toString() + ", <" + name + "> expected");
+ }
+
+ private void assureEndElement(XMLEvent event, String name) throws XMLStreamException {
+ if(!isEndElement(event, name))
+ throw new javax.xml.stream.XMLStreamException("Unexpected end element: " + event.toString() + ", " + name + "> expected");
+ }
+
+ private boolean isStartElement(XMLEvent event, String name) {
+ if(!event.isStartElement())
+ return false;
+ StartElement element = ((StartElement)event);
+ return element.getName().toString().equals(name);
+ }
+
+ private boolean isEndElement(XMLEvent event, String name) {
+ if(!event.isEndElement())
+ return false;
+ EndElement element = ((EndElement)event);
+ return element.getName().toString().equals(name);
+ }
+}
diff --git a/src/globalization/Translations.xsd b/src/globalization/Translations.xsd
new file mode 100644
index 00000000..aff8ab76
--- /dev/null
+++ b/src/globalization/Translations.xsd
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/globalization/Translator.java b/src/globalization/Translator.java
index da61c595..c6c4569d 100644
--- a/src/globalization/Translator.java
+++ b/src/globalization/Translator.java
@@ -1,13 +1,86 @@
package globalization;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FilenameFilter;
+import java.io.InputStream;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.xml.stream.XMLStreamException;
import org.apache.commons.text.StringSubstitutor;
+import settings.Settings;
+
public class Translator {
+ Map> translations = new HashMap>();
+
+ //XXX: replace singleton pattern by dependency injection?
+ private static Translator instance;
+
+ private Translator() {
+ InitializeTranslations();
+ }
+
+ public static Translator getInstance() {
+ if (instance == null) {
+ instance = new Translator();
+ }
+
+ return instance;
+ }
+
+ private Settings settings() {
+ return Settings.getInstance();
+ }
+
+ private void InitializeTranslations() {
+ String path = this.settings().translationsPath();
+ File dir = new File(path);
+ File [] files = dir.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.endsWith(".xml");
+ }
+ });
+
+ Map> translations = new HashMap<>();
+ TranslationXmlStreamReader translationReader = new TranslationXmlStreamReader();
+ for (File file : files) {
+ Iterable entries = null;
+ try {
+ InputStream stream = new FileInputStream(file);
+ entries = translationReader.ReadFrom(stream);
+ } catch (FileNotFoundException ex) {
+ Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Translation file not found: %s", file), ex);
+ } catch (XMLStreamException ex) {
+ Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Error in translation file: %s", file), ex);
+ }
+
+ for(TranslationEntry entry : entries) {
+ Map localTranslations = translations.get(entry.locale());
+ if(localTranslations == null) {
+ localTranslations = new HashMap<>();
+ translations.put(entry.locale(), localTranslations);
+ }
+
+ if(localTranslations.containsKey(entry.path())) {
+ Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Duplicate entry for locale '%s' and path '%s' in translation file '%s'. Falling back to default translations.", entry.locale(), entry.path(), file));
+ return;
+ }
+ localTranslations.put(entry.path(), entry.template());
+ }
+ }
+
+ // everything is fine, so we store all read translations
+ this.translations = translations;
+ }
+
private Map createMap(Map.Entry[] entries) {
HashMap map = new HashMap<>();
for (AbstractMap.Entry entry : entries) {
@@ -16,37 +89,88 @@ public class Translator {
return map;
}
- //XXX: replace singleton pattern by dependency injection?
- private static Translator instance;
+ public String translate(Locale locale, String contextPath, String keyPath, AbstractMap.Entry... templateValues) {
+ Map map = createMap(templateValues);
+ return translate(locale, contextPath, keyPath, map);
+ }
- public static Translator getInstance() {
- if (instance == null) {
- instance = new Translator();
+ public String translate(String contextPath, String keyPath, AbstractMap.Entry... templateValues) {
+ Map map = createMap(templateValues);
+ return translate(contextPath, keyPath, map);
+ }
+
+ public String translate(Locale locale, String contextPath, String keyPath, Map templateValues) {
+ return translate(locale, contextPath, keyPath, null, templateValues);
+ }
+
+ public String translate(String contextPath, String keyPath, Map templateValues) {
+ return translate(contextPath, keyPath, null, templateValues);
+ }
+
+ public String translate(Locale locale, String contextPath, String keyPath, String defaultTemplate, AbstractMap.Entry... templateValues) {
+ Map map = createMap(templateValues);
+ return translate(locale, contextPath, keyPath, defaultTemplate, map);
+ }
+
+ public String translate(String contextPath, String keyPath, String defaultTemplate, AbstractMap.Entry... templateValues) {
+ Map map = createMap(templateValues);
+ return translate(contextPath, keyPath, defaultTemplate, map);
+ }
+
+ public String translate(Locale locale, String contextPath, String keyPath, String defaultTemplate, Map templateValues) {
+ // look for requested language
+ String template = null;
+ if(locale != null)
+ template = getTemplateFromNearestPath(locale, contextPath, keyPath);
+
+ if(template != null)
+ return substitute(template, templateValues);
+
+ return translate(contextPath, keyPath, defaultTemplate, templateValues);
+ }
+
+ public String translate(String contextPath, String keyPath, String defaultTemplate, Map templateValues) {
+ // scan default languages
+ String template = null;
+ for(String language : this.settings().translationsDefaultLocales()) {
+ Locale defaultLocale = Locale.forLanguageTag(language);
+ template = getTemplateFromNearestPath(defaultLocale, contextPath, keyPath);
+ if(template != null)
+ break;
}
-
- return instance;
+
+ if(template == null)
+ template = defaultTemplate; // fallback template
+
+ return substitute(template, templateValues);
}
- public String translate(Locale locale, String templateKey, AbstractMap.Entry... templateValues) {
- Map map = createMap(templateValues);
- return translate(locale, templateKey, map);
- }
-
- public String translate(Locale locale, String templateKey, Map templateValues) {
- return translate(locale, templateKey, null, templateValues);
- }
-
- public String translate(Locale locale, String templateKey, String defaultTemplate, AbstractMap.Entry... templateValues) {
- Map map = createMap(templateValues);
- return translate(locale, templateKey, defaultTemplate, map);
- }
-
- public String translate(Locale locale, String templateKey, String defaultTemplate, Map templateValues) {
- String template = defaultTemplate; // TODO: get template for the given locale if available
-
+ private String substitute(String template, Map templateValues) {
+ if(templateValues == null)
+ return template;
+
StringSubstitutor sub = new StringSubstitutor(templateValues);
String result = sub.replace(template);
return result;
}
+
+ private String getTemplateFromNearestPath(Locale locale, String contextPath, String keyPath) {
+ Map localTranslations = this.translations.get(locale);
+ if(localTranslations == null)
+ return null;
+
+ String template = null;
+ while(true) {
+ String path = ContextPaths.combinePaths(contextPath, keyPath);
+ template = localTranslations.get(path);
+ if(template != null)
+ break; // found template
+ if(ContextPaths.isRoot(contextPath))
+ break; // nothing found
+ contextPath = ContextPaths.getParent(contextPath);
+ }
+
+ return template;
+ }
}
diff --git a/src/settings/Settings.java b/src/settings/Settings.java
index 288020b6..090f4a21 100644
--- a/src/settings/Settings.java
+++ b/src/settings/Settings.java
@@ -24,11 +24,15 @@ public class Settings {
private int maxBytePerFee = 1024;
private String userpath = "";
- //RPC
+ // RPC
private int rpcPort = 9085;
private List rpcAllowed = new ArrayList(Arrays.asList("127.0.0.1", "::1")); // ipv4, ipv6
private boolean rpcEnabled = true;
+ // Globalization
+ private String translationsPath = "globalization/";
+ private String[] translationsDefaultLocales = {"en"};
+
// Constants
private static final String SETTINGS_FILENAME = "settings.json";
@@ -129,6 +133,17 @@ public class Settings {
{
this.rpcEnabled = ((Boolean) json.get("rpcenabled")).booleanValue();
}
+
+ // Globalization
+ if(json.containsKey("translationspath"))
+ {
+ this.translationsPath = ((String) json.get("translationspath"));
+ }
+
+ if(json.containsKey("translationsdefaultlocales"))
+ {
+ this.translationsDefaultLocales = ((String[]) json.get("translationsdefaultlocales"));
+ }
}
public boolean isTestNet() {
@@ -163,4 +178,14 @@ public class Settings {
{
return this.rpcEnabled;
}
+
+ public String translationsPath()
+ {
+ return this.translationsPath;
+ }
+
+ public String[] translationsDefaultLocales()
+ {
+ return this.translationsDefaultLocales;
+ }
}
diff --git a/src/test/ATTests.java b/src/test/ATTests.java
index 9d5a3a8a..860d381d 100644
--- a/src/test/ATTests.java
+++ b/src/test/ATTests.java
@@ -1,12 +1,11 @@
package test;
-import static org.junit.Assert.*;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import java.math.BigDecimal;
import java.util.Arrays;
-import org.junit.Test;
-
import com.google.common.hash.HashCode;
import data.at.ATStateData;
diff --git a/src/test/BlockTests.java b/src/test/BlockTests.java
index b0485c3a..f70370f8 100644
--- a/src/test/BlockTests.java
+++ b/src/test/BlockTests.java
@@ -1,11 +1,10 @@
package test;
-import static org.junit.Assert.*;
-
import java.math.BigDecimal;
import java.util.List;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import data.block.BlockData;
import data.transaction.TransactionData;
@@ -67,7 +66,7 @@ public class BlockTests extends Common {
// Block 949 has lots of varied transactions
// Blocks 390 & 754 have only payment transactions
BlockData blockData = repository.getBlockRepository().fromHeight(754);
- assertNotNull("Block 754 is required for this test", blockData);
+ assertNotNull(blockData, "Block 754 is required for this test");
Block block = new Block(repository, blockData);
assertTrue(block.isSignatureValid());
@@ -108,7 +107,7 @@ public class BlockTests extends Common {
// Block 949 has lots of varied transactions
// Blocks 390 & 754 have only payment transactions
BlockData blockData = repository.getBlockRepository().fromHeight(754);
- assertNotNull("Block 754 is required for this test", blockData);
+ assertNotNull(blockData, "Block 754 is required for this test");
Block block = new Block(repository, blockData);
assertTrue(block.isSignatureValid());
diff --git a/src/test/BlockchainTests.java b/src/test/BlockchainTests.java
index 264f8773..bdb2662f 100644
--- a/src/test/BlockchainTests.java
+++ b/src/test/BlockchainTests.java
@@ -1,6 +1,7 @@
package test;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import qora.block.BlockChain;
import repository.DataException;
diff --git a/src/test/Common.java b/src/test/Common.java
index e3734218..fe1788ef 100644
--- a/src/test/Common.java
+++ b/src/test/Common.java
@@ -1,7 +1,9 @@
package test;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.AfterAll;
import repository.DataException;
import repository.RepositoryFactory;
@@ -13,13 +15,13 @@ public class Common {
// public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true;sql.pad_space=false";
public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
- @BeforeClass
+ @BeforeAll
public static void setRepository() throws DataException {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
}
- @AfterClass
+ @AfterAll
public static void closeRepository() throws DataException {
RepositoryManager.closeRepositoryFactory();
}
diff --git a/src/test/CompatibilityTests.java b/src/test/CompatibilityTests.java
index 3c630697..d5c3e154 100644
--- a/src/test/CompatibilityTests.java
+++ b/src/test/CompatibilityTests.java
@@ -1,8 +1,7 @@
package test;
-import static org.junit.Assert.*;
-
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import com.google.common.hash.HashCode;
diff --git a/src/test/CryptoTests.java b/src/test/CryptoTests.java
index dfe7299b..b90e335a 100644
--- a/src/test/CryptoTests.java
+++ b/src/test/CryptoTests.java
@@ -1,8 +1,7 @@
package test;
-import static org.junit.Assert.*;
-
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import com.google.common.hash.HashCode;
@@ -16,7 +15,7 @@ public class CryptoTests {
byte[] digest = Crypto.digest(input);
byte[] expected = HashCode.fromString("6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d").asBytes();
- assertArrayEquals(digest, expected);
+ assertArrayEquals(expected, digest);
}
@Test
@@ -25,7 +24,7 @@ public class CryptoTests {
byte[] digest = Crypto.doubleDigest(input);
byte[] expected = HashCode.fromString("1406e05881e299367766d313e26c05564ec91bf721d31726bd6e46e60689539a").asBytes();
- assertArrayEquals(digest, expected);
+ assertArrayEquals(expected, digest);
}
@Test
diff --git a/src/test/ExceptionTests.java b/src/test/ExceptionTests.java
index 37100d53..b0029a8b 100644
--- a/src/test/ExceptionTests.java
+++ b/src/test/ExceptionTests.java
@@ -1,8 +1,8 @@
package test;
-import static org.junit.Assert.*;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
-import org.junit.Test;
import qora.block.Block;
public class ExceptionTests {
diff --git a/src/test/GenesisTests.java b/src/test/GenesisTests.java
index aa2f002b..504c6c93 100644
--- a/src/test/GenesisTests.java
+++ b/src/test/GenesisTests.java
@@ -1,13 +1,12 @@
package test;
-import static org.junit.Assert.*;
-
import java.math.BigDecimal;
import java.util.List;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.AfterAll;
import data.transaction.TransactionData;
import qora.account.Account;
@@ -26,13 +25,13 @@ public class GenesisTests {
public static final String connectionUrl = "jdbc:hsqldb:mem:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true";
- @BeforeClass
+ @BeforeAll
public static void setRepository() throws DataException {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
}
- @AfterClass
+ @AfterAll
public static void closeRepository() throws DataException {
RepositoryManager.closeRepositoryFactory();
}
@@ -40,7 +39,7 @@ public class GenesisTests {
@Test
public void testGenesisBlockTransactions() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
- assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight());
+ assertEquals(0, repository.getBlockRepository().getBlockchainHeight(), "Blockchain should be empty for this test");
GenesisBlock block = new GenesisBlock(repository);
diff --git a/src/test/GlobalizationTests.java b/src/test/GlobalizationTests.java
new file mode 100644
index 00000000..967cba08
--- /dev/null
+++ b/src/test/GlobalizationTests.java
@@ -0,0 +1,171 @@
+package test;
+
+import globalization.TranslationEntry;
+import globalization.TranslationXmlStreamReader;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import javax.xml.stream.XMLStreamException;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import static test.utils.AssertExtensions.*;
+import test.utils.EqualityComparer;
+
+public class GlobalizationTests {
+
+ private class TranslationEntryEqualityComparer implements EqualityComparer {
+
+ @Override
+ public boolean equals(TranslationEntry first, TranslationEntry second) {
+ if(first == null && second == null)
+ return true;
+ if(first == null && second != null || first != null && second == null)
+ return false;
+
+ if(!first.locale().equals(second.locale()))
+ return false;
+ if(!first.path().equals(second.path()))
+ return false;
+ if(!first.template().equals(second.template()))
+ return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode(TranslationEntry item) {
+ int hash = 17;
+ final int multiplier = 59;
+
+ hash = hash * multiplier + item.locale().hashCode();
+ hash = hash * multiplier + item.path().hashCode();
+ hash = hash * multiplier + item.template().hashCode();
+
+ return hash;
+ }
+
+ }
+
+ @Test
+ public void TestTranslationXmlReaderContextPaths() throws XMLStreamException {
+ String xml =
+ "\n" +
+ "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n";
+
+ List expected = new ArrayList<>();
+ expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/key1", "1"));
+ expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/path2/path3/key2", "2"));
+ expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/path4/key3", "3"));
+
+ InputStream is = new ByteArrayInputStream(xml.getBytes(Charset.forName("UTF-8")));
+ TranslationXmlStreamReader reader = new TranslationXmlStreamReader();
+ Iterable actual = reader.ReadFrom(is);
+
+ for(TranslationEntry i:expected)System.out.println(i);for(TranslationEntry i:actual)System.out.println(i);
+ assertItemsEqual(expected, actual, new TranslationEntryEqualityComparer());
+ }
+
+ @Test
+ public void TestTranslationXmlReaderLocales() throws XMLStreamException {
+ String xml =
+ "\n" +
+ "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n";
+
+ List expected = new ArrayList();
+ expected.add(new TranslationEntry(Locale.forLanguageTag("default"), "/key1", "1"));
+ expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/key2", "2"));
+ expected.add(new TranslationEntry(Locale.forLanguageTag("de-DE"), "/path1/path2/key3", "3"));
+
+ InputStream is = new ByteArrayInputStream(xml.getBytes(Charset.forName("UTF-8")));
+ TranslationXmlStreamReader reader = new TranslationXmlStreamReader();
+ Iterable actual = reader.ReadFrom(is);
+
+ assertItemsEqual(expected, actual, new TranslationEntryEqualityComparer());
+ }
+
+ @Test
+ public void TestTranslationXmlReader_BadPath() {
+ String xml =
+ "\n" +
+ "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n";
+
+ InputStream is = new ByteArrayInputStream(xml.getBytes(Charset.forName("UTF-8")));
+ TranslationXmlStreamReader reader = new TranslationXmlStreamReader();
+
+ assertThrows(XMLStreamException.class, () -> reader.ReadFrom(is));
+ }
+
+ @Test
+ public void TestTranslationXmlReader_BadKey1() {
+ String xml =
+ "\n" +
+ "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n";
+
+ InputStream is = new ByteArrayInputStream(xml.getBytes(Charset.forName("UTF-8")));
+ TranslationXmlStreamReader reader = new TranslationXmlStreamReader();
+
+ assertThrows(XMLStreamException.class, () -> reader.ReadFrom(is));
+ }
+
+ @Test
+ public void TestTranslationXmlReader_BadKey2() {
+ String xml =
+ "\n" +
+ "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n";
+
+ InputStream is = new ByteArrayInputStream(xml.getBytes(Charset.forName("UTF-8")));
+ TranslationXmlStreamReader reader = new TranslationXmlStreamReader();
+
+ assertThrows(XMLStreamException.class, () -> reader.ReadFrom(is));
+ }
+}
diff --git a/src/test/LoadTests.java b/src/test/LoadTests.java
index aa617985..ab86a7c7 100644
--- a/src/test/LoadTests.java
+++ b/src/test/LoadTests.java
@@ -1,8 +1,7 @@
package test;
-import static org.junit.Assert.*;
-
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import data.transaction.PaymentTransactionData;
import data.transaction.TransactionData;
@@ -21,25 +20,25 @@ public class LoadTests extends Common {
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionRepository transactionRepository = repository.getTransactionRepository();
- assertTrue("Migrate from old database to at least block 49778 before running this test",
- repository.getBlockRepository().getBlockchainHeight() >= 49778);
+ assertTrue(repository.getBlockRepository().getBlockchainHeight() >= 49778,
+ "Migrate from old database to at least block 49778 before running this test");
String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt";
byte[] signature = Base58.decode(signature58);
TransactionData transactionData = transactionRepository.fromSignature(signature);
- assertNotNull("Transaction data not loaded from repository", transactionData);
- assertEquals("Transaction data not PAYMENT type", TransactionType.PAYMENT, transactionData.getType());
- assertEquals(PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey()), "QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E");
+ assertNotNull(transactionData, "Transaction data not loaded from repository");
+ assertEquals(TransactionType.PAYMENT, transactionData.getType(), "Transaction data not PAYMENT type");
+ assertEquals("QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E", PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey()));
PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData;
assertNotNull(paymentTransactionData);
- assertEquals(PublicKeyAccount.getAddress(paymentTransactionData.getSenderPublicKey()), "QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E");
- assertEquals(paymentTransactionData.getRecipient(), "QZsv8vbJ6QfrBNba4LMp5UtHhAzhrxvVUU");
- assertEquals(paymentTransactionData.getTimestamp(), 1416209264000L);
- assertEquals(Base58.encode(paymentTransactionData.getReference()),
- "31dC6kHHBeG5vYb8LMaZDjLEmhc9kQB2VUApVd8xWncSRiXu7yMejdprjYFMP2rUnzZxWd4KJhkq6LsV7rQvU1kY");
+ assertEquals("QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E", PublicKeyAccount.getAddress(paymentTransactionData.getSenderPublicKey()));
+ assertEquals("QZsv8vbJ6QfrBNba4LMp5UtHhAzhrxvVUU", paymentTransactionData.getRecipient());
+ assertEquals(1416209264000L, paymentTransactionData.getTimestamp());
+ assertEquals("31dC6kHHBeG5vYb8LMaZDjLEmhc9kQB2VUApVd8xWncSRiXu7yMejdprjYFMP2rUnzZxWd4KJhkq6LsV7rQvU1kY",
+ Base58.encode(paymentTransactionData.getReference()));
}
}
@@ -48,8 +47,8 @@ public class LoadTests extends Common {
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionRepository transactionRepository = repository.getTransactionRepository();
- assertTrue("Migrate from old database to at least block 49778 before running this test",
- repository.getBlockRepository().getBlockchainHeight() >= 49778);
+ assertTrue(repository.getBlockRepository().getBlockchainHeight() >= 49778,
+ "Migrate from old database to at least block 49778 before running this test");
String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt";
byte[] signature = Base58.decode(signature58);
diff --git a/src/test/NavigationTests.java b/src/test/NavigationTests.java
index 4ebdbc24..952a59c0 100644
--- a/src/test/NavigationTests.java
+++ b/src/test/NavigationTests.java
@@ -1,8 +1,7 @@
package test;
-import static org.junit.Assert.*;
-
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import data.block.BlockData;
import data.transaction.TransactionData;
@@ -20,8 +19,8 @@ public class NavigationTests extends Common {
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionRepository transactionRepository = repository.getTransactionRepository();
- assertTrue("Migrate from old database to at least block 49778 before running this test",
- repository.getBlockRepository().getBlockchainHeight() >= 49778);
+ assertTrue(repository.getBlockRepository().getBlockchainHeight() >= 49778,
+ "Migrate from old database to at least block 49778 before running this test");
String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt";
byte[] signature = Base58.decode(signature58);
@@ -29,11 +28,11 @@ public class NavigationTests extends Common {
System.out.println("Navigating to Block from transaction " + signature58);
TransactionData transactionData = transactionRepository.fromSignature(signature);
- assertNotNull("Transaction data not loaded from repository", transactionData);
- assertEquals("Transaction data not PAYMENT type", TransactionType.PAYMENT, transactionData.getType());
+ assertNotNull(transactionData, "Transaction data not loaded from repository");
+ assertEquals(TransactionType.PAYMENT, transactionData.getType(), "Transaction data not PAYMENT type");
BlockData blockData = transactionRepository.getBlockDataFromSignature(signature);
- assertNotNull("Block 49778 not loaded from database", blockData);
+ assertNotNull(blockData, "Block 49778 not loaded from database");
System.out.println("Block " + blockData.getHeight() + ", signature: " + Base58.encode(blockData.getSignature()));
diff --git a/src/test/RepositoryTests.java b/src/test/RepositoryTests.java
index a37b71c6..0658d98a 100644
--- a/src/test/RepositoryTests.java
+++ b/src/test/RepositoryTests.java
@@ -1,10 +1,10 @@
package test;
-import static org.junit.Assert.*;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.junit.Test;
import repository.DataException;
import repository.Repository;
diff --git a/src/test/SaveTests.java b/src/test/SaveTests.java
index 1338e3b8..80bbf74e 100644
--- a/src/test/SaveTests.java
+++ b/src/test/SaveTests.java
@@ -3,7 +3,8 @@ package test;
import java.math.BigDecimal;
import java.time.Instant;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import data.transaction.PaymentTransactionData;
import qora.account.PublicKeyAccount;
diff --git a/src/test/SerializationTests.java b/src/test/SerializationTests.java
index ede95240..61a1b0e8 100644
--- a/src/test/SerializationTests.java
+++ b/src/test/SerializationTests.java
@@ -1,12 +1,11 @@
package test;
-import static org.junit.Assert.*;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import java.util.Arrays;
import java.util.List;
-import org.junit.Test;
-
import data.block.BlockData;
import data.transaction.GenesisTransactionData;
import data.transaction.TransactionData;
@@ -61,15 +60,15 @@ public class SerializationTests extends Common {
TransactionData parsedTransactionData = TransactionTransformer.fromBytes(bytes);
- assertTrue("Transaction signature mismatch", Arrays.equals(transactionData.getSignature(), parsedTransactionData.getSignature()));
+ assertTrue(Arrays.equals(transactionData.getSignature(), parsedTransactionData.getSignature()), "Transaction signature mismatch");
- assertEquals("Data length mismatch", TransactionTransformer.getDataLength(transactionData), bytes.length);
+ assertEquals(bytes.length, TransactionTransformer.getDataLength(transactionData), "Data length mismatch");
}
private void testSpecificBlockTransactions(int height, TransactionType type) throws DataException, TransformationException {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(height);
- assertNotNull("Block " + height + " is required for this test", blockData);
+ assertNotNull(blockData, "Block " + height + " is required for this test");
Block block = new Block(repository, blockData);
diff --git a/src/test/SignatureTests.java b/src/test/SignatureTests.java
index 2aa3f963..bc15ee14 100644
--- a/src/test/SignatureTests.java
+++ b/src/test/SignatureTests.java
@@ -1,11 +1,10 @@
package test;
-import static org.junit.Assert.*;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
import java.math.BigDecimal;
-import org.junit.Test;
-
import data.block.BlockData;
import qora.account.PrivateKeyAccount;
import qora.block.Block;
diff --git a/src/test/TransactionTests.java b/src/test/TransactionTests.java
index df8a17da..cb73ccb2 100644
--- a/src/test/TransactionTests.java
+++ b/src/test/TransactionTests.java
@@ -1,6 +1,8 @@
package test;
-import static org.junit.Assert.*;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.AfterEach;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
@@ -10,8 +12,6 @@ import java.util.Arrays;
import java.util.List;
import org.json.simple.JSONObject;
-import org.junit.After;
-import org.junit.Test;
import com.google.common.hash.HashCode;
@@ -97,7 +97,7 @@ public class TransactionTests {
RepositoryManager.setRepositoryFactory(repositoryFactory);
try (final Repository repository = RepositoryManager.getRepository()) {
- assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight());
+ assertEquals(0, repository.getBlockRepository().getBlockchainHeight(), "Blockchain should be empty for this test");
}
// [Un]set genesis timestamp as required by test
@@ -136,7 +136,7 @@ public class TransactionTests {
repository.saveChanges();
}
- @After
+ @AfterEach
public void closeRepository() throws DataException {
RepositoryManager.closeRepositoryFactory();
}
@@ -176,8 +176,8 @@ public class TransactionTests {
block.addTransaction(paymentTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -185,21 +185,21 @@ public class TransactionTests {
// Check sender's balance
BigDecimal expectedBalance = initialSenderBalance.subtract(amount).subtract(fee);
BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance();
- assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect");
// Fee should be in generator's balance
expectedBalance = initialGeneratorBalance.add(fee);
actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance();
- assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect");
// Amount should be in recipient's balance
expectedBalance = amount;
actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance();
- assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Recipient's new balance incorrect");
// Check recipient's reference
byte[] recipientsReference = recipient.getLastReference();
- assertTrue("Recipient's new reference incorrect", Arrays.equals(paymentTransaction.getTransactionData().getSignature(), recipientsReference));
+ assertTrue(Arrays.equals(paymentTransaction.getTransactionData().getSignature(), recipientsReference), "Recipient's new reference incorrect");
// Orphan block
block.orphan();
@@ -207,11 +207,11 @@ public class TransactionTests {
// Check sender's balance
actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance();
- assertTrue("Sender's reverted balance incorrect", initialSenderBalance.compareTo(actualBalance) == 0);
+ assertTrue(initialSenderBalance.compareTo(actualBalance) == 0, "Sender's reverted balance incorrect");
// Check generator's balance
actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance();
- assertTrue("Generator's new balance incorrect", initialGeneratorBalance.compareTo(actualBalance) == 0);
+ assertTrue(initialGeneratorBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect");
}
@Test
@@ -237,8 +237,8 @@ public class TransactionTests {
block.addTransaction(registerNameTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -246,19 +246,19 @@ public class TransactionTests {
// Check sender's balance
BigDecimal expectedBalance = initialSenderBalance.subtract(fee);
BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance();
- assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect");
// Fee should be in generator's balance
expectedBalance = initialGeneratorBalance.add(fee);
actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance();
- assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect");
// Check name was registered
NameData actualNameData = this.repository.getNameRepository().fromName(name);
assertNotNull(actualNameData);
// Check sender's reference
- assertTrue("Sender's new reference incorrect", Arrays.equals(registerNameTransactionData.getSignature(), sender.getLastReference()));
+ assertTrue(Arrays.equals(registerNameTransactionData.getSignature(), sender.getLastReference()), "Sender's new reference incorrect");
// Update variables for use by other tests
reference = sender.getLastReference();
@@ -293,8 +293,8 @@ public class TransactionTests {
block.addTransaction(updateNameTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -338,8 +338,8 @@ public class TransactionTests {
block.addTransaction(sellNameTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -389,8 +389,8 @@ public class TransactionTests {
block.addTransaction(cancelSellNameTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -455,8 +455,8 @@ public class TransactionTests {
block.addTransaction(buyNameTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -508,8 +508,8 @@ public class TransactionTests {
block.addTransaction(createPollTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -517,19 +517,19 @@ public class TransactionTests {
// Check sender's balance
BigDecimal expectedBalance = initialSenderBalance.subtract(fee);
BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance();
- assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect");
// Fee should be in generator's balance
expectedBalance = initialGeneratorBalance.add(fee);
actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance();
- assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect");
// Check poll was created
PollData actualPollData = this.repository.getVotingRepository().fromPollName(pollName);
assertNotNull(actualPollData);
// Check sender's reference
- assertTrue("Sender's new reference incorrect", Arrays.equals(createPollTransactionData.getSignature(), sender.getLastReference()));
+ assertTrue(Arrays.equals(createPollTransactionData.getSignature(), sender.getLastReference()), "Sender's new reference incorrect");
// Update variables for use by other tests
reference = sender.getLastReference();
@@ -567,8 +567,8 @@ public class TransactionTests {
block.addTransaction(voteOnPollTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -588,10 +588,10 @@ public class TransactionTests {
List votes = repository.getVotingRepository().getVotes(pollName);
assertNotNull(votes);
- assertEquals("Only one vote expected", 1, votes.size());
+ assertEquals(1, votes.size(), "Only one vote expected");
- assertEquals("Wrong vote option index", pollOptionsSize - 1, votes.get(0).getOptionIndex());
- assertTrue("Wrong voter public key", Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey()));
+ assertEquals(pollOptionsSize - 1, votes.get(0).getOptionIndex(), "Wrong vote option index");
+ assertTrue(Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey()), "Wrong voter public key");
// Orphan last block
BlockData lastBlockData = repository.getBlockRepository().getLastBlock();
@@ -603,10 +603,10 @@ public class TransactionTests {
votes = repository.getVotingRepository().getVotes(pollName);
assertNotNull(votes);
- assertEquals("Only one vote expected", 1, votes.size());
+ assertEquals(1, votes.size(), "Only one vote expected");
- assertEquals("Wrong vote option index", pollOptionsSize - 1 - 1, votes.get(0).getOptionIndex());
- assertTrue("Wrong voter public key", Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey()));
+ assertEquals(pollOptionsSize - 1 - 1, votes.get(0).getOptionIndex(), "Wrong vote option index");
+ assertTrue(Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey()), "Wrong voter public key");
}
@Test
@@ -634,8 +634,8 @@ public class TransactionTests {
block.addTransaction(issueAssetTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -643,12 +643,12 @@ public class TransactionTests {
// Check sender's balance
BigDecimal expectedBalance = initialSenderBalance.subtract(fee);
BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance();
- assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect");
// Fee should be in generator's balance
expectedBalance = initialGeneratorBalance.add(fee);
actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance();
- assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect");
// Check we now have an assetId
Long assetId = issueAssetTransactionData.getAssetId();
@@ -672,11 +672,11 @@ public class TransactionTests {
// Check sender's balance
actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance();
- assertTrue("Sender's reverted balance incorrect", initialSenderBalance.compareTo(actualBalance) == 0);
+ assertTrue(initialSenderBalance.compareTo(actualBalance) == 0, "Sender's reverted balance incorrect");
// Check generator's balance
actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance();
- assertTrue("Generator's reverted balance incorrect", initialGeneratorBalance.compareTo(actualBalance) == 0);
+ assertTrue(initialGeneratorBalance.compareTo(actualBalance) == 0, "Generator's reverted balance incorrect");
// Check asset no longer exists
assertFalse(assetRepo.assetExists(assetId));
@@ -724,8 +724,8 @@ public class TransactionTests {
block.addTransaction(transferAssetTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -733,12 +733,12 @@ public class TransactionTests {
// Check sender's balance
BigDecimal expectedBalance = originalSenderBalance.subtract(fee);
BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance();
- assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect");
// Fee should be in generator's balance
expectedBalance = originalGeneratorBalance.add(fee);
actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance();
- assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect");
// Check asset balances
BigDecimal actualSenderAssetBalance = sender.getConfirmedBalance(assetId);
@@ -756,11 +756,11 @@ public class TransactionTests {
// Check sender's balance
actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance();
- assertTrue("Sender's reverted balance incorrect", originalSenderBalance.compareTo(actualBalance) == 0);
+ assertTrue(originalSenderBalance.compareTo(actualBalance) == 0, "Sender's reverted balance incorrect");
// Check generator's balance
actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance();
- assertTrue("Generator's reverted balance incorrect", originalGeneratorBalance.compareTo(actualBalance) == 0);
+ assertTrue(originalGeneratorBalance.compareTo(actualBalance) == 0, "Generator's reverted balance incorrect");
// Check asset balances
actualSenderAssetBalance = sender.getConfirmedBalance(assetId);
@@ -828,8 +828,8 @@ public class TransactionTests {
block.addTransaction(createOrderTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -909,8 +909,8 @@ public class TransactionTests {
block.addTransaction(cancelOrderTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -984,8 +984,8 @@ public class TransactionTests {
block.addTransaction(createOrderTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -998,7 +998,7 @@ public class TransactionTests {
// Check order has trades
List trades = assetRepo.getOrdersTrades(orderId);
assertNotNull(trades);
- assertEquals("Trade didn't happen", 1, trades.size());
+ assertEquals(1, trades.size(), "Trade didn't happen");
TradeData tradeData = trades.get(0);
// Check trade has correct values
@@ -1093,20 +1093,20 @@ public class TransactionTests {
block.addTransaction(multiPaymentTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
// Check sender's balance
BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance();
- assertTrue("Sender's new balance incorrect", expectedSenderBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedSenderBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect");
// Fee should be in generator's balance
BigDecimal expectedBalance = initialGeneratorBalance.add(fee);
actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance();
- assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect");
// Check recipients
for (int i = 0; i < payments.size(); ++i) {
@@ -1114,12 +1114,12 @@ public class TransactionTests {
Account recipient = new Account(this.repository, paymentData.getRecipient());
byte[] recipientsReference = recipient.getLastReference();
- assertTrue("Recipient's new reference incorrect", Arrays.equals(multiPaymentTransaction.getTransactionData().getSignature(), recipientsReference));
+ assertTrue(Arrays.equals(multiPaymentTransaction.getTransactionData().getSignature(), recipientsReference), "Recipient's new reference incorrect");
// Amount should be in recipient's balance
expectedBalance = paymentData.getAmount();
actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance();
- assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Recipient's new balance incorrect");
}
@@ -1129,11 +1129,11 @@ public class TransactionTests {
// Check sender's balance
actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance();
- assertTrue("Sender's reverted balance incorrect", initialSenderBalance.compareTo(actualBalance) == 0);
+ assertTrue(initialSenderBalance.compareTo(actualBalance) == 0, "Sender's reverted balance incorrect");
// Check generator's balance
actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance();
- assertTrue("Generator's new balance incorrect", initialGeneratorBalance.compareTo(actualBalance) == 0);
+ assertTrue(initialGeneratorBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect");
}
@Test
@@ -1163,8 +1163,8 @@ public class TransactionTests {
block.addTransaction(messageTransactionData);
block.sign();
- assertTrue("Block signatures invalid", block.isSignatureValid());
- assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
+ assertTrue(block.isSignatureValid(), "Block signatures invalid");
+ assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid");
block.process();
repository.saveChanges();
@@ -1172,17 +1172,17 @@ public class TransactionTests {
// Check sender's balance
BigDecimal expectedBalance = initialSenderBalance.subtract(amount).subtract(fee);
BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance();
- assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect");
// Fee should be in generator's balance
expectedBalance = initialGeneratorBalance.add(fee);
actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance();
- assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect");
// Amount should be in recipient's balance
expectedBalance = amount;
actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance();
- assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0);
+ assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Recipient's new balance incorrect");
}
}
\ No newline at end of file
diff --git a/src/test/utils/AssertExtensions.java b/src/test/utils/AssertExtensions.java
new file mode 100644
index 00000000..fabce6a0
--- /dev/null
+++ b/src/test/utils/AssertExtensions.java
@@ -0,0 +1,42 @@
+package test.utils;
+
+import com.google.common.collect.Iterables;
+import java.lang.reflect.Array;
+import java.lang.Class;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class AssertExtensions {
+
+ public static void assertItemsEqual(Collection expected, Iterable actual, EqualityComparer comparer) {
+ assertItemsEqual(expected, actual, comparer, (String)null);
+ }
+
+ public static void assertItemsEqual(Collection expected, Iterable actual, EqualityComparer comparer, String message) {
+ List> expectedSet = new ArrayList>();
+ for(T item: expected)
+ expectedSet.add(new EquatableWrapper(item, comparer));
+
+ List> actualSet = new ArrayList>();
+ for(T item: actual)
+ actualSet.add(new EquatableWrapper(item, comparer));
+
+ assertItemsEqual(expectedSet, actualSet, message);
+ }
+
+ public static void assertItemsEqual(Collection expected, Iterable actual) {
+ assertItemsEqual(expected, actual, (String)null);
+ }
+
+ public static void assertItemsEqual(Collection expected, Iterable actual, String message) {
+ List list = new ArrayList();
+ T[] expectedArray = (T[])expected.toArray();
+ assertThat(message, actual, containsInAnyOrder(expectedArray));
+ }
+}
diff --git a/src/test/utils/EqualityComparer.java b/src/test/utils/EqualityComparer.java
new file mode 100644
index 00000000..c560c9ce
--- /dev/null
+++ b/src/test/utils/EqualityComparer.java
@@ -0,0 +1,6 @@
+package test.utils;
+
+public interface EqualityComparer {
+ boolean equals(T first, T second);
+ int hashCode(T item);
+}
diff --git a/src/test/utils/EquatableWrapper.java b/src/test/utils/EquatableWrapper.java
new file mode 100644
index 00000000..b719cf32
--- /dev/null
+++ b/src/test/utils/EquatableWrapper.java
@@ -0,0 +1,34 @@
+package test.utils;
+
+class EquatableWrapper {
+
+ private final T item;
+ private final EqualityComparer comparer;
+
+ public EquatableWrapper(T item, EqualityComparer comparer) {
+ this.item = item;
+ this.comparer = comparer;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if(obj == null)
+ return false;
+ if (!(this.getClass().isInstance(obj)))
+ return false;
+ EquatableWrapper otherWrapper = (EquatableWrapper)obj;
+ if (otherWrapper.item == this.item)
+ return true;
+ return this.comparer.equals(this.item, otherWrapper.item);
+ }
+
+ @Override
+ public int hashCode() {
+ return this.comparer.hashCode(this.item);
+ }
+
+ @Override
+ public String toString() {
+ return this.item.toString();
+ }
+}
\ No newline at end of file