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() + ", 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