From 2fc74ac58313741dd692821b94a001b8f4dc8dd9 Mon Sep 17 00:00:00 2001 From: Kc Date: Wed, 18 Jul 2018 23:42:40 +0200 Subject: [PATCH 1/6] Added reference to Jersey for RESTful services. Added Api package. Added BlocksResource as first candidate for API implementation. --- pom.xml | 5 +++++ src/api/BlocksResource.java | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/api/BlocksResource.java diff --git a/pom.xml b/pom.xml index bdf7f1b1..2a87677e 100644 --- a/pom.xml +++ b/pom.xml @@ -42,5 +42,10 @@ commons-net 3.3 + + org.glassfish.jersey.core + jersey-server + 2.27 + \ No newline at end of file diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java new file mode 100644 index 00000000..0b2f0427 --- /dev/null +++ b/src/api/BlocksResource.java @@ -0,0 +1,28 @@ +package api; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; + +import repository.DataException; +import repository.Repository; +import repository.RepositoryManager; + +@Path("blocks") +@Produces(MediaType.APPLICATION_JSON) +public class BlocksResource { + + @GET + @Path("/height") + public static String getHeight() + { + try (final Repository repository = RepositoryManager.getRepository()) { + return String.valueOf(repository.getBlockRepository().getBlockchainHeight()); + } catch (Exception e) { + throw new WebApplicationException("What happened?"); + } + } + +} From 9fb434cdd62ff0a44f852e8d38a5661ab612c301 Mon Sep 17 00:00:00 2001 From: Kc Date: Sun, 2 Sep 2018 10:35:13 +0200 Subject: [PATCH 2/6] CHANGED: fixed ApiService --- pom.xml | 10 ++++++ src/api/ApiService.java | 71 ++++++++++++++++++++++++++++++++++++++ src/settings/Settings.java | 41 ++++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 src/api/ApiService.java diff --git a/pom.xml b/pom.xml index 2a87677e..ef1ef89d 100644 --- a/pom.xml +++ b/pom.xml @@ -47,5 +47,15 @@ jersey-server 2.27 + + org.glassfish.jersey.containers + jersey-container-servlet + 2.27 + + + org.eclipse.jetty + jetty-maven-plugin + 9.4.11.v20180605 + \ No newline at end of file diff --git a/src/api/ApiService.java b/src/api/ApiService.java new file mode 100644 index 00000000..509f6913 --- /dev/null +++ b/src/api/ApiService.java @@ -0,0 +1,71 @@ +package api; + +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.InetAccessHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; + +import settings.Settings; + +public class ApiService { + + public Server server; + + public ApiService() + { + //CREATE CONFIG + Set> s = new HashSet>(); + s.add(BlocksResource.class); + + ResourceConfig config = new ResourceConfig(s); + + //CREATE CONTAINER + ServletContainer container = new ServletContainer(config); + + //CREATE CONTEXT + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + context.addServlet(new ServletHolder(container),"/*"); + + //CREATE WHITELIST + InetAccessHandler accessHandler = new InetAccessHandler(); + for(String pattern : Settings.getInstance().getRpcAllowed()) + accessHandler.include(pattern); + accessHandler.setHandler(context); + + //CREATE RPC SERVER + this.server = new Server(Settings.getInstance().getRpcPort()); + this.server.setHandler(accessHandler); + } + + 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/settings/Settings.java b/src/settings/Settings.java index be62d2a9..4600f867 100644 --- a/src/settings/Settings.java +++ b/src/settings/Settings.java @@ -2,8 +2,12 @@ package settings; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Set; +import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.JSONValue; @@ -20,9 +24,15 @@ public class Settings { private int maxBytePerFee = 1024; private String userpath = ""; + //RPC + private int rpcPort = 9085; + private List rpcAllowed = new ArrayList(Arrays.asList("127.0.0.1")); + private boolean rpcEnabled = true; + // Constants private static final String SETTINGS_FILENAME = "settings.json"; + // Constructors private Settings() { @@ -102,6 +112,23 @@ public class Settings { this.genesisTimestamp = ((Long) json.get("testnetstamp")).longValue(); } } + + // RPC + if(json.containsKey("rpcport")) + { + this.rpcPort = ((Long) json.get("rpcport")).intValue(); + } + + if(json.containsKey("rpcallowed")) + { + JSONArray allowedArray = (JSONArray) json.get("rpcallowed"); + this.rpcAllowed = new ArrayList(allowedArray); + } + + if(json.containsKey("rpcenabled")) + { + this.rpcEnabled = ((Boolean) json.get("rpcenabled")).booleanValue(); + } } public boolean isTestNet() { @@ -122,4 +149,18 @@ public class Settings { return this.userpath; } + public int getRpcPort() + { + return this.rpcPort; + } + + public List getRpcAllowed() + { + return this.rpcAllowed; + } + + public boolean isRpcEnabled() + { + return this.rpcEnabled; + } } From d63ff02b97d06f5208109a982b7a11e716a36880 Mon Sep 17 00:00:00 2001 From: Kc Date: Sun, 16 Sep 2018 23:24:20 +0200 Subject: [PATCH 3/6] CHANGED: added first API resources with jetty and jersey --- pom.xml | 41 +++++++++++---- src/Start.java | 20 ++++++++ src/api/ApiService.java | 99 ++++++++++++++++++++----------------- src/api/BlocksResource.java | 25 ++++++---- src/settings/Settings.java | 2 +- 5 files changed, 123 insertions(+), 64 deletions(-) create mode 100644 src/Start.java diff --git a/pom.xml b/pom.xml index ef1ef89d..a4c754e2 100644 --- a/pom.xml +++ b/pom.xml @@ -43,19 +43,42 @@ 3.3 - org.glassfish.jersey.core - jersey-server - 2.27 + org.hsqldb + hsqldb + 2.4.0 + jar - org.glassfish.jersey.containers - jersey-container-servlet - 2.27 + org.glassfish.jersey.core + jersey-server + 2.27 - org.eclipse.jetty - jetty-maven-plugin - 9.4.11.v20180605 + javax.servlet + javax.servlet-api + 4.0.1 + + + org.eclipse.jetty + jetty-server + 9.4.11.v20180605 + config + + + org.glassfish.jersey.containers + jersey-container-servlet-core + 2.27 + + + org.eclipse.jetty + jetty-servlet + 9.4.11.v20180605 + jar + + + org.glassfish.jersey.inject + jersey-hk2 + 2.27 \ No newline at end of file diff --git a/src/Start.java b/src/Start.java new file mode 100644 index 00000000..99632030 --- /dev/null +++ b/src/Start.java @@ -0,0 +1,20 @@ + +import api.ApiService; +import repository.DataException; +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(); + } +} diff --git a/src/api/ApiService.java b/src/api/ApiService.java index 509f6913..e523a7d6 100644 --- a/src/api/ApiService.java +++ b/src/api/ApiService.java @@ -1,5 +1,7 @@ package api; +//import io.swagger.jaxrs.config.DefaultJaxrsConfig; + import java.util.HashSet; import java.util.Set; @@ -10,62 +12,71 @@ 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; - public Server server; - - public ApiService() - { - //CREATE CONFIG - Set> s = new HashSet>(); + public ApiService() + { + // resources to register + Set> s = new HashSet>(); s.add(BlocksResource.class); - - ResourceConfig config = new ResourceConfig(s); - - //CREATE CONTAINER - ServletContainer container = new ServletContainer(config); - - //CREATE CONTEXT - ServletContextHandler context = new ServletContextHandler(); - context.setContextPath("/"); - context.addServlet(new ServletHolder(container),"/*"); + ResourceConfig config = new ResourceConfig(s); + + // create RPC server + this.server = new Server(Settings.getInstance().getRpcPort()); - //CREATE WHITELIST + // whitelist InetAccessHandler accessHandler = new InetAccessHandler(); for(String pattern : Settings.getInstance().getRpcAllowed()) - accessHandler.include(pattern); + accessHandler.include(pattern); + this.server.setHandler(accessHandler); + + // context + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); + context.setContextPath("/"); accessHandler.setHandler(context); - //CREATE RPC SERVER - this.server = new Server(Settings.getInstance().getRpcPort()); - this.server.setHandler(accessHandler); - } - - public void start() - { - try + // API servlet + ServletContainer container = new ServletContainer(config); + ServletHolder apiServlet = new ServletHolder(container); + apiServlet.setInitOrder(1); + context.addServlet(apiServlet, "/api/*"); + + /* + // Setup Swagger servlet + ServletHolder swaggerServlet = context.addServlet(DefaultJaxrsConfig.class, "/swagger-core"); + swaggerServlet.setInitOrder(2); + swaggerServlet.setInitParameter("api.version", "1.0.0"); + */ + + } + + public void start() + { + try { - //START RPC - server.start(); - } + //START RPC + server.start(); + } catch (Exception e) - { - //FAILED TO START RPC - } - } - - public void stop() - { - try { - //STOP RPC - server.stop(); - } + //FAILED TO START RPC + } + } + + public void stop() + { + try + { + //STOP RPC + server.stop(); + } catch (Exception e) - { - //FAILED TO STOP RPC - } - } + { + //FAILED TO STOP RPC + } + } } diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java index 0b2f0427..d46744a2 100644 --- a/src/api/BlocksResource.java +++ b/src/api/BlocksResource.java @@ -1,9 +1,12 @@ package api; +import javax.servlet.http.HttpServletRequest; + import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import repository.DataException; @@ -13,16 +16,18 @@ import repository.RepositoryManager; @Path("blocks") @Produces(MediaType.APPLICATION_JSON) public class BlocksResource { + @Context + HttpServletRequest request; - @GET - @Path("/height") - public static String getHeight() - { - try (final Repository repository = RepositoryManager.getRepository()) { - return String.valueOf(repository.getBlockRepository().getBlockchainHeight()); - } catch (Exception e) { - throw new WebApplicationException("What happened?"); - } - } + @GET + @Path("/height") + public static String getHeight() + { + try (final Repository repository = RepositoryManager.getRepository()) { + return String.valueOf(repository.getBlockRepository().getBlockchainHeight()); + } catch (Exception e) { + throw new WebApplicationException("What happened?"); + } + } } diff --git a/src/settings/Settings.java b/src/settings/Settings.java index 4600f867..288020b6 100644 --- a/src/settings/Settings.java +++ b/src/settings/Settings.java @@ -26,7 +26,7 @@ public class Settings { //RPC private int rpcPort = 9085; - private List rpcAllowed = new ArrayList(Arrays.asList("127.0.0.1")); + private List rpcAllowed = new ArrayList(Arrays.asList("127.0.0.1", "::1")); // ipv4, ipv6 private boolean rpcEnabled = true; // Constants From 4f279fc6161b60dcf27ea602f0fcba6ec57b5d9b Mon Sep 17 00:00:00 2001 From: Kc Date: Tue, 18 Sep 2018 23:41:37 +0200 Subject: [PATCH 4/6] ADDED: ApiClient ADDED: UsageDescription annotation ADDED: Start class as entry point first implementation of annotated resource descriptions --- src/Start.java | 4 ++ src/api/ApiClient.java | 123 ++++++++++++++++++++++++++++++++++ src/api/ApiService.java | 20 +++--- src/api/BlocksResource.java | 1 + src/api/UsageDescription.java | 14 ++++ 5 files changed, 151 insertions(+), 11 deletions(-) create mode 100644 src/api/ApiClient.java create mode 100644 src/api/UsageDescription.java diff --git a/src/Start.java b/src/Start.java index 99632030..8ab1825d 100644 --- a/src/Start.java +++ b/src/Start.java @@ -1,4 +1,5 @@ +import api.ApiClient; import api.ApiService; import repository.DataException; import repository.RepositoryFactory; @@ -16,5 +17,8 @@ public class Start { ApiService apiService = new ApiService(); apiService.start(); + + ApiClient client = new ApiClient(apiService); + String test = client.executeCommand("help GET blocks/height"); } } diff --git a/src/api/ApiClient.java b/src/api/ApiClient.java new file mode 100644 index 00000000..174d51f2 --- /dev/null +++ b/src/api/ApiClient.java @@ -0,0 +1,123 @@ +package api; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.ws.rs.Path; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.PATCH; +import javax.ws.rs.DELETE; +import javax.ws.rs.HttpMethod; + +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> REST_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<>(); + + for (Class resource : resources) { + Path resourcePath = resource.getDeclaredAnnotation(Path.class); + if(resourcePath == null) + continue; + + String resourcePathString = resourcePath.value(); + + for(Method method : resource.getDeclaredMethods()) + { + UsageDescription usageDescription = method.getAnnotation(UsageDescription.class); + if(usageDescription == null) + continue; + + String usageDescriptionString = usageDescription.value(); + + Path methodPath = method.getDeclaredAnnotation(Path.class); + String methodPathString = (methodPath != null) ? methodPath.value() : ""; + + for(Class restMethodAnnotation : REST_METHOD_ANNOTATIONS) + { + Annotation annotation = method.getDeclaredAnnotation(restMethodAnnotation); + if(annotation == null) + continue; + + HttpMethod httpMethod = annotation.annotationType().getDeclaredAnnotation(HttpMethod.class); + String httpMethodString = httpMethod.value(); + + Pattern pattern = Pattern.compile("^ *" + httpMethodString + " *" + getRegexPatternForPath(resourcePathString + methodPathString)); + String fullPath = httpMethodString + " " + resourcePathString + methodPathString; + result.add(new HelpString(pattern, fullPath, usageDescriptionString)); + } + } + } + + return result; + } + + private String getRegexPatternForPath(String path) + { + return path + .replaceAll("\\.", "\\.") // escapes "." as "\." + .replaceAll("\\{.*?\\}", ".*?"); // replace placeholders "{...}" by the "ungreedy match anything" pattern ".*?" + } + + public String executeCommand(String command) + { + final Matcher helpMatch = HELP_COMMAND_PATTERN.matcher(command); + if(helpMatch.matches()) + { + command = helpMatch.group("command"); + StringBuilder help = new StringBuilder(); + + for(HelpString helpString : helpStrings) + { + if(helpString.pattern.matcher(command).matches()) + { + help.append(helpString.fullPath + "\n"); + help.append(helpString.description + "\n"); + } + } + + return help.toString(); + } + + return null; + } +} diff --git a/src/api/ApiService.java b/src/api/ApiService.java index e523a7d6..f14fd239 100644 --- a/src/api/ApiService.java +++ b/src/api/ApiService.java @@ -17,13 +17,14 @@ import settings.Settings; public class ApiService { private Server server; + private Set> resources; public ApiService() { // resources to register - Set> s = new HashSet>(); - s.add(BlocksResource.class); - ResourceConfig config = new ResourceConfig(s); + resources = new HashSet>(); + resources.add(BlocksResource.class); + ResourceConfig config = new ResourceConfig(resources); // create RPC server this.server = new Server(Settings.getInstance().getRpcPort()); @@ -44,14 +45,11 @@ public class ApiService { ServletHolder apiServlet = new ServletHolder(container); apiServlet.setInitOrder(1); context.addServlet(apiServlet, "/api/*"); - - /* - // Setup Swagger servlet - ServletHolder swaggerServlet = context.addServlet(DefaultJaxrsConfig.class, "/swagger-core"); - swaggerServlet.setInitOrder(2); - swaggerServlet.setInitParameter("api.version", "1.0.0"); - */ - + } + + Iterable> getResources() + { + return resources; } public void start() diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java index d46744a2..8b8e3850 100644 --- a/src/api/BlocksResource.java +++ b/src/api/BlocksResource.java @@ -21,6 +21,7 @@ public class BlocksResource { @GET @Path("/height") + @UsageDescription("Returns the height of the blockchain") public static String getHeight() { try (final Repository repository = RepositoryManager.getRepository()) { diff --git a/src/api/UsageDescription.java b/src/api/UsageDescription.java new file mode 100644 index 00000000..e7f100fb --- /dev/null +++ b/src/api/UsageDescription.java @@ -0,0 +1,14 @@ +package api; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(value={ElementType.TYPE,ElementType.METHOD}) +@Retention(value=RetentionPolicy.RUNTIME) +@Documented +public abstract @interface UsageDescription { + public abstract String value(); +} \ No newline at end of file From 19a9a3a98b7a420c9928ba84edc3775f3ccff189 Mon Sep 17 00:00:00 2001 From: Kc Date: Thu, 20 Sep 2018 23:48:20 +0200 Subject: [PATCH 5/6] CHANGED: integrated Swagger/OpenApi CHANGED: added method stubs and describing annotations to BlocksResource --- pom.xml | 10 ++ src/Start.java | 3 +- src/api/ApiClient.java | 69 ++++++--- src/api/ApiService.java | 7 +- src/api/BlocksResource.java | 280 +++++++++++++++++++++++++++++++++- src/api/Security.java | 10 ++ src/api/UsageDescription.java | 14 -- 7 files changed, 347 insertions(+), 46 deletions(-) create mode 100644 src/api/Security.java delete mode 100644 src/api/UsageDescription.java diff --git a/pom.xml b/pom.xml index a4c754e2..d1773583 100644 --- a/pom.xml +++ b/pom.xml @@ -80,5 +80,15 @@ jersey-hk2 2.27 + + io.swagger.core.v3 + swagger-jaxrs2 + 2.0.4 + + + io.swagger.core.v3 + swagger-jaxrs2-servlet-initializer + 2.0.4 + \ No newline at end of file diff --git a/src/Start.java b/src/Start.java index 8ab1825d..9b0fce81 100644 --- a/src/Start.java +++ b/src/Start.java @@ -19,6 +19,7 @@ public class Start { apiService.start(); ApiClient client = new ApiClient(apiService); - String test = client.executeCommand("help GET blocks/height"); + String test = client.executeCommand("help ALL"); + System.out.println(test); } } diff --git a/src/api/ApiClient.java b/src/api/ApiClient.java index 174d51f2..0d989215 100644 --- a/src/api/ApiClient.java +++ b/src/api/ApiClient.java @@ -1,12 +1,12 @@ package api; +import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource; +import io.swagger.v3.oas.annotations.Operation; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.ws.rs.Path; @@ -33,7 +33,7 @@ public class ApiClient { } private static final Pattern HELP_COMMAND_PATTERN = Pattern.compile("^ *help *(?.*)$", Pattern.CASE_INSENSITIVE); - private static final List> REST_METHOD_ANNOTATIONS = Arrays.asList( + private static final List> HTTP_METHOD_ANNOTATIONS = Arrays.asList( GET.class, POST.class, PUT.class, @@ -54,25 +54,31 @@ public class ApiClient { { 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()) { - UsageDescription usageDescription = method.getAnnotation(UsageDescription.class); - if(usageDescription == null) + Operation operationAnnotation = method.getAnnotation(Operation.class); + if(operationAnnotation == null) continue; - String usageDescriptionString = usageDescription.value(); + String description = operationAnnotation.description(); Path methodPath = method.getDeclaredAnnotation(Path.class); String methodPathString = (methodPath != null) ? methodPath.value() : ""; - for(Class restMethodAnnotation : REST_METHOD_ANNOTATIONS) + // scan for each potential http method + for(Class restMethodAnnotation : HTTP_METHOD_ANNOTATIONS) { Annotation annotation = method.getDeclaredAnnotation(restMethodAnnotation); if(annotation == null) @@ -81,21 +87,37 @@ public class ApiClient { HttpMethod httpMethod = annotation.annotationType().getDeclaredAnnotation(HttpMethod.class); String httpMethodString = httpMethod.value(); - Pattern pattern = Pattern.compile("^ *" + httpMethodString + " *" + getRegexPatternForPath(resourcePathString + methodPathString)); String fullPath = httpMethodString + " " + resourcePathString + methodPathString; - result.add(new HelpString(pattern, fullPath, usageDescriptionString)); + 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 getRegexPatternForPath(String path) - { - return path - .replaceAll("\\.", "\\.") // escapes "." as "\." - .replaceAll("\\{.*?\\}", ".*?"); // replace placeholders "{...}" by the "ungreedy match anything" pattern ".*?" + 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) @@ -104,20 +126,23 @@ public class ApiClient { if(helpMatch.matches()) { command = helpMatch.group("command"); - StringBuilder help = new StringBuilder(); - + StringBuilder result = new StringBuilder(); + + boolean showAll = command.trim().equalsIgnoreCase("all"); for(HelpString helpString : helpStrings) { - if(helpString.pattern.matcher(command).matches()) - { - help.append(helpString.fullPath + "\n"); - help.append(helpString.description + "\n"); - } + if(showAll || helpString.pattern.matcher(command).matches()) + appendHelp(result, helpString); } - return help.toString(); + return result.toString(); } return null; } + + private void appendHelp(StringBuilder builder, HelpString helpString) { + builder.append(helpString.fullPath + "\n"); + builder.append(helpString.description + "\n"); + } } diff --git a/src/api/ApiService.java b/src/api/ApiService.java index f14fd239..a0e7bd26 100644 --- a/src/api/ApiService.java +++ b/src/api/ApiService.java @@ -1,7 +1,7 @@ package api; -//import io.swagger.jaxrs.config.DefaultJaxrsConfig; - +import io.swagger.v3.jaxrs2.integration.OpenApiServlet; +import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource; import java.util.HashSet; import java.util.Set; @@ -24,6 +24,7 @@ public class ApiService { // resources to register resources = new HashSet>(); resources.add(BlocksResource.class); + resources.add(OpenApiResource.class); // swagger ResourceConfig config = new ResourceConfig(resources); // create RPC server @@ -44,7 +45,7 @@ public class ApiService { ServletContainer container = new ServletContainer(config); ServletHolder apiServlet = new ServletHolder(container); apiServlet.setInitOrder(1); - context.addServlet(apiServlet, "/api/*"); + context.addServlet(apiServlet, "/*"); } Iterable> getResources() diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java index 8b8e3850..56903d23 100644 --- a/src/api/BlocksResource.java +++ b/src/api/BlocksResource.java @@ -1,15 +1,19 @@ package api; +import io.swagger.v3.oas.annotations.Operation; +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 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.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; -import repository.DataException; import repository.Repository; import repository.RepositoryManager; @@ -20,15 +24,279 @@ public class BlocksResource { HttpServletRequest request; @GET - @Path("/height") - @UsageDescription("Returns the height of the blockchain") - public static String getHeight() + @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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + try (final Repository repository = RepositoryManager.getRepository()) { - return String.valueOf(repository.getBlockRepository().getBlockchainHeight()); + return repository.getBlockRepository().getBlockchainHeight(); } catch (Exception e) { - throw new WebApplicationException("What happened?"); + throw new WebApplicationException(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); + + 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); + + throw new UnsupportedOperationException(); + } } diff --git a/src/api/Security.java b/src/api/Security.java new file mode 100644 index 00000000..30a0333f --- /dev/null +++ b/src/api/Security.java @@ -0,0 +1,10 @@ +package api; + +import javax.servlet.http.HttpServletRequest; + +public class Security { + public static void checkApiCallAllowed(final String messageToDisplay, HttpServletRequest request) + { + // TODO + } +} diff --git a/src/api/UsageDescription.java b/src/api/UsageDescription.java deleted file mode 100644 index e7f100fb..00000000 --- a/src/api/UsageDescription.java +++ /dev/null @@ -1,14 +0,0 @@ -package api; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(value={ElementType.TYPE,ElementType.METHOD}) -@Retention(value=RetentionPolicy.RUNTIME) -@Documented -public abstract @interface UsageDescription { - public abstract String value(); -} \ No newline at end of file From 646462942c360d3418908a3f4f0f0b8876770f5c Mon Sep 17 00:00:00 2001 From: Kc Date: Mon, 24 Sep 2018 00:21:47 +0200 Subject: [PATCH 6/6] 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; + } +}