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