From 646462942c360d3418908a3f4f0f0b8876770f5c Mon Sep 17 00:00:00 2001 From: Kc Date: Mon, 24 Sep 2018 00:21:47 +0200 Subject: [PATCH] ADDED: globalization.Translator - basic globalization support (implementation needed) ADDED: api.Security (implementation needed) ADDED: api.APIErrorFactory CHANGED: added command execution to ApiClient --- pom.xml | 10 + src/Start.java | 28 +- src/api/ApiClient.java | 295 ++++++++++------- src/api/ApiError.java | 120 +++++++ src/api/ApiErrorFactory.java | 181 +++++++++++ src/api/ApiErrorMessage.java | 22 ++ src/api/ApiException.java | 36 +++ src/api/ApiService.java | 121 +++---- src/api/BlocksResource.java | 518 +++++++++++++++--------------- src/api/Security.java | 8 +- src/globalization/Translator.java | 52 +++ 11 files changed, 938 insertions(+), 453 deletions(-) create mode 100644 src/api/ApiError.java create mode 100644 src/api/ApiErrorFactory.java create mode 100644 src/api/ApiErrorMessage.java create mode 100644 src/api/ApiException.java create mode 100644 src/globalization/Translator.java diff --git a/pom.xml b/pom.xml index d1773583..1154fb5d 100644 --- a/pom.xml +++ b/pom.xml @@ -90,5 +90,15 @@ swagger-jaxrs2-servlet-initializer 2.0.4 + + org.apache.commons + commons-text + 1.4 + + + org.glassfish.jersey.media + jersey-media-moxy + 2.27 + \ No newline at end of file diff --git a/src/Start.java b/src/Start.java index 9b0fce81..585f65c6 100644 --- a/src/Start.java +++ b/src/Start.java @@ -6,20 +6,20 @@ import repository.RepositoryFactory; import repository.RepositoryManager; 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"; - public static void main(String args[]) throws DataException - { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl); - RepositoryManager.setRepositoryFactory(repositoryFactory); - - ApiService apiService = new ApiService(); - apiService.start(); - - ApiClient client = new ApiClient(apiService); - String test = client.executeCommand("help ALL"); - System.out.println(test); - } + 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"; + + public static void main(String args[]) throws DataException { + RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl); + RepositoryManager.setRepositoryFactory(repositoryFactory); + + ApiService apiService = ApiService.getInstance(); + apiService.start(); + + //// testing the API client + //ApiClient client = ApiClient.getInstance(); + //String test = client.executeCommand("GET blocks/height"); + //System.out.println(test); + } } diff --git a/src/api/ApiClient.java b/src/api/ApiClient.java index 0d989215..49d9cd8a 100644 --- a/src/api/ApiClient.java +++ b/src/api/ApiClient.java @@ -1,5 +1,6 @@ package api; +import globalization.Translator; import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource; import io.swagger.v3.oas.annotations.Operation; import java.lang.annotation.Annotation; @@ -7,6 +8,7 @@ import java.lang.reflect.Method; 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; @@ -16,133 +18,184 @@ import javax.ws.rs.PUT; import javax.ws.rs.PATCH; import javax.ws.rs.DELETE; import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.apache.commons.lang3.StringUtils; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import settings.Settings; public class ApiClient { - private class HelpString - { - public final Pattern pattern; - public final String fullPath; - public final String description; - - public HelpString(Pattern pattern, String fullPath, String description) - { - this.pattern = pattern; - this.fullPath = fullPath; - this.description = description; - } - } - - private static final Pattern HELP_COMMAND_PATTERN = Pattern.compile("^ *help *(?.*)$", Pattern.CASE_INSENSITIVE); - private static final List> HTTP_METHOD_ANNOTATIONS = Arrays.asList( - GET.class, - POST.class, - PUT.class, - PATCH.class, - DELETE.class - ); - - ApiService apiService; - List helpStrings; - - public ApiClient(ApiService apiService) - { - this.apiService = apiService; - this.helpStrings = getHelpStrings(apiService.getResources()); - } - private List getHelpStrings(Iterable> resources) - { - List result = new ArrayList<>(); - - // scan each resource class - for (Class resource : resources) { - if(OpenApiResource.class.isAssignableFrom(resource)) - continue; // ignore swagger resources - - Path resourcePath = resource.getDeclaredAnnotation(Path.class); - if(resourcePath == null) - continue; - - String resourcePathString = resourcePath.value(); - - // scan each method - for(Method method : resource.getDeclaredMethods()) - { - Operation operationAnnotation = method.getAnnotation(Operation.class); - if(operationAnnotation == null) - continue; - - String description = operationAnnotation.description(); - - Path methodPath = method.getDeclaredAnnotation(Path.class); - String methodPathString = (methodPath != null) ? methodPath.value() : ""; + private class HelpString { - // scan for each potential http method - for(Class restMethodAnnotation : HTTP_METHOD_ANNOTATIONS) - { - Annotation annotation = method.getDeclaredAnnotation(restMethodAnnotation); - if(annotation == null) - continue; + public final Pattern pattern; + public final String fullPath; + public final String description; - HttpMethod httpMethod = annotation.annotationType().getDeclaredAnnotation(HttpMethod.class); - String httpMethodString = httpMethod.value(); + public HelpString(Pattern pattern, String fullPath, String description) { + this.pattern = pattern; + this.fullPath = fullPath; + this.description = description; + } + } - String fullPath = httpMethodString + " " + resourcePathString + methodPathString; - Pattern pattern = Pattern.compile("^ *(" + httpMethodString + " *)?" + getHelpPatternForPath(resourcePathString + methodPathString)); - result.add(new HelpString(pattern, fullPath, description)); - } - } - } - - // sort by path - result.sort((h1, h2)-> h1.fullPath.compareTo(h2.fullPath)); - - return result; - } - - private String getHelpPatternForPath(String path) - { - path = path - .replaceAll("\\.", "\\.") // escapes "." as "\." - .replaceAll("\\{.*?\\}", ".*?"); // replace placeholders "{...}" by the "ungreedy match anything" pattern ".*?" + 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( + GET.class, + POST.class, + PUT.class, + PATCH.class, + DELETE.class + ); - // arrange the regex pattern so that it also matches partial - StringBuilder result = new StringBuilder(); - String[] parts = path.split("/"); - for(int i = 0; i < parts.length; i++) - { - if(i!=0) - result.append("(/"); // opening bracket - result.append(parts[i]); - } - for(int i = 0; i < parts.length - 1; i++) - result.append(")?"); // closing bracket - return result.toString(); - } - - public String executeCommand(String command) - { - final Matcher helpMatch = HELP_COMMAND_PATTERN.matcher(command); - if(helpMatch.matches()) - { - command = helpMatch.group("command"); - StringBuilder result = new StringBuilder(); - - boolean showAll = command.trim().equalsIgnoreCase("all"); - for(HelpString helpString : helpStrings) - { - if(showAll || helpString.pattern.matcher(command).matches()) - appendHelp(result, helpString); - } - - return result.toString(); - } - - return null; - } + ApiService apiService; + private Translator translator; + List helpStrings; - private void appendHelp(StringBuilder builder, HelpString helpString) { - builder.append(helpString.fullPath + "\n"); - builder.append(helpString.description + "\n"); - } + public ApiClient(ApiService apiService, Translator translator) { + this.apiService = apiService; + this.helpStrings = getHelpStrings(apiService.getResources()); + } + + //XXX: replace singleton pattern by dependency injection? + private static ApiClient instance; + + public static ApiClient getInstance() { + if (instance == null) { + instance = new ApiClient(ApiService.getInstance(), Translator.getInstance()); + } + + return instance; + } + + private List getHelpStrings(Iterable> resources) { + List result = new ArrayList<>(); + + // scan each resource class + for (Class resource : resources) { + if (OpenApiResource.class.isAssignableFrom(resource)) { + continue; // ignore swagger resources + } + Path resourcePath = resource.getDeclaredAnnotation(Path.class); + if (resourcePath == null) { + continue; + } + + String resourcePathString = resourcePath.value(); + + // scan each method + for (Method method : resource.getDeclaredMethods()) { + Operation operationAnnotation = method.getAnnotation(Operation.class); + if (operationAnnotation == null) { + continue; + } + + String description = operationAnnotation.description(); + + Path methodPath = method.getDeclaredAnnotation(Path.class); + String methodPathString = (methodPath != null) ? methodPath.value() : ""; + + // scan for each potential http method + for (Class restMethodAnnotation : HTTP_METHOD_ANNOTATIONS) { + Annotation annotation = method.getDeclaredAnnotation(restMethodAnnotation); + if (annotation == null) { + continue; + } + + HttpMethod httpMethod = annotation.annotationType().getDeclaredAnnotation(HttpMethod.class); + String httpMethodString = httpMethod.value(); + + String fullPath = httpMethodString + " " + resourcePathString + methodPathString; + Pattern pattern = Pattern.compile("^ *(" + httpMethodString + " *)?" + getHelpPatternForPath(resourcePathString + methodPathString)); + result.add(new HelpString(pattern, fullPath, description)); + } + } + } + + // sort by path + result.sort((h1, h2) -> h1.fullPath.compareTo(h2.fullPath)); + + return result; + } + + private String getHelpPatternForPath(String path) { + path = path + .replaceAll("\\.", "\\.") // escapes "." as "\." + .replaceAll("\\{.*?\\}", ".*?"); // replace placeholders "{...}" by the "ungreedy match anything" pattern ".*?" + + // arrange the regex pattern so that it also matches partial + StringBuilder result = new StringBuilder(); + String[] parts = path.split("/"); + for (int i = 0; i < parts.length; i++) { + if (i != 0) { + result.append("(/"); // opening bracket + } + result.append(parts[i]); + } + for (int i = 0; i < parts.length - 1; i++) { + result.append(")?"); // closing bracket + } + return result.toString(); + } + + public String executeCommand(String command) { + // check if this is a help command + Matcher match = HELP_COMMAND_PATTERN.matcher(command); + if (match.matches()) { + command = match.group("command"); + StringBuilder result = new StringBuilder(); + + boolean showAll = command.trim().equalsIgnoreCase("all"); + for (HelpString helpString : helpStrings) { + if (showAll || helpString.pattern.matcher(command).matches()) { + appendHelp(result, helpString); + } + } + + return result.toString(); + } + + + 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."); + + // send the command to the API service + String method = match.group("method"); + String path = match.group("path"); + String url = String.format("http://127.0.0.1:%d/%s", Settings.getInstance().getRpcPort(), path); + + Client client = ClientBuilder.newClient(); + client.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true); // workaround for non-standard HTTP methods like PATCH + WebTarget wt = client.target(url); + Invocation.Builder builder = wt.request(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN); + Response response = builder.method(method); + + // send back result + final String body = response.readEntity(String.class); + 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); + } + result.append("\nType help to get a list of commands."); + } else { + result.append(body); + } + return result.toString(); + } + + private void appendHelp(StringBuilder builder, HelpString helpString) { + builder.append(helpString.fullPath + "\n"); + builder.append(helpString.description + "\n"); + } } diff --git a/src/api/ApiError.java b/src/api/ApiError.java new file mode 100644 index 00000000..471fa9f2 --- /dev/null +++ b/src/api/ApiError.java @@ -0,0 +1,120 @@ + +package api; + +public enum ApiError { + //COMMON + UNKNOWN(0, 500), + JSON(1, 400), + NO_BALANCE(2, 422), + NOT_YET_RELEASED(3, 422), + + //VALIDATION + INVALID_SIGNATURE(101, 400), + INVALID_ADDRESS(102, 400), + INVALID_SEED(103, 400), + INVALID_AMOUNT(104, 400), + INVALID_FEE(105, 400), + INVALID_SENDER(106, 400), + INVALID_RECIPIENT(107, 400), + INVALID_NAME_LENGTH(108, 400), + INVALID_VALUE_LENGTH(109, 400), + INVALID_NAME_OWNER(110, 400), + INVALID_BUYER(111, 400), + INVALID_PUBLIC_KEY(112, 400), + INVALID_OPTIONS_LENGTH(113, 400), + INVALID_OPTION_LENGTH(114, 400), + INVALID_DATA(115, 400), + INVALID_DATA_LENGTH(116, 400), + INVALID_UPDATE_VALUE(117, 400), + KEY_ALREADY_EXISTS(118, 422), + KEY_NOT_EXISTS(119, 404), + LAST_KEY_IS_DEFAULT_KEY_ERROR(120, 422), + FEE_LESS_REQUIRED(121, 422), + WALLET_NOT_IN_SYNC(122, 422), + INVALID_NETWORK_ADDRESS(123, 404), + + //WALLET + WALLET_NO_EXISTS(201, 404), + WALLET_ADDRESS_NO_EXISTS(202, 404), + WALLET_LOCKED(203, 422), + WALLET_ALREADY_EXISTS(204, 422), + WALLET_API_CALL_FORBIDDEN_BY_USER(205, 403), + + //BLOCKS + BLOCK_NO_EXISTS(301, 404), + + //TRANSACTIONS + TRANSACTION_NO_EXISTS(311, 404), + PUBLIC_KEY_NOT_FOUND(304, 404), + + //NAMING + NAME_NO_EXISTS(401, 404), + NAME_ALREADY_EXISTS(402, 422), + NAME_ALREADY_FOR_SALE(403, 422), + NAME_NOT_LOWER_CASE(404, 422), + NAME_SALE_NO_EXISTS(410, 404), + BUYER_ALREADY_OWNER(411, 422), + + //POLLS + POLL_NO_EXISTS(501, 404), + POLL_ALREADY_EXISTS(502, 422), + DUPLICATE_OPTION(503, 422), + POLL_OPTION_NO_EXISTS(504, 404), + ALREADY_VOTED_FOR_THAT_OPTION(505, 422), + + //ASSET + INVALID_ASSET_ID(601, 400), + + //NAME PAYMENTS + NAME_NOT_REGISTERED(701, 422), + NAME_FOR_SALE(702, 422), + NAME_WITH_SPACE(703, 422), + + //ATs + INVALID_DESC_LENGTH(801, 400), + EMPTY_CODE(802, 400), + DATA_SIZE(803, 400), + NULL_PAGES(804, 400), + INVALID_TYPE_LENGTH(805, 400), + INVALID_TAGS_LENGTH(806, 400), + INVALID_CREATION_BYTES(809, 400), + + //BLOG/Namestorage + BODY_EMPTY(901, 400), + BLOG_DISABLED(902, 403), + NAME_NOT_OWNER(903, 422), + TX_AMOUNT(904, 400), + BLOG_ENTRY_NO_EXISTS(905, 404), + BLOG_EMPTY(906, 404), + POSTID_EMPTY(907, 400), + POST_NOT_EXISTING(908, 404), + COMMENTING_DISABLED(909, 403), + COMMENT_NOT_EXISTING(910, 404), + INVALID_COMMENT_OWNER(911, 422), + + //Messages + MESSAGE_FORMAT_NOT_HEX(1001, 400), + MESSAGE_BLANK(1002, 400), + NO_PUBLIC_KEY(1003, 422), + MESSAGESIZE_EXCEEDED(1004, 400); + + private final int code; // API error code + private final int status; // HTTP status code + + private ApiError(int code) { + this(code, 400); // defaults to "400 - BAD REQUEST" + } + + private ApiError(int code, int status) { + this.code = code; + this.status = status; + } + + int getCode() { + return this.code; + } + + int getStatus() { + return this.status; + } +} \ No newline at end of file diff --git a/src/api/ApiErrorFactory.java b/src/api/ApiErrorFactory.java new file mode 100644 index 00000000..ad7c7f7e --- /dev/null +++ b/src/api/ApiErrorFactory.java @@ -0,0 +1,181 @@ +package api; + +import globalization.Translator; +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public class ApiErrorFactory { + + private class ErrorMessageEntry { + + String templateKey; + String defaultTemplate; + AbstractMap.Entry[] templateValues; + + public ErrorMessageEntry(String templateKey, String defaultTemplate, AbstractMap.Entry... templateValues) { + this.templateKey = templateKey; + this.defaultTemplate = defaultTemplate; + this.templateValues = templateValues; + } + } + + private Translator translator; + private Map errorMessages; + + public ApiErrorFactory(Translator translator) { + this.translator = translator; + + this.errorMessages = new HashMap(); + + //COMMON + this.errorMessages.put(ApiError.UNKNOWN, createErrorMessageEntry(ApiError.UNKNOWN, "unknown error")); + this.errorMessages.put(ApiError.JSON, createErrorMessageEntry(ApiError.JSON, "failed to parse json message")); + this.errorMessages.put(ApiError.NO_BALANCE, createErrorMessageEntry(ApiError.NO_BALANCE, "not enough balance")); + this.errorMessages.put(ApiError.NOT_YET_RELEASED, createErrorMessageEntry(ApiError.NOT_YET_RELEASED, "that feature is not yet released")); + + //VALIDATION + this.errorMessages.put(ApiError.INVALID_SIGNATURE, createErrorMessageEntry(ApiError.INVALID_SIGNATURE, "invalid signature")); + this.errorMessages.put(ApiError.INVALID_ADDRESS, createErrorMessageEntry(ApiError.INVALID_ADDRESS, "invalid address")); + this.errorMessages.put(ApiError.INVALID_SEED, createErrorMessageEntry(ApiError.INVALID_SEED, "invalid seed")); + this.errorMessages.put(ApiError.INVALID_AMOUNT, createErrorMessageEntry(ApiError.INVALID_AMOUNT, "invalid amount")); + this.errorMessages.put(ApiError.INVALID_FEE, createErrorMessageEntry(ApiError.INVALID_FEE, "invalid fee")); + this.errorMessages.put(ApiError.INVALID_SENDER, createErrorMessageEntry(ApiError.INVALID_SENDER, "invalid sender")); + this.errorMessages.put(ApiError.INVALID_RECIPIENT, createErrorMessageEntry(ApiError.INVALID_RECIPIENT, "invalid recipient")); + this.errorMessages.put(ApiError.INVALID_NAME_LENGTH, createErrorMessageEntry(ApiError.INVALID_NAME_LENGTH, "invalid name length")); + this.errorMessages.put(ApiError.INVALID_VALUE_LENGTH, createErrorMessageEntry(ApiError.INVALID_VALUE_LENGTH, "invalid value length")); + this.errorMessages.put(ApiError.INVALID_NAME_OWNER, createErrorMessageEntry(ApiError.INVALID_NAME_OWNER, "invalid name owner")); + this.errorMessages.put(ApiError.INVALID_BUYER, createErrorMessageEntry(ApiError.INVALID_BUYER, "invalid buyer")); + this.errorMessages.put(ApiError.INVALID_PUBLIC_KEY, createErrorMessageEntry(ApiError.INVALID_PUBLIC_KEY, "invalid public key")); + this.errorMessages.put(ApiError.INVALID_OPTIONS_LENGTH, createErrorMessageEntry(ApiError.INVALID_OPTIONS_LENGTH, "invalid options length")); + this.errorMessages.put(ApiError.INVALID_OPTION_LENGTH, createErrorMessageEntry(ApiError.INVALID_OPTION_LENGTH, "invalid option length")); + this.errorMessages.put(ApiError.INVALID_DATA, createErrorMessageEntry(ApiError.INVALID_DATA, "invalid data")); + this.errorMessages.put(ApiError.INVALID_DATA_LENGTH, createErrorMessageEntry(ApiError.INVALID_DATA_LENGTH, "invalid data length")); + this.errorMessages.put(ApiError.INVALID_UPDATE_VALUE, createErrorMessageEntry(ApiError.INVALID_UPDATE_VALUE, "invalid update value")); + this.errorMessages.put(ApiError.KEY_ALREADY_EXISTS, createErrorMessageEntry(ApiError.KEY_ALREADY_EXISTS, "key already exists, edit is false")); + this.errorMessages.put(ApiError.KEY_NOT_EXISTS, createErrorMessageEntry(ApiError.KEY_NOT_EXISTS, "the key does not exist")); +// TODO +// this.errorMessages.put(ApiError.LAST_KEY_IS_DEFAULT_KEY_ERROR, createErrorMessageEntry(ApiError.LAST_KEY_IS_DEFAULT_KEY_ERROR, +// "you can't delete the key \"${key}\" if it is the only key", +// new AbstractMap.SimpleEntry("key", Qorakeys.DEFAULT.toString()))); + this.errorMessages.put(ApiError.FEE_LESS_REQUIRED, createErrorMessageEntry(ApiError.FEE_LESS_REQUIRED, "fee less required")); + this.errorMessages.put(ApiError.WALLET_NOT_IN_SYNC, createErrorMessageEntry(ApiError.WALLET_NOT_IN_SYNC, "wallet needs to be synchronized")); + this.errorMessages.put(ApiError.INVALID_NETWORK_ADDRESS, createErrorMessageEntry(ApiError.INVALID_NETWORK_ADDRESS, "invalid network address")); + + //WALLET + this.errorMessages.put(ApiError.WALLET_NO_EXISTS, createErrorMessageEntry(ApiError.WALLET_NO_EXISTS, "wallet does not exist")); + this.errorMessages.put(ApiError.WALLET_ADDRESS_NO_EXISTS, createErrorMessageEntry(ApiError.WALLET_ADDRESS_NO_EXISTS, "address does not exist in wallet")); + this.errorMessages.put(ApiError.WALLET_LOCKED, createErrorMessageEntry(ApiError.WALLET_LOCKED, "wallet is locked")); + this.errorMessages.put(ApiError.WALLET_ALREADY_EXISTS, createErrorMessageEntry(ApiError.WALLET_ALREADY_EXISTS, "wallet already exists")); + this.errorMessages.put(ApiError.WALLET_API_CALL_FORBIDDEN_BY_USER, createErrorMessageEntry(ApiError.WALLET_API_CALL_FORBIDDEN_BY_USER, "user denied api call")); + + //BLOCK + this.errorMessages.put(ApiError.BLOCK_NO_EXISTS, createErrorMessageEntry(ApiError.BLOCK_NO_EXISTS, "block does not exist")); + + //TRANSACTIONS + this.errorMessages.put(ApiError.TRANSACTION_NO_EXISTS, createErrorMessageEntry(ApiError.TRANSACTION_NO_EXISTS, "transactions does not exist")); + this.errorMessages.put(ApiError.PUBLIC_KEY_NOT_FOUND, createErrorMessageEntry(ApiError.PUBLIC_KEY_NOT_FOUND, "public key not found")); + + //NAMING + this.errorMessages.put(ApiError.NAME_NO_EXISTS, createErrorMessageEntry(ApiError.NAME_NO_EXISTS, "name does not exist")); + this.errorMessages.put(ApiError.NAME_ALREADY_EXISTS, createErrorMessageEntry(ApiError.NAME_ALREADY_EXISTS, "name already exists")); + this.errorMessages.put(ApiError.NAME_ALREADY_FOR_SALE, createErrorMessageEntry(ApiError.NAME_ALREADY_FOR_SALE, "name already for sale")); + this.errorMessages.put(ApiError.NAME_NOT_LOWER_CASE, createErrorMessageEntry(ApiError.NAME_NOT_LOWER_CASE, "name must be lower case")); + this.errorMessages.put(ApiError.NAME_SALE_NO_EXISTS, createErrorMessageEntry(ApiError.NAME_SALE_NO_EXISTS, "namesale does not exist")); + this.errorMessages.put(ApiError.BUYER_ALREADY_OWNER, createErrorMessageEntry(ApiError.BUYER_ALREADY_OWNER, "buyer is already owner")); + + //POLLS + this.errorMessages.put(ApiError.POLL_NO_EXISTS, createErrorMessageEntry(ApiError.POLL_NO_EXISTS, "poll does not exist")); + this.errorMessages.put(ApiError.POLL_ALREADY_EXISTS, createErrorMessageEntry(ApiError.POLL_ALREADY_EXISTS, "poll already exists")); + this.errorMessages.put(ApiError.DUPLICATE_OPTION, createErrorMessageEntry(ApiError.DUPLICATE_OPTION, "not all options are unique")); + this.errorMessages.put(ApiError.POLL_OPTION_NO_EXISTS, createErrorMessageEntry(ApiError.POLL_OPTION_NO_EXISTS, "option does not exist")); + this.errorMessages.put(ApiError.ALREADY_VOTED_FOR_THAT_OPTION, createErrorMessageEntry(ApiError.ALREADY_VOTED_FOR_THAT_OPTION, "already voted for that option")); + + //ASSETS + this.errorMessages.put(ApiError.INVALID_ASSET_ID, createErrorMessageEntry(ApiError.INVALID_ASSET_ID, "invalid asset id")); + + //NAME PAYMENTS +// TODO +// this.errorMessages.put(ApiError.NAME_NOT_REGISTERED, createErrorMessageEntry(ApiError.NAME_NOT_REGISTERED, NameResult.NAME_NOT_REGISTERED.getStatusMessage())); +// 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.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")); + + //BLOG + this.errorMessages.put(ApiError.BODY_EMPTY, createErrorMessageEntry(ApiError.BODY_EMPTY, "invalid body it must not be empty")); + this.errorMessages.put(ApiError.BLOG_DISABLED, createErrorMessageEntry(ApiError.BLOG_DISABLED, "this blog is disabled")); + this.errorMessages.put(ApiError.NAME_NOT_OWNER, createErrorMessageEntry(ApiError.NAME_NOT_OWNER, "the creator address does not own the author name")); +// this.errorMessages.put(ApiError.TX_AMOUNT, createErrorMessageEntry(ApiError.TX_AMOUNT, +// "the data size is too large - currently only ${BATCH_TX_AMOUNT} arbitrary transactions are allowed at once!", +// new AbstractMap.SimpleEntry("BATCH_TX_AMOUNT", BATCH_TX_AMOUNT))); + this.errorMessages.put(ApiError.BLOG_ENTRY_NO_EXISTS, createErrorMessageEntry(ApiError.BLOG_ENTRY_NO_EXISTS, "transaction with this signature contains no entries!")); + this.errorMessages.put(ApiError.BLOG_EMPTY, createErrorMessageEntry(ApiError.BLOG_EMPTY, "this blog is empty")); + this.errorMessages.put(ApiError.POSTID_EMPTY, createErrorMessageEntry(ApiError.POSTID_EMPTY, "the attribute postid is empty! this is the signature of the post you want to comment")); + this.errorMessages.put(ApiError.POST_NOT_EXISTING, createErrorMessageEntry(ApiError.POST_NOT_EXISTING, "for the given postid no blogpost to comment was found")); + this.errorMessages.put(ApiError.COMMENTING_DISABLED, createErrorMessageEntry(ApiError.COMMENTING_DISABLED, "commenting is for this blog disabled")); + this.errorMessages.put(ApiError.COMMENT_NOT_EXISTING, createErrorMessageEntry(ApiError.COMMENT_NOT_EXISTING, "for the given signature no comment was found")); + this.errorMessages.put(ApiError.INVALID_COMMENT_OWNER, createErrorMessageEntry(ApiError.INVALID_COMMENT_OWNER, "invalid comment owner")); + + //MESSAGES + this.errorMessages.put(ApiError.MESSAGE_FORMAT_NOT_HEX, createErrorMessageEntry(ApiError.MESSAGE_FORMAT_NOT_HEX, "the Message format is not hex - correct the text or use isTextMessage = true")); + this.errorMessages.put(ApiError.MESSAGE_BLANK, createErrorMessageEntry(ApiError.MESSAGE_BLANK, "The message attribute is missing or content is blank")); + this.errorMessages.put(ApiError.NO_PUBLIC_KEY, createErrorMessageEntry(ApiError.NO_PUBLIC_KEY, "The recipient has not yet performed any action in the blockchain.\nYou can't send an encrypted message to him.")); + this.errorMessages.put(ApiError.MESSAGESIZE_EXCEEDED, createErrorMessageEntry(ApiError.MESSAGESIZE_EXCEEDED, "Message size exceeded!")); + + } + + //XXX: replace singleton pattern by dependency injection? + private static ApiErrorFactory instance; + + public static ApiErrorFactory getInstance() { + if (instance == null) { + instance = new ApiErrorFactory(Translator.getInstance()); + } + + return instance; + } + + private ErrorMessageEntry createErrorMessageEntry(ApiError errorCode, String defaultTemplate, AbstractMap.SimpleEntry... templateValues) { + String templateKey = String.format("%s: ApiError.%s message", ApiErrorFactory.class.getSimpleName(), errorCode.name()); + return new ErrorMessageEntry(templateKey, defaultTemplate, templateValues); + } + + public ApiException createError(ApiError error) { + return createError(error, null); + } + + public ApiException createError(ApiError error, Throwable throwable) { + Locale locale = Locale.ENGLISH; // XXX: should this be in local language? + + // TODO: handle AT errors +// old AT error handling +// JSONObject jsonObject = new JSONObject(); +// jsonObject.put("error", error); +// if ( error > Transaction.AT_ERROR ) +// { +// jsonObject.put("message", AT_Error.getATError(error - Transaction.AT_ERROR) ); +// } +// else +// { +// jsonObject.put("message", this.errorMessages.get(error)); +// } +// +// +// return new WebApplicationException(Response.status(Response.Status.BAD_REQUEST).entity(jsonObject.toJSONString()).build()); + ErrorMessageEntry errorMessage = this.errorMessages.get(error); + String message = this.translator.translate(locale, errorMessage.templateKey, errorMessage.defaultTemplate, errorMessage.templateValues); + + return new ApiException(error.getStatus(), error.getCode(), message, throwable); + } +} diff --git a/src/api/ApiErrorMessage.java b/src/api/ApiErrorMessage.java new file mode 100644 index 00000000..0c84fe4a --- /dev/null +++ b/src/api/ApiErrorMessage.java @@ -0,0 +1,22 @@ +package api; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +public class ApiErrorMessage { + + @XmlElement(name = "error") + public int error; + + @XmlElement(name = "message") + public String message; + + ApiErrorMessage() { + } + + ApiErrorMessage(int errorCode, String message) { + this.error = errorCode; + this.message = message; + } +} diff --git a/src/api/ApiException.java b/src/api/ApiException.java new file mode 100644 index 00000000..b8419cc7 --- /dev/null +++ b/src/api/ApiException.java @@ -0,0 +1,36 @@ +package api; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +public class ApiException extends WebApplicationException { + // HTTP status code + + int status; + + // API error code + int error; + + String message; + + public ApiException(int status, int error, String message) { + this(status, error, message, null); + } + + public ApiException(int status, int error, String message, Throwable throwable) { + super( + message, + throwable, + Response.status(Status.fromStatusCode(status)) + .entity(new ApiErrorMessage(error, message)) + .type(MediaType.APPLICATION_JSON) + .build() + ); + + this.status = status; + this.error = error; + this.message = message; + } +} diff --git a/src/api/ApiService.java b/src/api/ApiService.java index a0e7bd26..afa236ec 100644 --- a/src/api/ApiService.java +++ b/src/api/ApiService.java @@ -1,6 +1,5 @@ package api; -import io.swagger.v3.jaxrs2.integration.OpenApiServlet; import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource; import java.util.HashSet; import java.util.Set; @@ -12,70 +11,72 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; - import settings.Settings; public class ApiService { - private Server server; - private Set> resources; - public ApiService() - { - // resources to register - resources = new HashSet>(); - resources.add(BlocksResource.class); - resources.add(OpenApiResource.class); // swagger - ResourceConfig config = new ResourceConfig(resources); + private final Server server; + private final Set> resources; + + public ApiService() { + // resources to register + this.resources = new HashSet>(); + this.resources.add(BlocksResource.class); + this.resources.add(OpenApiResource.class); // swagger + ResourceConfig config = new ResourceConfig(this.resources); - // create RPC server - this.server = new Server(Settings.getInstance().getRpcPort()); - - // whitelist - InetAccessHandler accessHandler = new InetAccessHandler(); - for(String pattern : Settings.getInstance().getRpcAllowed()) - accessHandler.include(pattern); - this.server.setHandler(accessHandler); - - // context - ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); - context.setContextPath("/"); - accessHandler.setHandler(context); - - // API servlet - ServletContainer container = new ServletContainer(config); - ServletHolder apiServlet = new ServletHolder(container); - apiServlet.setInitOrder(1); - context.addServlet(apiServlet, "/*"); - } - - Iterable> getResources() - { - return resources; - } + // create RPC server + this.server = new Server(Settings.getInstance().getRpcPort()); - public void start() - { - try - { - //START RPC - server.start(); - } - catch (Exception e) - { - //FAILED TO START RPC - } - } + // whitelist + InetAccessHandler accessHandler = new InetAccessHandler(); + for (String pattern : Settings.getInstance().getRpcAllowed()) { + accessHandler.include(pattern); + } + this.server.setHandler(accessHandler); - public void stop() - { - try - { - //STOP RPC - server.stop(); - } - catch (Exception e) - { - //FAILED TO STOP RPC - } - } + // context + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); + context.setContextPath("/"); + accessHandler.setHandler(context); + + // API servlet + ServletContainer container = new ServletContainer(config); + ServletHolder apiServlet = new ServletHolder(container); + apiServlet.setInitOrder(1); + context.addServlet(apiServlet, "/*"); + } + + //XXX: replace singleton pattern by dependency injection? + private static ApiService instance; + + public static ApiService getInstance() { + if (instance == null) { + instance = new ApiService(); + } + + return instance; + } + + Iterable> getResources() { + return resources; + } + + public void start() { + try { + //START RPC + server.start(); + } catch (Exception e) { + //FAILED TO START RPC + } + } + + public void stop() { + try { + //STOP RPC + server.stop(); + } catch (Exception e) { + //FAILED TO STOP RPC + } + } } diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java index 56903d23..ae79734d 100644 --- a/src/api/BlocksResource.java +++ b/src/api/BlocksResource.java @@ -1,5 +1,6 @@ package api; +import globalization.Translator; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -10,7 +11,6 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; @@ -18,285 +18,295 @@ import repository.Repository; import repository.RepositoryManager; @Path("blocks") -@Produces(MediaType.APPLICATION_JSON) +@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) public class BlocksResource { - @Context - HttpServletRequest request; - @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 = "Wallet does not exist" - ) - } - ) - public String getBlocks() - { - Security.checkApiCallAllowed("GET blocks", request); + @Context + HttpServletRequest request; - throw new UnsupportedOperationException(); - } + private ApiErrorFactory apiErrorFactory; - @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 = "Invalid address" - ), - @ApiResponse( - responseCode = "422", - description = "Wallet does not exist" - ), - @ApiResponse( - responseCode = "422", - description = "Address does not exist in wallet" - ) - } - ) - public String getBlocks(@PathParam("address") String address) - { - Security.checkApiCallAllowed("GET blocks/address/" + address, request); + public BlocksResource() { + this(new ApiErrorFactory(new Translator())); + } - throw new UnsupportedOperationException(); - } - - @GET @Path("/{signature}") - @Operation( - description = "Returns the block that matches the given signature", - responses = { - @ApiResponse( - description = "The block" - //content = @Content(schema = @Schema(implementation = ???)) - ), - @ApiResponse( - responseCode = "400", - description = "Invalid signature" - ), - @ApiResponse( - responseCode = "422", - description = "Block does not exist" - ) - } - ) - public String getBlock(@PathParam("signature") String signature) - { - Security.checkApiCallAllowed("GET blocks", request); + public BlocksResource(ApiErrorFactory apiErrorFactory) { + this.apiErrorFactory = apiErrorFactory; + } - throw new UnsupportedOperationException(); - } + @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 = "Wallet does not exist" + ) + } + ) + public String getBlocks() { + Security.checkApiCallAllowed("GET blocks", request); - @GET @Path("/first") - @Operation( - description = "Returns the genesis block", - responses = { - @ApiResponse( - description = "The block" - //content = @Content(schema = @Schema(implementation = ???)) - ) - } - ) - public String getFirstBlock() - { - Security.checkApiCallAllowed("GET blocks/first", request); + throw new UnsupportedOperationException(); + } - 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 = "Invalid address" + ), + @ApiResponse( + responseCode = "422", + description = "Wallet does not exist" + ), + @ApiResponse( + responseCode = "422", + description = "Address does not exist in wallet" + ) + } + ) + public String getBlocks(@PathParam("address") String address) { + Security.checkApiCallAllowed("GET blocks/address/" + address, request); - @GET @Path("/last") - @Operation( - description = "Returns the last valid block", - responses = { - @ApiResponse( - description = "The block" - //content = @Content(schema = @Schema(implementation = ???)) - ) - } - ) - public String getLastBlock() - { - Security.checkApiCallAllowed("GET blocks/last", request); + throw new UnsupportedOperationException(); + } - throw new UnsupportedOperationException(); - } + @GET + @Path("/{signature}") + @Operation( + description = "Returns the block that matches the given signature", + responses = { + @ApiResponse( + description = "The block" + //content = @Content(schema = @Schema(implementation = ???)) + ), + @ApiResponse( + responseCode = "400", + description = "Invalid signature" + ), + @ApiResponse( + responseCode = "422", + description = "Block does not exist" + ) + } + ) + public String getBlock(@PathParam("signature") String signature) { + Security.checkApiCallAllowed("GET blocks", request); - @GET @Path("/child/{signature}") - @Operation( - description = "Returns the child block of the block that matches the given signature", - responses = { - @ApiResponse( - description = "The block" - //content = @Content(schema = @Schema(implementation = ???)) - ), - @ApiResponse( - responseCode = "400", - description = "Invalid signature" - ), - @ApiResponse( - responseCode = "422", - description = "Block does not exist" - ) - } - ) - public String getChild(@PathParam("signature") String signature) - { - Security.checkApiCallAllowed("GET blocks/child", request); + throw new UnsupportedOperationException(); + } - throw new UnsupportedOperationException(); - } + @GET + @Path("/first") + @Operation( + description = "Returns the genesis block", + responses = { + @ApiResponse( + description = "The block" + //content = @Content(schema = @Schema(implementation = ???)) + ) + } + ) + public String getFirstBlock() { + Security.checkApiCallAllowed("GET blocks/first", request); - @GET @Path("/generatingbalance") - @Operation( - description = "Calculates the generating balance of the block that will follow the last block", - responses = { - @ApiResponse( - description = "The generating balance", - content = @Content(schema = @Schema(implementation = long.class)) - ) - } - ) - public long getGeneratingBalance() - { - Security.checkApiCallAllowed("GET blocks/generatingbalance", request); + throw new UnsupportedOperationException(); + } - throw new UnsupportedOperationException(); - } + @GET + @Path("/last") + @Operation( + description = "Returns the last valid block", + responses = { + @ApiResponse( + description = "The block" + //content = @Content(schema = @Schema(implementation = ???)) + ) + } + ) + public String getLastBlock() { + Security.checkApiCallAllowed("GET blocks/last", request); - @GET @Path("/generatingbalance/{signature}") - @Operation( - description = "Calculates the generating balance of the block that will follow the block that matches the signature", - responses = { - @ApiResponse( - description = "The block", - content = @Content(schema = @Schema(implementation = long.class)) - ), - @ApiResponse( - responseCode = "400", - description = "Invalid signature" - ), - @ApiResponse( - responseCode = "422", - description = "Block does not exist" - ) - } - ) - public long getGeneratingBalance(@PathParam("signature") String signature) - { - Security.checkApiCallAllowed("GET blocks/generatingbalance", request); + throw new UnsupportedOperationException(); + } - throw new UnsupportedOperationException(); - } + @GET + @Path("/child/{signature}") + @Operation( + description = "Returns the child block of the block that matches the given signature", + responses = { + @ApiResponse( + description = "The block" + //content = @Content(schema = @Schema(implementation = ???)) + ), + @ApiResponse( + responseCode = "400", + description = "Invalid signature" + ), + @ApiResponse( + responseCode = "422", + description = "Block does not exist" + ) + } + ) + public String getChild(@PathParam("signature") String signature) { + Security.checkApiCallAllowed("GET blocks/child", request); - @GET @Path("/time") - @Operation( - description = "Calculates the time it should take for the network to generate the next block", - responses = { - @ApiResponse( - description = "The time", // in seconds? - content = @Content(schema = @Schema(implementation = long.class)) - ) - } - ) - public long getTimePerBlock() - { - Security.checkApiCallAllowed("GET blocks/time", request); + throw new UnsupportedOperationException(); + } - throw new UnsupportedOperationException(); - } + @GET + @Path("/generatingbalance") + @Operation( + description = "Calculates the generating balance of the block that will follow the last block", + responses = { + @ApiResponse( + description = "The generating balance", + content = @Content(schema = @Schema(implementation = long.class)) + ) + } + ) + public long getGeneratingBalance() { + Security.checkApiCallAllowed("GET blocks/generatingbalance", request); - @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", - responses = { - @ApiResponse( - description = "The time", // in seconds? - content = @Content(schema = @Schema(implementation = long.class)) - ) - } - ) - public String getTimePerBlock(@PathParam("generating") long generatingbalance) - { - Security.checkApiCallAllowed("GET blocks/time", request); + throw new UnsupportedOperationException(); + } + + @GET + @Path("/generatingbalance/{signature}") + @Operation( + description = "Calculates the generating balance of the block that will follow the block that matches the signature", + responses = { + @ApiResponse( + description = "The block", + content = @Content(schema = @Schema(implementation = long.class)) + ), + @ApiResponse( + responseCode = "400", + description = "Invalid signature" + ), + @ApiResponse( + responseCode = "422", + description = "Block does not exist" + ) + } + ) + public long getGeneratingBalance(@PathParam("signature") String signature) { + Security.checkApiCallAllowed("GET blocks/generatingbalance", request); + + throw new UnsupportedOperationException(); + } + + @GET + @Path("/time") + @Operation( + description = "Calculates the time it should take for the network to generate the next block", + responses = { + @ApiResponse( + description = "The time", // in seconds? + content = @Content(schema = @Schema(implementation = long.class)) + ) + } + ) + public long getTimePerBlock() { + Security.checkApiCallAllowed("GET blocks/time", request); + + throw new UnsupportedOperationException(); + } + + @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", + responses = { + @ApiResponse( + description = "The time", // in seconds? + content = @Content(schema = @Schema(implementation = long.class)) + ) + } + ) + public String getTimePerBlock(@PathParam("generating") long generatingbalance) { + Security.checkApiCallAllowed("GET blocks/time", request); + + throw new UnsupportedOperationException(); + } + + @GET + @Path("/height") + @Operation( + description = "Returns the block height of the last block.", + responses = { + @ApiResponse( + description = "The height", + content = @Content(schema = @Schema(implementation = int.class)) + ) + } + ) + public int getHeight() { + Security.checkApiCallAllowed("GET blocks/height", request); - throw new UnsupportedOperationException(); - } - - @GET @Path("/height") - @Operation( - description = "Returns the block height of the last block.", - responses = { - @ApiResponse( - description = "The height", - content = @Content(schema = @Schema(implementation = int.class)) - ) - } - ) - public int getHeight() - { - Security.checkApiCallAllowed("GET blocks/height", request); - try (final Repository repository = RepositoryManager.getRepository()) { return repository.getBlockRepository().getBlockchainHeight(); } catch (Exception e) { - throw new WebApplicationException(e); + throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); } - } + } - @GET @Path("/height/{signature}") - @Operation( - description = "Returns the block height of the block that matches the given signature", - responses = { - @ApiResponse( - description = "The height", - content = @Content(schema = @Schema(implementation = int.class)) - ), - @ApiResponse( - responseCode = "400", - description = "Invalid signature" - ), - @ApiResponse( - responseCode = "422", - description = "Block does not exist" - ) - } - ) - public int getHeight(@PathParam("signature") String signature) - { - Security.checkApiCallAllowed("GET blocks/height", request); + @GET + @Path("/height/{signature}") + @Operation( + description = "Returns the block height of the block that matches the given signature", + responses = { + @ApiResponse( + description = "The height", + content = @Content(schema = @Schema(implementation = int.class)) + ), + @ApiResponse( + responseCode = "400", + description = "Invalid signature" + ), + @ApiResponse( + responseCode = "422", + description = "Block does not exist" + ) + } + ) + public int getHeight(@PathParam("signature") String signature) { + Security.checkApiCallAllowed("GET blocks/height", request); - throw new UnsupportedOperationException(); - } + throw new UnsupportedOperationException(); + } - @GET @Path("/byheight/{height}") - @Operation( - description = "Returns the block whith given height", - responses = { - @ApiResponse( - description = "The block" - //content = @Content(schema = @Schema(implementation = ???)) - ), - @ApiResponse( - responseCode = "422", - description = "Block does not exist" - ) - } - ) - public String getbyHeight(@PathParam("height") int height) - { - Security.checkApiCallAllowed("GET blocks/byheight", request); + @GET + @Path("/byheight/{height}") + @Operation( + description = "Returns the block whith given height", + responses = { + @ApiResponse( + description = "The block" + //content = @Content(schema = @Schema(implementation = ???)) + ), + @ApiResponse( + responseCode = "422", + description = "Block does not exist" + ) + } + ) + public String getbyHeight(@PathParam("height") int height) { + Security.checkApiCallAllowed("GET blocks/byheight", request); - throw new UnsupportedOperationException(); - } + throw new UnsupportedOperationException(); + } } diff --git a/src/api/Security.java b/src/api/Security.java index 30a0333f..d05a41af 100644 --- a/src/api/Security.java +++ b/src/api/Security.java @@ -3,8 +3,8 @@ package api; import javax.servlet.http.HttpServletRequest; public class Security { - public static void checkApiCallAllowed(final String messageToDisplay, HttpServletRequest request) - { - // TODO - } + + public static void checkApiCallAllowed(final String messageToDisplay, HttpServletRequest request) { + // TODO + } } diff --git a/src/globalization/Translator.java b/src/globalization/Translator.java new file mode 100644 index 00000000..da61c595 --- /dev/null +++ b/src/globalization/Translator.java @@ -0,0 +1,52 @@ +package globalization; + +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import org.apache.commons.text.StringSubstitutor; + +public class Translator { + + private Map createMap(Map.Entry[] entries) { + HashMap map = new HashMap<>(); + for (AbstractMap.Entry entry : entries) { + map.put(entry.getKey(), entry.getValue()); + } + return map; + } + + //XXX: replace singleton pattern by dependency injection? + private static Translator instance; + + public static Translator getInstance() { + if (instance == null) { + instance = new Translator(); + } + + return instance; + } + + 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 + + StringSubstitutor sub = new StringSubstitutor(templateValues); + String result = sub.replace(template); + + return result; + } +}