Refactoring, new translations, cleaning up warnings.

Refactored to standard Maven layout:
src/main/java
src/main/resources
src/test/java
etc.

New translation code that uses locale-specific ResourceBundles
to load translations on demand.

Reworked API error/exceptions code to a shorter, simpler
@ApiErrors annotation. Processing of @ApiErrors annotations
produces an example for each possible API error and includes
API error string in HTTP response code, e.g.
400 INVALID_SIGNATURE
Missing API error cases added to each API call.

Translation of openAPI.json removed (for now).

block-explorer.html and BIP39 wordlists now read as resources
instead of direct from disk.

Java compile warnings fixed.
Some runtime warnings remain:

WARNING: A provider api.resource.ApiDefinition registered in SERVER runtime does not implement any provider interfaces applicable in the SERVER runtime.
WARNING: A provider api.resource.AnnotationPostProcessor registered in SERVER runtime does not implement any provider interfaces applicable in the SERVER runtime.
WARN org.reflections.Reflections - given scan urls are empty. set urls in the configuration
This commit is contained in:
catbref 2018-12-21 11:14:16 +00:00
parent aab6b69da1
commit c4ed4b378c
284 changed files with 835 additions and 2781 deletions

View File

@ -1,12 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src">
<classpathentry kind="src" output="target/classes" path="target/generated-sources/package-info">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/classes" path="target/generated-sources/package-info">
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
@ -22,20 +28,11 @@
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="tests">
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="ignore_optional_problems" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="m2e-apt" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
<attributes>
<attribute name="optional" value="true"/>
@ -44,5 +41,10 @@
<attribute name="m2e-apt" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -1,4 +1,6 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
encoding//src/test/java=UTF-8
encoding/<project>=UTF-8
encoding/src=UTF-8
encoding/tests=UTF-8

View File

@ -1,5 +1,6 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.8

View File

@ -1,37 +0,0 @@
### General
- Reduce Qora2 down to core blockchain node with RESTful API access.
Other libraries can process name-storage data into websites, or provide web-based wallet UI, etc.
- Trying to reduce number of external dependencies where possible, e.g. avoiding heavy-weight ORM like Hive, Hibernate, etc.
- Trying to reduce duplicated code, especially across transactions with lots of serialisation and signature generation.
- Transaction signatures should really be generated after creating the transaction,
compared to the old style of generating signature first (and throw-away transaction in the process)
then using signature when creating actual transaction!
- Trying to keep most of the source structure, naming, code paths, etc. similar to old Qora to reduce brain load!
- More comments/javadoc
- More JUnit tests
### Differences due to switching from MapDB to HSQLDB
- We might need to maintain more mappings in the database, e.g. Qora address to/from public key,
as previously public key objects could be stored directly in MapDB.
- The new database tried to store the data in "rawest" form, i.e. raw ```byte[]``` signatures, not base58-encoded.
- The ```Transactions``` table contains ```creator``` column, which duplicates various child table columns,
e.g. ```PaymentTransactions.sender```,
so that all transactions by a specific Qora account can be quickly found without scanning all child tables.
- SQL is contained within repository classes repository.* (interfaces) and repository.hsqldb.* (implementations).
- We use transfer objects in data.*
- "Business logic" is left in qora.*
- Some MapDB-based objects had Java Map<> objects as their values. These will need to be unpacked into separate tables.

View File

@ -1,104 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<localization>
<context locale="de">
<context path="Api">
<context path="ApiError">
<translation key="0" template="Unbekannter Fehler" />
<translation key="1" template="JSON Nachricht konnte nicht geparsed werden" />
<translation key="2" template="Guthaben ungenügend" />
<translation key="3" template="Feature wurde noch nicht veröffentlicht" />
<translation key="101" template="Ungültige Signatur" />
<translation key="102" template="Ungültige Adresse" />
<translation key="103" template="Ungültiger Seed" />
<translation key="104" template="Ungültiger Betrag" />
<translation key="105" template="Ungültige Gebühr" />
<translation key="106" template="Ungültiger Sender" />
<translation key="107" template="Ungültiger Empfänger" />
<translation key="108" template="Ungültige Namenslänge" />
<translation key="109" template="Ungültige Wertlänge" />
<translation key="110" template="Ungültiger Namensbesitzer" />
<translation key="111" template="Ungültiger Käufer" />
<translation key="112" template="Ungültiger Public Key" />
<translation key="113" template="Ungültige Optionen-Länge" />
<translation key="114" template="Ungültige Optionslänge" />
<translation key="115" template="Ungültige Daten" />
<translation key="116" template="Ungültige Datenlänge" />
<translation key="117" template="Ungültiger Update-Wert" />
<translation key="118" template="Der Schlüssel existiert bereits, Editieren ist deaktiviert" />
<translation key="119" template="Der Schlüssel existiert nicht" />
<translation key="120" template="Du kannst den Schlüssel '${key}' nicht löschen, wenn er der einzige ist" />
<translation key="121" template="fee less required" />
<translation key="122" template="Das Wallet muss synchronisiert werden" />
<translation key="123" template="Ungültige Netzwerkadresse" />
<translation key="201" template="Das Wallet existiert nicht" />
<translation key="202" template="Die Adresse existiert nicht im Wallet" />
<translation key="203" template="Das Wallet ist abgeschlossen" />
<translation key="204" template="Das Wallet existiert bereits" />
<translation key="205" template="Der Benutzer hat den API-Aufruf abgelehnt" />
<translation key="301" template="Der Block existiert nicht" />
<translation key="311" template="Die Transaktion existiert nicht" />
<translation key="304" template="Public Key wurde nicht gefunden" />
<translation key="401" template="Der Name existiert nicht" />
<translation key="402" template="Der Name existiert bereits" />
<translation key="403" template="Der Name steht bereits zum Verkauf" />
<translation key="404" template="Der Name muss aus Kleinbuchstaben bestehen" />
<translation key="410" template="Namensverkauf existiert nicht" />
<translation key="411" template="Der Käufer ist bereits Besitzer" />
<translation key="501" template="Die Abstimmung existiert nicht" />
<translation key="502" template="Die Abstimmung existiert bereits" />
<translation key="503" template="Nicht alle Optionen sind eindeutig" />
<translation key="504" template="Die option existiert nicht" />
<translation key="505" template="Bereits für diese Option abgestimmt" />
<translation key="601" template="Ungültige Asset ID" />
<!--
<translation key="701" template="?NAME_NOT_REGISTERED?" />
<translation key="702" template="?NAME_FOR_SALE?" />
<translation key="703" template="?NAME_WITH_SPACE?" />
-->
<translation key="801" template="Ungültige Beschreibungslänge. Max. Länge ${MAX_LENGTH}" />
<translation key="802" template="Der Code ist leer" />
<translation key="803" template="Ungültige Datenlänge" />
<translation key="804" template="Ungültige Seiten" />
<translation key="805" template="Ungültige Typlänge" />
<translation key="806" template="Ungültige Tag-Länge" />
<translation key="809" template="Fehler in Creation Bytes" />
<translation key="901" template="invalid body it must not be empty" />
<translation key="902" template="Dieser Blog ist deaktiviert" />
<translation key="903" template="the creator address does not own the author name" />
<translation key="904" template="the data size is too large - currently only ${BATCH_TX_AMOUNT} arbitrary transactions are allowed at once!" />
<translation key="905" template="transaction with this signature contains no entries!" />
<translation key="906" template="this blog is empty" />
<translation key="907" template="the attribute postid is empty! this is the signature of the post you want to comment" />
<translation key="908" template="for the given postid no blogpost to comment was found" />
<translation key="909" template="commenting is for this blog disabled" />
<translation key="910" template="for the given signature no comment was found" />
<translation key="911" template="invalid comment owner" />
<translation key="1001" template="the Message format is not hex - correct the text or use isTextMessage = true" />
<translation key="1002" template="The message attribute is missing or content is blank" />
<translation key="1003" template="The recipient has not yet performed any action in the blockchain.\nYou can't send an encrypted message to him." />
<translation key="1004" template="Message size exceeded!" />
</context>
<context path="ApiClient">
<translation key="invalid command" template="Ungültiger Befehl!\nGib 'help all' ein, um eine Liste aller gültigen Befehle zu erhalten." />
<translation key="error footer" template="Gib 'help all' ein, um eine Liste aller gültigen Befehle zu erhalten." />
<translation key="help: success responses" template="Antwort bei Erfolg:" />
<translation key="help: failure responses" template="Antwort bei Misserfolg:" />
</context>
</context>
</context>
</localization>

View File

@ -1,108 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<localization>
<context locale="en">
<context path="Api">
<context path="ApiError">
<translation key="0" template="unknown error" />
<translation key="1" template="failed to parse json message" />
<translation key="2" template="not enough balance" />
<translation key="3" template="that feature is not yet released" />
<translation key="101" template="invalid signature" />
<translation key="102" template="invalid address" />
<translation key="103" template="invalid seed" />
<translation key="104" template="invalid amount" />
<translation key="105" template="invalid fee" />
<translation key="106" template="invalid sender" />
<translation key="107" template="invalid recipient" />
<translation key="108" template="invalid name length" />
<translation key="109" template="invalid value length" />
<translation key="110" template="invalid name owner" />
<translation key="111" template="invalid buyer" />
<translation key="112" template="invalid public key" />
<translation key="113" template="invalid options length" />
<translation key="114" template="invalid option length" />
<translation key="115" template="invalid data" />
<translation key="116" template="invalid data length" />
<translation key="117" template="invalid update value" />
<translation key="118" template="key already exists, edit is false" />
<translation key="119" template="the key does not exist" />
<translation key="120" template="you can't delete the key '${key}' if it is the only key" />
<translation key="121" template="fee less required" />
<translation key="122" template="wallet needs to be synchronized" />
<translation key="123" template="invalid network address" />
<translation key="201" template="wallet does not exist" />
<translation key="202" template="address does not exist in wallet" />
<translation key="203" template="wallet is locked" />
<translation key="204" template="wallet already exists" />
<translation key="205" template="user denied api call" />
<translation key="301" template="block does not exist" />
<translation key="311" template="transactions does not exist" />
<translation key="304" template="public key not found" />
<translation key="401" template="name does not exist" />
<translation key="402" template="name already exists" />
<translation key="403" template="name already for sale" />
<translation key="404" template="name must be lower case" />
<translation key="410" template="namesale does not exist" />
<translation key="411" template="buyer is already owner" />
<translation key="501" template="poll does not exist" />
<translation key="502" template="poll already exists" />
<translation key="503" template="not all options are unique" />
<translation key="504" template="option does not exist" />
<translation key="505" template="already voted for that option" />
<translation key="601" template="invalid asset id" />
<!--
<translation key="701" template="?NAME_NOT_REGISTERED?" />
<translation key="702" template="?NAME_FOR_SALE?" />
<translation key="703" template="?NAME_WITH_SPACE?" />
-->
<translation key="801" template="invalid description length. max length ${MAX_LENGTH}" />
<translation key="802" template="code is empty" />
<translation key="803" template="invalid data length" />
<translation key="804" template="invalid pages" />
<translation key="805" template="invalid type length" />
<translation key="806" template="invalid tags length" />
<translation key="809" template="error in creation bytes" />
<translation key="901" template="invalid body it must not be empty" />
<translation key="902" template="this blog is disabled" />
<translation key="903" template="the creator address does not own the author name" />
<translation key="904" template="the data size is too large - currently only ${BATCH_TX_AMOUNT} arbitrary transactions are allowed at once!" />
<translation key="905" template="transaction with this signature contains no entries!" />
<translation key="906" template="this blog is empty" />
<translation key="907" template="the attribute postid is empty! this is the signature of the post you want to comment" />
<translation key="908" template="for the given postid no blogpost to comment was found" />
<translation key="909" template="commenting is for this blog disabled" />
<translation key="910" template="for the given signature no comment was found" />
<translation key="911" template="invalid comment owner" />
<translation key="1001" template="the Message format is not hex - correct the text or use isTextMessage = true" />
<translation key="1002" template="The message attribute is missing or content is blank" />
<translation key="1003" template="The recipient has not yet performed any action in the blockchain.\nYou can't send an encrypted message to him." />
<translation key="1004" template="Message size exceeded!" />
</context>
<context path="ApiClient">
<translation key="invalid command" template="Invalid command! \nType 'help all' to get a list of commands." />
<translation key="error footer" template="Type 'help all' to get a list of commands." />
<translation key="API error response" template="(API error: ${ERROR_CODE}) ${DESCRIPTION}" />
<translation key="error: with body" template="HTTP Status ${STATUS}: ${BODY}" />
<translation key="error: without body" template="HTTP Status ${STATUS}" />
<translation key="help: success responses" template="On success returns:" />
<translation key="help: failure responses" template="On failure returns:" />
</context>
</context>
</context>
</localization>

11
pom.xml
View File

@ -17,8 +17,8 @@
<swagger-ui.version>3.19.0</swagger-ui.version>
</properties>
<build>
<sourceDirectory>src</sourceDirectory>
<testSourceDirectory>tests</testSourceDirectory>
<sourceDirectory>src/main/java</sourceDirectory>
<testSourceDirectory>src/test/java</testSourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
@ -119,14 +119,13 @@
<packages>
<package>
<pattern>data.**</pattern>
<template>${project.basedir}/src/data/package-info.java</template>
<template>${project.build.sourceDirectory}/data/package-info.java</template>
</package>
<package>
<pattern>api.models**</pattern>
<template>${project.basedir}/src/data/package-info.java</template>
<template>${project.build.sourceDirectory}/data/package-info.java</template>
</package>
</packages>
<sourceDirectory>${project.basedir}/src</sourceDirectory>
<outputDirectory>${project.build.directory}/generated-sources/package-info</outputDirectory>
</configuration>
<executions>
@ -391,4 +390,4 @@
<version>${bouncycastle.version}</version>
</dependency>
</dependencies>
</project>
</project>

View File

@ -1,239 +0,0 @@
package api;
import com.fasterxml.jackson.databind.node.ArrayNode;
import globalization.ContextPaths;
import globalization.Translator;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.jaxrs2.Reader;
import io.swagger.v3.jaxrs2.ReaderListener;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class AnnotationPostProcessor implements ReaderListener {
private static final Logger LOGGER = LogManager.getLogger(AnnotationPostProcessor.class);
private class ContextInformation {
public String path;
public Map<String, String> keys;
}
private final Translator translator;
private final ApiErrorFactory apiErrorFactory;
public AnnotationPostProcessor() {
this(Translator.getInstance(), ApiErrorFactory.getInstance());
}
public AnnotationPostProcessor(Translator translator, ApiErrorFactory apiErrorFactory) {
this.translator = translator;
this.apiErrorFactory = apiErrorFactory;
}
@Override
public void beforeScan(Reader reader, OpenAPI openAPI) {
}
@Override
public void afterScan(Reader reader, OpenAPI openAPI) {
// Populate Components section with reusable parameters, like "limit" and "offset"
// We take the reusable parameters from AdminResource.globalParameters path "/admin/unused"
Components components = openAPI.getComponents();
PathItem globalParametersPathItem = openAPI.getPaths().get("/admin/unused");
if (globalParametersPathItem != null) {
for (Parameter parameter : globalParametersPathItem.getGet().getParameters())
components.addParameters(parameter.getName(), parameter);
openAPI.getPaths().remove("/admin/unused");
}
// use context path and keys from "x-translation" extension annotations
// to translate supported annotations and finally remove "x-translation" extensions
Info resourceInfo = openAPI.getInfo();
ContextInformation resourceContext = getContextInformation(openAPI.getExtensions());
removeTranslationAnnotations(openAPI.getExtensions());
TranslateProperties(Constants.TRANSLATABLE_INFO_PROPERTIES, resourceContext, resourceInfo);
for (Map.Entry<String, PathItem> pathEntry : openAPI.getPaths().entrySet())
{
PathItem pathItem = pathEntry.getValue();
ContextInformation pathContext = getContextInformation(pathItem.getExtensions(), resourceContext);
removeTranslationAnnotations(pathItem.getExtensions());
TranslateProperties(Constants.TRANSLATABLE_PATH_ITEM_PROPERTIES, pathContext, pathItem);
for (Operation operation : pathItem.readOperations()) {
ContextInformation operationContext = getContextInformation(operation.getExtensions(), pathContext);
removeTranslationAnnotations(operation.getExtensions());
TranslateProperties(Constants.TRANSLATABLE_OPERATION_PROPERTIES, operationContext, operation);
addApiErrorResponses(operation);
removeApiErrorsAnnotations(operation.getExtensions());
for (Map.Entry<String, ApiResponse> responseEntry : operation.getResponses().entrySet()) {
ApiResponse response = responseEntry.getValue();
ContextInformation responseContext = getContextInformation(response.getExtensions(), operationContext);
removeTranslationAnnotations(response.getExtensions());
TranslateProperties(Constants.TRANSLATABLE_API_RESPONSE_PROPERTIES, responseContext, response);
}
}
}
}
private void addApiErrorResponses(Operation operation) {
List<ApiError> apiErrors = getApiErrors(operation.getExtensions());
if(apiErrors != null) {
for(ApiError apiError : apiErrors) {
String statusCode = Integer.toString(apiError.getStatus());
ApiResponse apiResponse = operation.getResponses().get(statusCode);
if(apiResponse == null) {
Schema errorMessageSchema = ModelConverters.getInstance().readAllAsResolvedSchema(ApiErrorMessage.class).schema;
MediaType mediaType = new MediaType().schema(errorMessageSchema);
Content content = new Content().addMediaType(javax.ws.rs.core.MediaType.APPLICATION_JSON, mediaType);
apiResponse = new ApiResponse().content(content);
operation.getResponses().addApiResponse(statusCode, apiResponse);
}
int apiErrorCode = apiError.getCode();
ApiErrorMessage apiErrorMessage = new ApiErrorMessage(apiErrorCode, this.apiErrorFactory.getErrorMessage(apiError));
Example example = new Example().value(apiErrorMessage);
// XXX: addExamples(..) is not working in Swagger 2.0.4. This bug is referenced in https://github.com/swagger-api/swagger-ui/issues/2651
// Replace the call to .setExample(..) by .addExamples(..) when the bug is fixed.
apiResponse.getContent().get(javax.ws.rs.core.MediaType.APPLICATION_JSON).setExample(example);
//apiResponse.getContent().get(javax.ws.rs.core.MediaType.APPLICATION_JSON).addExamples(Integer.toString(apiErrorCode), example);
}
}
}
private <T> void TranslateProperties(List<TranslatableProperty<T>> translatableProperties, ContextInformation context, T item) {
if(context.keys != null) {
Map<String, String> keys = context.keys;
for(TranslatableProperty<T> prop : translatableProperties) {
String key = keys.get(prop.keyName());
if(key != null) {
String originalValue = prop.getValue(item);
// XXX: use browser locale instead default?
String translation = translator.translate(context.path, key, originalValue);
prop.setValue(item, translation);
}
}
}
}
private List<ApiError> getApiErrors(Map<String, Object> extensions) {
if(extensions == null)
return null;
List<String> apiErrorStrings = new ArrayList();
try {
ArrayNode apiErrorsNode = (ArrayNode)extensions.get("x-" + Constants.API_ERRORS_EXTENSION_NAME);
if(apiErrorsNode == null)
return null;
for(int i = 0; i < apiErrorsNode.size(); i++) {
String errorString = apiErrorsNode.get(i).asText();
apiErrorStrings.add(errorString);
}
} catch(Exception e) {
// TODO: error logging
return null;
}
List<ApiError> result = new ArrayList<>();
for(String apiErrorString : apiErrorStrings) {
ApiError apiError = null;
try {
apiError = ApiError.valueOf(apiErrorString);
} catch(IllegalArgumentException e) {
try {
int errorCodeInt = Integer.parseInt(apiErrorString);
apiError = ApiError.fromCode(errorCodeInt);
} catch (NumberFormatException ex) {
return null;
}
}
if(apiError == null)
return null;
result.add(apiError);
}
return result;
}
private ContextInformation getContextInformation(Map<String, Object> extensions) {
return getContextInformation(extensions, null);
}
private ContextInformation getContextInformation(Map<String, Object> extensions, ContextInformation base) {
if(extensions != null) {
Map<String, Object> translationDefinitions = (Map<String, Object>)extensions.get("x-" + Constants.TRANSLATION_EXTENSION_NAME);
if(translationDefinitions != null) {
ContextInformation result = new ContextInformation();
result.path = combinePaths(base, (String)translationDefinitions.get(Constants.TRANSLATION_PATH_EXTENSION_NAME));
result.keys = getTranslationKeys(translationDefinitions);
return result;
}
}
if(base != null) {
ContextInformation result = new ContextInformation();
result.path = base.path;
return result;
}
return null;
}
private void removeApiErrorsAnnotations(Map<String, Object> extensions) {
String extensionName = Constants.API_ERRORS_EXTENSION_NAME;
removeExtension(extensions, extensionName);
}
private void removeTranslationAnnotations(Map<String, Object> extensions) {
String extensionName = Constants.TRANSLATION_EXTENSION_NAME;
removeExtension(extensions, extensionName);
}
private void removeExtension(Map<String, Object> extensions, String extensionName) {
if(extensions == null)
return;
extensions.remove("x-" + extensionName);
}
private Map<String, String> getTranslationKeys(Map<String, Object> translationDefinitions) {
Map<String, String> result = new HashMap<>();
for(TranslatableProperty prop : Constants.TRANSLATABLE_INFO_PROPERTIES) {
String key = (String)translationDefinitions.get(prop.keyName());
if(key != null)
result.put(prop.keyName(), key);
}
return result;
}
private String combinePaths(ContextInformation base, String path) {
String basePath = (base != null) ? base.path : null;
return ContextPaths.combinePaths(basePath, path);
}
}

View File

@ -1,340 +0,0 @@
package api;
import globalization.ContextPaths;
import globalization.Translator;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.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;
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 HelpInfo {
public final Pattern pattern;
public final String fullPath;
public final String description;
public final List<String> success;
public final List<String> errors;
public HelpInfo(Pattern pattern, String fullPath, String description, List<String> success, List<String> errors) {
this.pattern = pattern;
this.fullPath = fullPath;
this.description = description;
this.success = success;
this.errors = errors;
}
}
private static final String TRANSLATION_CONTEXT_PATH = "/Api/ApiClient";
private static final Pattern COMMAND_PATTERN = Pattern.compile("^ *(?<method>GET|POST|PUT|PATCH|DELETE) *(?<path>.*)$");
private static final Pattern HELP_COMMAND_PATTERN = Pattern.compile("^ *help *(?<command>.*)$", Pattern.CASE_INSENSITIVE);
private static final List<Class<? extends Annotation>> HTTP_METHOD_ANNOTATIONS = Arrays.asList(
GET.class,
POST.class,
PUT.class,
PATCH.class,
DELETE.class
);
private final Translator translator;
ApiService apiService;
List<HelpInfo> helpInfos;
public ApiClient(ApiService apiService, Translator translator) {
this.apiService = apiService;
this.translator = translator;
this.helpInfos = getHelpInfos(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<HelpInfo> getHelpInfos(Iterable<Class<?>> resources) {
List<HelpInfo> 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();
// get translation context path for resource
String resourceContextPath = "/";
OpenAPIDefinition openAPIDefinition = resource.getDeclaredAnnotation(OpenAPIDefinition.class);
if(openAPIDefinition != null)
resourceContextPath = getContextPath(openAPIDefinition.extensions());
// scan each method
for (Method method : resource.getDeclaredMethods()) {
Operation operationAnnotation = method.getAnnotation(Operation.class);
if (operationAnnotation == null)
continue;
String description = operationAnnotation.description();
// translate
String operationContextPath = ContextPaths.combinePaths(resourceContextPath, getContextPath(operationAnnotation.extensions()));
String operationDescriptionKey = getDescriptionTranslationKey(operationAnnotation.extensions());
if(operationDescriptionKey != null)
description = translator.translate(operationContextPath, operationDescriptionKey, description);
// extract responses
ArrayList success = new ArrayList();
ArrayList errors = new ArrayList();
for(ApiResponse response : operationAnnotation.responses()) {
String responseDescription = response.description();
if(StringUtils.isBlank(responseDescription))
continue; // ignore responses without description
// translate
String responseContextPath = ContextPaths.combinePaths(operationContextPath, getContextPath(response.extensions()));
String responseDescriptionKey = getDescriptionTranslationKey(response.extensions());
if(responseDescriptionKey != null)
responseDescription = translator.translate(responseContextPath, responseDescriptionKey, responseDescription);
String apiErrorCode = getApiErrorCode(response.extensions());
if(apiErrorCode != null) {
responseDescription = translator.translate(TRANSLATION_CONTEXT_PATH, "API error response", "(API error: ${ERROR_CODE}) ${DESCRIPTION}",
new AbstractMap.SimpleEntry<>("ERROR_CODE", apiErrorCode),
new AbstractMap.SimpleEntry<>("DESCRIPTION", responseDescription)
);
}
try {
// try to identify response type by status code
int responseCode = Integer.parseInt(response.responseCode());
if(responseCode >= 400) {
errors.add(responseDescription);
} else {
success.add(responseDescription);
}
} catch (NumberFormatException e) {
// try to identify response type by content
if(response.content().length > 0) {
Content content = response.content()[0];
Class<?> implementation = content.schema().implementation();
if(implementation != null && ApiErrorMessage.class.isAssignableFrom(implementation)) {
errors.add(responseDescription);
} else {
success.add(responseDescription);
}
} else {
success.add(responseDescription);
}
}
}
Path methodPath = method.getDeclaredAnnotation(Path.class);
String methodPathString = (methodPath != null) ? methodPath.value() : "";
// scan for each potential http method
for (Class<? extends Annotation> 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 HelpInfo(pattern, fullPath, description, success, errors));
}
}
}
// sort by path
result.sort((h1, h2) -> h1.fullPath.compareTo(h2.fullPath));
return result;
}
private String getApiErrorCode(Extension[] extensions) {
if(extensions == null)
return null;
for(Extension extension : extensions) {
if(extension.name() != null && !extension.name().isEmpty())
continue;
for(ExtensionProperty prop : extension.properties()) {
if(Constants.API_ERROR_CODE_EXTENSION_NAME.equals(prop.name())) {
return prop.value();
}
}
}
return null;
}
private String getContextPath(Extension[] extensions) {
return getTranslationExtensionValue(extensions, Constants.TRANSLATION_PATH_EXTENSION_NAME);
}
private String getDescriptionTranslationKey(Extension[] extensions) {
return getTranslationExtensionValue(extensions, Constants.TRANSLATION_ANNOTATION_DESCRIPTION_KEY);
}
private String getTranslationExtensionValue(Extension[] extensions, String key) {
if(extensions == null)
return null;
for(Extension extension : extensions) {
if(!Constants.TRANSLATION_EXTENSION_NAME.equals(extension.name()))
continue;
for(ExtensionProperty prop : extension.properties()) {
if(key.equals(prop.name())) {
return prop.value();
}
}
}
return null;
}
private String getHelpPatternForPath(String path) {
path = path
.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 (HelpInfo helpString : helpInfos) {
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(TRANSLATION_CONTEXT_PATH, "invalid command", "Invalid command! \nType 'help all' to get a list of commands.");
// send the command to the API service
String method = match.group("method");
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) {
if(StringUtils.isBlank(body)) {
result.append(
this.translator.translate(TRANSLATION_CONTEXT_PATH, "error without body", "HTTP Status ${STATUS}",
new AbstractMap.SimpleEntry<>("STATUS", status)
)
);
}else{
result.append(
this.translator.translate(TRANSLATION_CONTEXT_PATH, "error with body", "HTTP Status ${STATUS}: ${BODY}",
new AbstractMap.SimpleEntry<>("STATUS", status),
new AbstractMap.SimpleEntry<>("BODY", body)
)
);
}
result.append("\n");
result.append(this.translator.translate(TRANSLATION_CONTEXT_PATH, "error footer", "Type 'help all' to get a list of commands."));
} else {
result.append(body);
}
return result.toString();
}
private void appendHelp(StringBuilder builder, HelpInfo help) {
builder.append(help.fullPath + "\n");
builder.append(" " + help.description + "\n");
if(help.success != null && help.success.size() > 0) {
builder.append(" ");
builder.append(this.translator.translate(TRANSLATION_CONTEXT_PATH, "help: success responses", "On success returns:"));
builder.append("\n");
for(String content : help.success) {
builder.append(" " + content + "\n");
}
}
if(help.errors != null && help.errors.size() > 0) {
builder.append(" ");
builder.append(this.translator.translate(TRANSLATION_CONTEXT_PATH, "help: failure responses", "On failure returns:"));
builder.append("\n");
for(String content : help.errors) {
builder.append(" " + content + "\n");
}
}
}
}

View File

@ -1,200 +0,0 @@
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<String, Object>[] templateValues;
public ErrorMessageEntry(String templateKey, String defaultTemplate, AbstractMap.Entry<String, Object>... templateValues) {
this.templateKey = templateKey;
this.defaultTemplate = defaultTemplate;
this.templateValues = templateValues;
}
}
private Translator translator;
private Map<ApiError, ErrorMessageEntry> errorMessages;
public ApiErrorFactory(Translator translator) {
this.translator = translator;
this.errorMessages = new HashMap<ApiError, ErrorMessageEntry>();
//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"));
this.errorMessages.put(ApiError.UNAUTHORIZED, createErrorMessageEntry(ApiError.UNAUTHORIZED, "api call unauthorized"));
this.errorMessages.put(ApiError.REPOSITORY_ISSUE, createErrorMessageEntry(ApiError.REPOSITORY_ISSUE, "repository error"));
//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<String.Object>("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"));
this.errorMessages.put(ApiError.ADDRESS_NO_EXISTS, createErrorMessageEntry(ApiError.ADDRESS_NO_EXISTS, "account address does not exist"));
this.errorMessages.put(ApiError.INVALID_CRITERIA, createErrorMessageEntry(ApiError.INVALID_CRITERIA, "invalid search criteria"));
//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, "wallet 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, "transaction 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
// TODO
// this.errorMessages.put(ApiError.INVALID_DESC_LENGTH, createErrorMessageEntry(ApiError.INVALID_DESC_LENGTH,
// "invalid description length. max length ${MAX_LENGTH}",
// new AbstractMap.SimpleEntry<String, Object>("MAX_LENGTH", AT_Constants.DESC_MAX_LENGTH));
this.errorMessages.put(ApiError.EMPTY_CODE, createErrorMessageEntry(ApiError.EMPTY_CODE, "code is empty"));
this.errorMessages.put(ApiError.DATA_SIZE, createErrorMessageEntry(ApiError.DATA_SIZE, "invalid data length"));
this.errorMessages.put(ApiError.NULL_PAGES, createErrorMessageEntry(ApiError.NULL_PAGES, "invalid pages"));
this.errorMessages.put(ApiError.INVALID_TYPE_LENGTH, createErrorMessageEntry(ApiError.INVALID_TYPE_LENGTH, "invalid type length"));
this.errorMessages.put(ApiError.INVALID_TAGS_LENGTH, createErrorMessageEntry(ApiError.INVALID_TAGS_LENGTH, "invalid tags length"));
this.errorMessages.put(ApiError.INVALID_CREATION_BYTES, createErrorMessageEntry(ApiError.INVALID_CREATION_BYTES, "error in creation bytes"));
//BLOG
this.errorMessages.put(ApiError.BODY_EMPTY, createErrorMessageEntry(ApiError.BODY_EMPTY, "invalid body it must not be empty"));
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<String,Object>("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 them."));
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<String, Object>... templateValues) {
String templateKey = String.format(Constants.APIERROR_KEY, errorCode.name());
return new ErrorMessageEntry(templateKey, defaultTemplate, templateValues);
}
public String getErrorMessage(ApiError error) {
return getErrorMessage(null, error);
}
public String getErrorMessage(Locale locale, ApiError error) {
ErrorMessageEntry errorMessage = this.errorMessages.get(error);
String message = this.translator.translate(locale, Constants.APIERROR_CONTEXT_PATH, errorMessage.templateKey, errorMessage.defaultTemplate, errorMessage.templateValues);
return message;
}
public ApiException createError(ApiError error) {
return createError(error, null);
}
public ApiException createError(Locale locale, ApiError error) {
return createError(locale, error, null);
}
public ApiException createError(ApiError error, Throwable throwable) {
return createError(null, error, throwable);
}
public ApiException createError(Locale locale, ApiError error, Throwable throwable) {
// 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());
String message = getErrorMessage(locale, error);
return new ApiException(error.getStatus(), error.getCode(), message, throwable);
}
}

View File

@ -1,35 +0,0 @@
package api;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import io.swagger.v3.oas.annotations.Operation;
@Path("/")
public class BlockExplorerResource {
@Context
HttpServletRequest request;
public BlockExplorerResource() {
}
@GET
@Path("/block-explorer.html")
@Operation(hidden = true)
public String getBlockExplorer() {
try {
byte[] htmlBytes = Files.readAllBytes(FileSystems.getDefault().getPath("block-explorer.html"));
return new String(htmlBytes, "UTF-8");
} catch (IOException e) {
return "block-explorer.html not found";
}
}
}

View File

@ -1,55 +0,0 @@
package globalization;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import settings.Settings;
/** Providing multi-language BIP39 word lists, downloaded from https://github.com/bitcoin/bips/tree/master/bip-0039 */
public class BIP39WordList {
private static BIP39WordList instance;
private static Map<String, List<String>> wordListsByLang;
private BIP39WordList() {
wordListsByLang = new HashMap<>();
String path = Settings.getInstance().translationsPath();
File dir = new File(path);
File[] files = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.startsWith("BIP39.");
}
});
try {
for (File file : files) {
String lang = file.getName().substring(6, 8);
List<String> words = Files.readAllLines(file.toPath());
wordListsByLang.put(lang, words);
}
} catch (IOException e) {
throw new RuntimeException("Unable to read BIP39 word list", e);
}
}
public static synchronized BIP39WordList getInstance() {
if (instance == null)
instance = new BIP39WordList();
return instance;
}
public List<String> getByLang(String lang) {
return Collections.unmodifiableList(wordListsByLang.get(lang));
}
}

View File

@ -1,33 +0,0 @@
package globalization;
import java.nio.file.Paths;
public class ContextPaths {
public static boolean isValidKey(String value) {
return !value.contains("/") && !ContextPaths.containsParentReference(value);
}
public static boolean containsParentReference(String value) {
for(String part : value.split("/")) {
if(part.equalsIgnoreCase(".."))
return true;
}
return false;
}
public static String combinePaths(String left, String right) {
left = (left != null) ? left : "";
right = (right != null) ? right : "";
return Paths.get("/", left, right).normalize().toString();
}
public static String getParent(String path) {
return combinePaths(path, "..");
}
public static boolean isRoot(String path) {
return path.equals("/");
}
}

View File

@ -1,32 +0,0 @@
package globalization;
import java.util.Locale;
public class TranslationEntry {
private Locale locale;
private String path;
private String template;
public TranslationEntry(Locale locale, String path, String template) {
this.locale = locale;
this.path = path;
this.template = template;
}
public Locale locale() {
return this.locale;
}
public String path() {
return this.path;
}
public String template() {
return this.template;
}
@Override
public String toString() {
return String.format("{locale: '%s', path: '%s', template: '%s'}", this.locale, this.path, this.template);
}
}

View File

@ -1,252 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package globalization;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.*;
import org.apache.commons.text.StringEscapeUtils;
public class TranslationXmlStreamReader {
private class State {
public final Locale locale;
public final String path;
public State(Locale locale, String path) {
this.locale = locale;
this.path = path;
}
}
private static final String LOCALIZATION_TAG_NAME = "localization";
private static final String CONTEXT_TAG_NAME = "context";
private static final String CONTEXT_LOCALE_ATTRIBUTE_NAME = "locale";
private static final String CONTEXT_PATH_ATTRIBUTE_NAME = "path";
private static final String TRANSLATION_TAG_NAME = "translation";
private static final String TRANSLATION_KEY_ATTRIBUTE_NAME = "key";
private static final String TRANSLATION_TEMPLATE_ATTRIBUTE_NAME = "template";
public Iterable<TranslationEntry> ReadFrom(InputStream stream) throws XMLStreamException {
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
XMLEventReader eventReader = inputFactory.createXMLEventReader(stream);
XMLEvent element = eventReader.nextEvent();
if(!element.isStartDocument())
throw new javax.xml.stream.XMLStreamException("XML declaration <?xml ... ?> must be first in the document");
State state = new State(Locale.forLanguageTag("default"), "/");
List<TranslationEntry> result = new ArrayList<>();
if (eventReader.hasNext())
{
XMLEvent event = eventReader.nextTag();
if (isStartElement(event, LOCALIZATION_TAG_NAME))
{
processLocalization(eventReader, (StartElement)event, state, result);
} else {
throw new javax.xml.stream.XMLStreamException("Unexpected element: " + event.toString());
}
}
while (eventReader.hasNext())
{
XMLEvent event = eventReader.nextEvent();
switch(event.getEventType()) {
case XMLEvent.COMMENT:
break;
case XMLEvent.CHARACTERS:
if(!event.asCharacters().isIgnorableWhiteSpace())
throw new javax.xml.stream.XMLStreamException("Unexpected content after end of root element: " + event.toString());
break;
case XMLEvent.END_DOCUMENT:
return result;
default:
throw new javax.xml.stream.XMLStreamException("Unexpected content after end of root element: " + event.toString());
}
}
throw new javax.xml.stream.XMLStreamException("End of document not found");
}
private void processLocalization(XMLEventReader eventReader, StartElement element, State state, List<TranslationEntry> result) throws XMLStreamException {
assureStartElement(element, LOCALIZATION_TAG_NAME);
Iterator<Attribute> attributes = element.getAttributes();
while (attributes.hasNext())
{
Attribute attribute = attributes.next();
QName name = attribute.getName();
throw new javax.xml.stream.XMLStreamException("Unexpected attribute: " + name);
}
XMLEvent event;
while(!(event = eventReader.nextTag()).isEndElement()) {
if(event.isStartElement()) {
StartElement childElement = (StartElement)event;
switch(childElement.getName().toString()) {
case CONTEXT_TAG_NAME:
processContext(eventReader, childElement, state, result);
break;
case TRANSLATION_TAG_NAME:
processTranslation(eventReader, childElement, state, result);
break;
default:
throw new javax.xml.stream.XMLStreamException("Unexpected element: " + event.toString());
}
} else {
throw new javax.xml.stream.XMLStreamException("Unexpected content: " + event.toString());
}
}
assureEndElement(event, LOCALIZATION_TAG_NAME);
}
private void processContext(XMLEventReader eventReader, StartElement element, State state, List<TranslationEntry> result) throws XMLStreamException {
assureStartElement(element, CONTEXT_TAG_NAME);
Locale locale = state.locale;
String contextPath = state.path;
Iterator<Attribute> attributes = element.getAttributes();
while (attributes.hasNext())
{
Attribute attribute = attributes.next();
QName name = attribute.getName();
String value = attribute.getValue();
switch(name.toString()) {
case CONTEXT_LOCALE_ATTRIBUTE_NAME:
locale = Locale.forLanguageTag(value);
break;
case CONTEXT_PATH_ATTRIBUTE_NAME:
assureIsValidPathExtension(value);
contextPath = ContextPaths.combinePaths(contextPath, value);
break;
default:
throw new javax.xml.stream.XMLStreamException("Unexpected attribute: " + name);
}
}
state = new State(locale, contextPath);
XMLEvent event;
while(!(event = eventReader.nextTag()).isEndElement()) {
if(event.isStartElement()) {
StartElement childElement = (StartElement)event;
switch(childElement.getName().toString()) {
case CONTEXT_TAG_NAME:
processContext(eventReader, childElement, state, result);
break;
case TRANSLATION_TAG_NAME:
processTranslation(eventReader, childElement, state, result);
break;
default:
throw new javax.xml.stream.XMLStreamException("Unexpected element: " + event.toString());
}
} else {
throw new javax.xml.stream.XMLStreamException("Unexpected content: " + event.toString());
}
}
assureEndElement(event, CONTEXT_TAG_NAME);
}
private void processTranslation(XMLEventReader eventReader, StartElement element, State state, List<TranslationEntry> result) throws XMLStreamException {
assureStartElement(element, TRANSLATION_TAG_NAME);
String path = null;
String template = null;
Iterator<Attribute> attributes = element.getAttributes();
while (attributes.hasNext())
{
Attribute attribute = attributes.next();
QName name = attribute.getName();
String value = attribute.getValue();
switch(name.toString()) {
case TRANSLATION_KEY_ATTRIBUTE_NAME:
assureIsValidKey(value);
path = ContextPaths.combinePaths(state.path, value);
break;
case TRANSLATION_TEMPLATE_ATTRIBUTE_NAME:
template = unescape(value);
break;
default:
throw new javax.xml.stream.XMLStreamException("Unexpected attribute: " + name);
}
}
XMLEvent event;
while(!(event = eventReader.nextTag()).isEndElement()) {
if(event.isStartElement()) {
throw new javax.xml.stream.XMLStreamException("Unexpected element: " + event.toString());
} else if(event.isCharacters()) {
if(template != null)
throw new javax.xml.stream.XMLStreamException("Content must be empty if 'template' attribute is used");
template = event.asCharacters().getData();
}
}
assureEndElement(event, TRANSLATION_TAG_NAME);
if(path == null)
throw new javax.xml.stream.XMLStreamException("Missing attribute: " + TRANSLATION_KEY_ATTRIBUTE_NAME);
if(template == null)
throw new javax.xml.stream.XMLStreamException("Missing attribute: " + TRANSLATION_TEMPLATE_ATTRIBUTE_NAME);
result.add(new TranslationEntry(state.locale, path, template));
}
private String unescape(String value) {
return StringEscapeUtils.unescapeJava(value);
}
private void assureIsValidPathExtension(String value) throws XMLStreamException {
if(ContextPaths.containsParentReference(value))
throw new javax.xml.stream.XMLStreamException("Parent reference .. is not allowed");
}
private void assureIsValidKey(String value) throws XMLStreamException {
if(!ContextPaths.isValidKey(value))
throw new javax.xml.stream.XMLStreamException("Key is not valid");
}
private void assureStartElement(XMLEvent event, String name) throws XMLStreamException {
if(!isStartElement(event, name))
throw new javax.xml.stream.XMLStreamException("Unexpected start element: " + event.toString() + ", <" + name + "> expected");
}
private void assureEndElement(XMLEvent event, String name) throws XMLStreamException {
if(!isEndElement(event, name))
throw new javax.xml.stream.XMLStreamException("Unexpected end element: " + event.toString() + ", </" + name + "> expected");
}
private boolean isStartElement(XMLEvent event, String name) {
if(!event.isStartElement())
return false;
StartElement element = ((StartElement)event);
return element.getName().toString().equals(name);
}
private boolean isEndElement(XMLEvent event, String name) {
if(!event.isEndElement())
return false;
EndElement element = ((EndElement)event);
return element.getName().toString().equals(name);
}
}

View File

@ -1,33 +0,0 @@
<?xml version="1.0"?>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<xs:schema version="1.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified">
<xs:complexType name="localizationType">
<xs:all>
<xs:element name="context" minOccurs="1" maxOccurs="unbounded" />
</xs:all>
</xs:complexType>
<xs:complexType name="contextType">
<xs:attribute name="path" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:attribute name="locale" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:all>
<xs:element type="translation" minOccurs="0" maxOccurs="unbounded" />
<xs:element type="context" minOccurs="0" maxOccurs="unbounded" />
</xs:all>
</xs:complexType>
<xs:complexType name="translationType">
<xs:attribute name="keyPath" type="xs:string" minOccurs="1" maxOccurs="1" />
<xs:attribute name="template" type="xs:string" minOccurs="1" maxOccurs="1" />
</xs:complexType>
<xs:element name="localization" type="localizationType" />
</xs:schema>

View File

@ -1,176 +0,0 @@
package globalization;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.stream.XMLStreamException;
import org.apache.commons.text.StringSubstitutor;
import settings.Settings;
public class Translator {
Map<Locale, Map<String, String>> translations = new HashMap<Locale, Map<String, String>>();
//XXX: replace singleton pattern by dependency injection?
private static Translator instance;
private Translator() {
InitializeTranslations();
}
public static Translator getInstance() {
if (instance == null) {
instance = new Translator();
}
return instance;
}
private Settings settings() {
return Settings.getInstance();
}
private void InitializeTranslations() {
String path = this.settings().translationsPath();
File dir = new File(path);
File [] files = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".xml");
}
});
Map<Locale, Map<String, String>> translations = new HashMap<>();
TranslationXmlStreamReader translationReader = new TranslationXmlStreamReader();
for (File file : files) {
Iterable<TranslationEntry> entries = null;
try {
InputStream stream = new FileInputStream(file);
entries = translationReader.ReadFrom(stream);
} catch (FileNotFoundException ex) {
Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Translation file not found: %s", file), ex);
} catch (XMLStreamException ex) {
Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Error in translation file: %s", file), ex);
}
for(TranslationEntry entry : entries) {
Map<String, String> localTranslations = translations.get(entry.locale());
if(localTranslations == null) {
localTranslations = new HashMap<>();
translations.put(entry.locale(), localTranslations);
}
if(localTranslations.containsKey(entry.path())) {
Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Duplicate entry for locale '%s' and path '%s' in translation file '%s'. Falling back to default translations.", entry.locale(), entry.path(), file));
return;
}
localTranslations.put(entry.path(), entry.template());
}
}
// everything is fine, so we store all read translations
this.translations = translations;
}
private Map<String, Object> createMap(Map.Entry<String, Object>[] entries) {
HashMap<String, Object> map = new HashMap<>();
for (AbstractMap.Entry<String, Object> entry : entries) {
map.put(entry.getKey(), entry.getValue());
}
return map;
}
public String translate(Locale locale, String contextPath, String keyPath, AbstractMap.Entry<String, Object>... templateValues) {
Map<String, Object> map = createMap(templateValues);
return translate(locale, contextPath, keyPath, map);
}
public String translate(String contextPath, String keyPath, AbstractMap.Entry<String, Object>... templateValues) {
Map<String, Object> map = createMap(templateValues);
return translate(contextPath, keyPath, map);
}
public String translate(Locale locale, String contextPath, String keyPath, Map<String, Object> templateValues) {
return translate(locale, contextPath, keyPath, null, templateValues);
}
public String translate(String contextPath, String keyPath, Map<String, Object> templateValues) {
return translate(contextPath, keyPath, null, templateValues);
}
public String translate(Locale locale, String contextPath, String keyPath, String defaultTemplate, AbstractMap.Entry<String, Object>... templateValues) {
Map<String, Object> map = createMap(templateValues);
return translate(locale, contextPath, keyPath, defaultTemplate, map);
}
public String translate(String contextPath, String keyPath, String defaultTemplate, AbstractMap.Entry<String, Object>... templateValues) {
Map<String, Object> map = createMap(templateValues);
return translate(contextPath, keyPath, defaultTemplate, map);
}
public String translate(Locale locale, String contextPath, String keyPath, String defaultTemplate, Map<String, Object> templateValues) {
// look for requested language
String template = null;
if(locale != null)
template = getTemplateFromNearestPath(locale, contextPath, keyPath);
if(template != null)
return substitute(template, templateValues);
return translate(contextPath, keyPath, defaultTemplate, templateValues);
}
public String translate(String contextPath, String keyPath, String defaultTemplate, Map<String, Object> templateValues) {
// scan default languages
String template = null;
for(String language : this.settings().translationsDefaultLocales()) {
Locale defaultLocale = Locale.forLanguageTag(language);
template = getTemplateFromNearestPath(defaultLocale, contextPath, keyPath);
if(template != null)
break;
}
if(template == null)
template = defaultTemplate; // fallback template
return substitute(template, templateValues);
}
private String substitute(String template, Map<String, Object> templateValues) {
if(templateValues == null)
return template;
StringSubstitutor sub = new StringSubstitutor(templateValues);
String result = sub.replace(template);
return result;
}
private String getTemplateFromNearestPath(Locale locale, String contextPath, String keyPath) {
Map<String, String> localTranslations = this.translations.get(locale);
if(localTranslations == null)
return null;
String template = null;
while(true) {
String path = ContextPaths.combinePaths(contextPath, keyPath);
template = localTranslations.get(path);
if(template != null)
break; // found template
if(ContextPaths.isRoot(contextPath))
break; // nothing found
contextPath = ContextPaths.getParent(contextPath);
}
return template;
}
}

View File

@ -1,7 +1,12 @@
package api;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import java.util.Map;
public enum ApiError {
//COMMON
// COMMON
UNKNOWN(0, 500),
JSON(1, 400),
NO_BALANCE(2, 422),
@ -9,7 +14,7 @@ public enum ApiError {
UNAUTHORIZED(4, 403),
REPOSITORY_ISSUE(5, 500),
//VALIDATION
// VALIDATION
INVALID_SIGNATURE(101, 400),
INVALID_ADDRESS(102, 400),
INVALID_SEED(103, 400),
@ -36,22 +41,24 @@ public enum ApiError {
ADDRESS_NO_EXISTS(124, 404),
INVALID_CRITERIA(125, 400),
INVALID_REFERENCE(126, 400),
TRANSFORMATION_ERROR(127, 400),
//WALLET
// 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
// BLOCKS
BLOCK_NO_EXISTS(301, 404),
//TRANSACTIONS
// TRANSACTIONS
TRANSACTION_NO_EXISTS(311, 404),
PUBLIC_KEY_NOT_FOUND(304, 404),
TRANSACTION_INVALID(312, 400),
//NAMING
// NAMING
NAME_NO_EXISTS(401, 404),
NAME_ALREADY_EXISTS(402, 422),
NAME_ALREADY_FOR_SALE(403, 422),
@ -59,24 +66,24 @@ public enum ApiError {
NAME_SALE_NO_EXISTS(410, 404),
BUYER_ALREADY_OWNER(411, 422),
//POLLS
// 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
// ASSET
INVALID_ASSET_ID(601, 400),
INVALID_ORDER_ID(602, 400),
ORDER_NO_EXISTS(603, 404),
//NAME PAYMENTS
// NAME PAYMENTS
NAME_NOT_REGISTERED(701, 422),
NAME_FOR_SALE(702, 422),
NAME_WITH_SPACE(703, 422),
//ATs
// ATs
INVALID_DESC_LENGTH(801, 400),
EMPTY_CODE(802, 400),
DATA_SIZE(803, 400),
@ -85,7 +92,7 @@ public enum ApiError {
INVALID_TAGS_LENGTH(806, 400),
INVALID_CREATION_BYTES(809, 400),
//BLOG/Namestorage
// BLOG/Namestorage
BODY_EMPTY(901, 400),
BLOG_DISABLED(902, 403),
NAME_NOT_OWNER(903, 422),
@ -98,12 +105,14 @@ public enum ApiError {
COMMENT_NOT_EXISTING(910, 404),
INVALID_COMMENT_OWNER(911, 422),
//Messages
// Messages
MESSAGE_FORMAT_NOT_HEX(1001, 400),
MESSAGE_BLANK(1002, 400),
NO_PUBLIC_KEY(1003, 422),
MESSAGESIZE_EXCEEDED(1004, 400);
private final static Map<Integer, ApiError> map = stream(ApiError.values()).collect(toMap(apiError -> apiError.code, apiError -> apiError));
private final int code; // API error code
private final int status; // HTTP status code
@ -117,20 +126,15 @@ public enum ApiError {
}
public static ApiError fromCode(int code) {
for(ApiError apiError : ApiError.values()) {
if(apiError.code == code)
return apiError;
}
return null;
return map.get(code);
}
int getCode() {
public int getCode() {
return this.code;
}
int getStatus() {
public int getStatus() {
return this.status;
}
}

View File

@ -7,15 +7,16 @@ import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class ApiErrorMessage {
public int error;
protected int error;
public String message;
protected String message;
ApiErrorMessage() {
protected ApiErrorMessage() {
}
ApiErrorMessage(int errorCode, String message) {
public ApiErrorMessage(int errorCode, String message) {
this.error = errorCode;
this.message = message;
}
}

View File

@ -0,0 +1,18 @@
package api;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation lists potential ApiErrors that may be returned, or thrown, during the execution of this method.
* <p>
* Value is expected to be an array of ApiError enum instances.
*
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiErrors {
ApiError[] value() default {};
}

View File

@ -6,14 +6,16 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
public class ApiException extends WebApplicationException {
// HTTP status code
int status;
private static final long serialVersionUID = 4619299036312089050L;
// HTTP status code
public int status;
// API error code
int error;
public int error;
String message;
public String message;
public ApiException(int status, int error, String message) {
this(status, error, message, null);

View File

@ -0,0 +1,20 @@
package api;
import javax.servlet.http.HttpServletRequest;
import globalization.Translator;
public enum ApiExceptionFactory {
INSTANCE;
public ApiException createException(HttpServletRequest request, ApiError apiError, Throwable throwable, Object... args) {
String template = Translator.INSTANCE.translate("ApiError", request.getLocale().getLanguage(), apiError.name());
String message = String.format(template, args);
return new ApiException(apiError.getStatus(), apiError.getCode(), message, throwable);
}
public ApiException createException(HttpServletRequest request, ApiError apiError) {
return createException(request, apiError, null);
}
}

View File

@ -2,8 +2,6 @@ package api;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
@ -17,30 +15,21 @@ import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import api.resource.AnnotationPostProcessor;
import api.resource.ApiDefinition;
import settings.Settings;
public class ApiService {
private final Server server;
private final Set<Class<?>> resources;
private final ResourceConfig config;
public ApiService() {
// Resources to register
this.resources = new HashSet<Class<?>>();
this.resources.add(AddressesResource.class);
this.resources.add(AdminResource.class);
this.resources.add(AssetsResource.class);
this.resources.add(BlocksResource.class);
this.resources.add(NamesResource.class);
this.resources.add(PaymentsResource.class);
this.resources.add(TransactionsResource.class);
this.resources.add(UtilsResource.class);
this.resources.add(BlockExplorerResource.class); // block-explorer.html
this.resources.add(OpenApiResource.class); // Swagger/OpenAPI
this.resources.add(ApiDefinition.class); // API info
this.resources.add(AnnotationPostProcessor.class); // For API resource annotations
ResourceConfig config = new ResourceConfig(this.resources);
config = new ResourceConfig();
config.packages("api.resource");
config.register(OpenApiResource.class);
config.register(ApiDefinition.class);
config.register(AnnotationPostProcessor.class);
// Create RPC server
this.server = new Server(Settings.getInstance().getRpcPort());
@ -94,8 +83,9 @@ public class ApiService {
return instance;
}
Iterable<Class<?>> getResources() {
return resources;
public Iterable<Class<?>> getResources() {
// return resources;
return config.getClasses();
}
public void start() {

View File

@ -13,10 +13,10 @@ public class Security {
try {
remoteAddr = InetAddress.getByName(request.getRemoteAddr());
} catch (UnknownHostException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.UNAUTHORIZED);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
}
if (!remoteAddr.isLoopbackAddress())
throw ApiErrorFactory.getInstance().createError(ApiError.UNAUTHORIZED);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.UNAUTHORIZED);
}
}

View File

@ -1,9 +1,7 @@
package api;
package api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
@ -21,6 +19,10 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import api.ApiError;
import api.ApiErrors;
import api.ApiException;
import api.ApiExceptionFactory;
import data.account.AccountBalanceData;
import data.account.AccountData;
import qora.account.Account;
@ -32,12 +34,8 @@ import repository.RepositoryManager;
import transform.Transformer;
import utils.Base58;
@Path("addresses")
@Path("/addresses")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="/Api/AddressesResource")
}
)
@Tag(name = "Addresses")
public class AddressesResource {
@ -49,30 +47,17 @@ public class AddressesResource {
@Operation(
summary = "Fetch reference for next transaction to be created by address",
description = "Returns the base58-encoded signature of the last confirmed transaction created by address, failing that: the first incoming transaction to address. Returns \"false\" if there is no transactions.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET lastreference:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the base58-encoded transaction signature or \"false\"",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public String getLastReference(@Parameter(ref = "address") @PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
byte[] lastReference = null;
try (final Repository repository = RepositoryManager.getRepository()) {
@ -81,7 +66,7 @@ public class AddressesResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
if(lastReference == null || lastReference.length == 0) {
@ -95,31 +80,18 @@ public class AddressesResource {
@Path("/lastreference/{address}/unconfirmed")
@Operation(
summary = "Fetch reference for next transaction to be created by address, considering unconfirmed transactions",
description = "Returns the base58-encoded signature of the last confirmed/unconfirmed transaction created by address, failing that: the first incoming transaction. Returns \\\"false\\\" if there is no transactions.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET lastreference:address:unconfirmed"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
description = "Returns the base58-encoded signature of the last confirmed/unconfirmed transaction created by address, failing that: the first incoming transaction. Returns \"false\" if there is no transactions.",
responses = {
@ApiResponse(
description = "the base58-encoded transaction signature",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public String getLastReferenceUnconfirmed(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
byte[] lastReference = null;
try (final Repository repository = RepositoryManager.getRepository()) {
@ -128,7 +100,7 @@ public class AddressesResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
if(lastReference == null || lastReference.length == 0) {
@ -143,21 +115,9 @@ public class AddressesResource {
@Operation(
summary = "Validates the given address",
description = "Returns true/false.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET validate:address"),
@ExtensionProperty(name="summary.key", value="operation:summary"),
@ExtensionProperty(name="description.key", value="operation:description"),
})
},
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
)
}
)
@ -170,30 +130,17 @@ public class AddressesResource {
@Operation(
summary = "Return the generating balance of the given address",
description = "Returns the effective balance of the given address, used in Proof-of-Stake calculationgs when generating a new block.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET generatingbalance:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the generating balance",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public BigDecimal getGeneratingBalanceOfAddress(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
@ -201,7 +148,7 @@ public class AddressesResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -209,30 +156,17 @@ public class AddressesResource {
@Path("/balance/{address}")
@Operation(
summary = "Returns the confirmed balance of the given address",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET balance:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the balance",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public BigDecimal getGeneratingBalance(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
@ -240,7 +174,7 @@ public class AddressesResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -249,30 +183,17 @@ public class AddressesResource {
@Operation(
summary = "Asset-specific balance request",
description = "Returns the confirmed balance of the given address for the given asset key.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET assetbalance:assetid:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\", \"INVALID_ASSET_ID\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the balance",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", format = "number"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public BigDecimal getAssetBalance(@PathParam("assetid") long assetid, @PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
@ -280,7 +201,7 @@ public class AddressesResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -289,37 +210,24 @@ public class AddressesResource {
@Operation(
summary = "All assets owned by this address",
description = "Returns the list of assets for this address, with balances.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET assets:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the list of assets",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AccountBalanceData.class))),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AccountBalanceData.class)))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public List<AccountBalanceData> getAssets(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getAccountRepository().getAllBalances(address);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -327,24 +235,10 @@ public class AddressesResource {
@Path("/balance/{address}/{confirmations}")
@Operation(
summary = "Calculates the balance of the given address for the given confirmations",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET balance:address:confirmations"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the balance",
content = @Content(schema = @Schema(implementation = String.class)),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
content = @Content(schema = @Schema(type = "string", format = "number"))
)
}
)
@ -357,30 +251,17 @@ public class AddressesResource {
@Operation(
summary = "Get public key of address",
description = "Returns the base58-encoded account public key of the given address, or \"false\" if address not known or has no public key.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET publickey:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the public key",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public String getPublicKey(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ADDRESS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
AccountData accountData = repository.getAccountRepository().getAccount(address);
@ -396,7 +277,7 @@ public class AddressesResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -405,46 +286,33 @@ public class AddressesResource {
@Operation(
summary = "Convert public key into address",
description = "Returns account address based on supplied public key. Expects base58-encoded, 32-byte public key.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET publickey:address"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "the address",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.REPOSITORY_ISSUE})
public String fromPublicKey(@PathParam("publickey") String publicKey58) {
// Decode public key
byte[] publicKey;
try {
publicKey = Base58.decode(publicKey58);
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_PUBLIC_KEY, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY, e);
}
// Correct size for public key?
if (publicKey.length != Transformer.PUBLIC_KEY_LENGTH)
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_PUBLIC_KEY);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
try (final Repository repository = RepositoryManager.getRepository()) {
return Crypto.toAddress(publicKey);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}

View File

@ -1,10 +1,8 @@
package api;
package api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -17,14 +15,11 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import api.Security;
import controller.Controller;
@Path("admin")
@Path("/admin")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="/Api/AdminResource")
}
)
@Tag(name = "Admin")
public class AdminResource {
@ -52,25 +47,15 @@ public class AdminResource {
@Operation(
summary = "Fetch running time of server",
description = "Returns uptime in milliseconds",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="operation:description")
})
},
responses = {
@ApiResponse(
description = "uptime in milliseconds",
content = @Content(schema = @Schema(implementation = String.class)),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
content = @Content(schema = @Schema(type = "number"))
)
}
)
public String uptime() {
return Long.toString(System.currentTimeMillis() - Controller.startTime);
public long uptime() {
return System.currentTimeMillis() - Controller.startTime;
}
@GET
@ -78,20 +63,10 @@ public class AdminResource {
@Operation(
summary = "Shutdown",
description = "Shutdown",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="operation:description")
})
},
responses = {
@ApiResponse(
description = "\"true\"",
content = @Content(schema = @Schema(implementation = String.class)),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
content = @Content(schema = @Schema(type = "string"))
)
}
)

View File

@ -0,0 +1,114 @@
package api.resource;
import globalization.Translator;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.jaxrs2.Reader;
import io.swagger.v3.jaxrs2.ReaderListener;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
import java.lang.reflect.Method;
import java.util.Locale;
import javax.ws.rs.Path;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import api.ApiError;
import api.ApiErrorMessage;
import api.ApiErrors;
import api.ApiService;
public class AnnotationPostProcessor implements ReaderListener {
private static final Logger LOGGER = LogManager.getLogger(AnnotationPostProcessor.class);
@Override
public void beforeScan(Reader reader, OpenAPI openAPI) {
}
@Override
public void afterScan(Reader reader, OpenAPI openAPI) {
// Populate Components section with reusable parameters, like "limit" and "offset"
// We take the reusable parameters from AdminResource.globalParameters path "/admin/unused"
Components components = openAPI.getComponents();
PathItem globalParametersPathItem = openAPI.getPaths().get("/admin/unused");
if (globalParametersPathItem != null) {
for (Parameter parameter : globalParametersPathItem.getGet().getParameters())
components.addParameters(parameter.getName(), parameter);
openAPI.getPaths().remove("/admin/unused");
}
// Search all ApiService resources (classes) for @ApiErrors annotations
// to generate corresponding openAPI operation responses.
for (Class<?> clazz : ApiService.getInstance().getResources()) {
Path classPath = clazz.getAnnotation(Path.class);
if (classPath == null)
continue;
String classPathString = classPath.value();
if (classPathString.charAt(0) != '/')
classPathString = "/" + classPathString;
for (Method method : clazz.getDeclaredMethods()) {
ApiErrors apiErrors = method.getAnnotation(ApiErrors.class);
if (apiErrors == null)
continue;
LOGGER.info("Found @ApiErrors annotation on " + clazz.getSimpleName() + "." + method.getName());
PathItem pathItem = getPathItemFromMethod(openAPI, classPathString, method);
for (Operation operation : pathItem.readOperations())
for (ApiError apiError : apiErrors.value())
addApiErrorResponse(operation, apiError);
}
}
}
private PathItem getPathItemFromMethod(OpenAPI openAPI, String classPathString, Method method) {
Path path = method.getAnnotation(Path.class);
if (path == null)
throw new RuntimeException("API method has no @Path annotation?");
String pathString = path.value();
return openAPI.getPaths().get(classPathString + pathString);
}
private void addApiErrorResponse(Operation operation, ApiError apiError) {
String statusCode = Integer.toString(apiError.getStatus()) + " " + apiError.name();
// Create response for this HTTP response code if it doesn't already exist
ApiResponse apiResponse = operation.getResponses().get(statusCode);
if (apiResponse == null) {
Schema<?> errorMessageSchema = ModelConverters.getInstance().readAllAsResolvedSchema(ApiErrorMessage.class).schema;
MediaType mediaType = new MediaType().schema(errorMessageSchema);
Content content = new Content().addMediaType(javax.ws.rs.core.MediaType.APPLICATION_JSON, mediaType);
apiResponse = new ApiResponse().content(content);
operation.getResponses().addApiResponse(statusCode, apiResponse);
}
// Add this specific ApiError code as an example
int apiErrorCode = apiError.getCode();
String lang = Locale.getDefault().getLanguage();
ApiErrorMessage apiErrorMessage = new ApiErrorMessage(apiErrorCode, Translator.INSTANCE.translate("ApiError", lang, apiError.name()));
Example example = new Example().value(apiErrorMessage);
// XXX: addExamples(..) is not working in Swagger 2.0.4. This bug is referenced in https://github.com/swagger-api/swagger-ui/issues/2651
// Replace the call to .setExample(..) by .addExamples(..) when the bug is fixed.
apiResponse.getContent().get(javax.ws.rs.core.MediaType.APPLICATION_JSON).setExample(example);
//apiResponse.getContent().get(javax.ws.rs.core.MediaType.APPLICATION_JSON).addExamples(Integer.toString(apiErrorCode), example);
}
}

View File

@ -1,4 +1,4 @@
package api;
package api.resource;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.extensions.Extension;
@ -25,5 +25,4 @@ import io.swagger.v3.oas.annotations.tags.Tag;
}
)
public class ApiDefinition {
}
}

View File

@ -1,4 +1,4 @@
package api;
package api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -30,6 +30,9 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import api.ApiError;
import api.ApiErrors;
import api.ApiExceptionFactory;
import api.models.AssetWithHolders;
import api.models.OrderWithTrades;
import api.models.TradeWithOrderInfo;
@ -58,6 +61,7 @@ public class AssetsResource {
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<AssetData> getAllAssets(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
try (final Repository repository = RepositoryManager.getRepository()) {
List<AssetData> assets = repository.getAssetRepository().getAllAssets();
@ -69,7 +73,7 @@ public class AssetsResource {
return assets;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -85,9 +89,10 @@ public class AssetsResource {
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE})
public AssetWithHolders getAssetInfo(@QueryParam("assetId") Integer assetId, @QueryParam("assetName") String assetName, @Parameter(ref = "includeHolders") @QueryParam("includeHolders") boolean includeHolders) {
if (assetId == null && (assetName == null || assetName.isEmpty()))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_CRITERIA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
AssetData assetData = null;
@ -98,7 +103,7 @@ public class AssetsResource {
assetData = repository.getAssetRepository().fromAssetName(assetName);
if (assetData == null)
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ASSET_ID);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
List<AccountBalanceData> holders = null;
if (includeHolders)
@ -106,7 +111,7 @@ public class AssetsResource {
return new AssetWithHolders(assetData, holders);
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -122,14 +127,15 @@ public class AssetsResource {
)
}
)
@ApiErrors({ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE})
public List<OrderData> getAssetOrders(@Parameter(ref = "assetId") @PathParam("assetId") int assetId, @Parameter(ref = "otherAssetId") @PathParam("otherAssetId") int otherAssetId,
@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
try (final Repository repository = RepositoryManager.getRepository()) {
if (!repository.getAssetRepository().assetExists(assetId))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ASSET_ID);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
if (!repository.getAssetRepository().assetExists(otherAssetId))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ASSET_ID);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
List<OrderData> orders = repository.getAssetRepository().getOpenOrders(assetId, otherAssetId);
@ -140,7 +146,7 @@ public class AssetsResource {
return orders;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -158,14 +164,15 @@ public class AssetsResource {
)
}
)
@ApiErrors({ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE})
public List<TradeWithOrderInfo> getAssetTrades(@Parameter(ref = "assetId") @PathParam("assetId") int assetId, @Parameter(ref = "otherAssetId") @PathParam("otherAssetId") int otherAssetId,
@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
try (final Repository repository = RepositoryManager.getRepository()) {
if (!repository.getAssetRepository().assetExists(assetId))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ASSET_ID);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
if (!repository.getAssetRepository().assetExists(otherAssetId))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ASSET_ID);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
List<TradeData> trades = repository.getAssetRepository().getTrades(assetId, otherAssetId);
@ -184,7 +191,7 @@ public class AssetsResource {
return fullTrades;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -200,25 +207,26 @@ public class AssetsResource {
)
}
)
@ApiErrors({ApiError.INVALID_ORDER_ID, ApiError.ORDER_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public OrderWithTrades getAssetOrder(@PathParam("orderId") String orderId58) {
// Decode orderID
byte[] orderId;
try {
orderId = Base58.decode(orderId58);
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_ORDER_ID, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ORDER_ID, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
OrderData orderData = repository.getAssetRepository().fromOrderId(orderId);
if (orderData == null)
throw ApiErrorFactory.getInstance().createError(ApiError.ORDER_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ORDER_NO_EXISTS);
List<TradeData> trades = repository.getAssetRepository().getOrdersTrades(orderId);
return new OrderWithTrades(orderData, trades);
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -245,20 +253,21 @@ public class AssetsResource {
)
}
)
@ApiErrors({ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID})
public String issueAsset(IssueAssetTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValid();
if (result != ValidationResult.OK)
throw new ApiException(400, ApiError.INVALID_DATA.getCode(), "Transaction invalid: " + result.name());
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = IssueAssetTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}

View File

@ -0,0 +1,37 @@
package api.resource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import io.swagger.v3.oas.annotations.Operation;
@Path("/")
public class BlockExplorerResource {
@Context
HttpServletRequest request;
@GET
@Path("/block-explorer.html")
@Operation(hidden = true)
public String getBlockExplorer() {
ClassLoader loader = this.getClass().getClassLoader();
try (InputStream inputStream = loader.getResourceAsStream("block-explorer.html")) {
if (inputStream == null)
return "block-explorer.html resource not found";
return new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.joining("\n"));
} catch (IOException e) {
return "Error reading block-explorer.html resource";
}
}
}

View File

@ -1,11 +1,9 @@
package api;
package api.resource;
import data.block.BlockData;
import data.transaction.TransactionData;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -26,6 +24,10 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import api.ApiError;
import api.ApiErrors;
import api.ApiException;
import api.ApiExceptionFactory;
import api.models.BlockWithTransactions;
import qora.block.Block;
import repository.DataException;
@ -33,22 +35,9 @@ import repository.Repository;
import repository.RepositoryManager;
import utils.Base58;
@Path("blocks")
@Produces({
MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN
})
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "/Api/BlocksResource"
)
}
)
@Tag(
name = "Blocks"
)
@Path("/blocks")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Tag(name = "Blocks")
public class BlocksResource {
@Context
@ -59,28 +48,6 @@ public class BlocksResource {
@Operation(
summary = "Fetch block using base58 signature",
description = "Returns the block that matches the given signature",
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET signature"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
), @Extension(
properties = {
@ExtensionProperty(
name = "apiErrors",
value = "[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]",
parseValue = true
),
}
)
},
responses = {
@ApiResponse(
description = "the block",
@ -88,30 +55,18 @@ public class BlocksResource {
schema = @Schema(
implementation = BlockWithTransactions.class
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
public BlockWithTransactions getBlock(@PathParam("signature") String signature58, @Parameter(
ref = "includeTransactions"
) @QueryParam("includeTransactions") boolean includeTransactions) {
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getBlock(@PathParam("signature") String signature58, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
// Decode signature
byte[] signature;
try {
signature = Base58.decode(signature58);
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_SIGNATURE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
@ -120,7 +75,7 @@ public class BlocksResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -129,18 +84,6 @@ public class BlocksResource {
@Operation(
summary = "Fetch genesis block",
description = "Returns the genesis block",
extensions = @Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET first"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
),
responses = {
@ApiResponse(
description = "the block",
@ -148,31 +91,19 @@ public class BlocksResource {
schema = @Schema(
implementation = BlockWithTransactions.class
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
public BlockWithTransactions getFirstBlock(@Parameter(
ref = "includeTransactions"
) @QueryParam("includeTransactions") boolean includeTransactions) {
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getFirstBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(1);
return packageBlockData(repository, blockData, includeTransactions);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -181,18 +112,6 @@ public class BlocksResource {
@Operation(
summary = "Fetch last/newest block in blockchain",
description = "Returns the last valid block",
extensions = @Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET last"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
),
responses = {
@ApiResponse(
description = "the block",
@ -200,31 +119,19 @@ public class BlocksResource {
schema = @Schema(
implementation = BlockWithTransactions.class
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
public BlockWithTransactions getLastBlock(@Parameter(
ref = "includeTransactions"
) @QueryParam("includeTransactions") boolean includeTransactions) {
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getLastBlock(@Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
return packageBlockData(repository, blockData, includeTransactions);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -233,28 +140,6 @@ public class BlocksResource {
@Operation(
summary = "Fetch child block using base58 signature of parent block",
description = "Returns the child block of the block that matches the given signature",
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET child:signature"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
), @Extension(
properties = {
@ExtensionProperty(
name = "apiErrors",
value = "[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]",
parseValue = true
),
}
)
},
responses = {
@ApiResponse(
description = "the block",
@ -262,30 +147,18 @@ public class BlocksResource {
schema = @Schema(
implementation = BlockWithTransactions.class
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
public BlockWithTransactions getChild(@PathParam("signature") String signature58, @Parameter(
ref = "includeTransactions"
) @QueryParam("includeTransactions") boolean includeTransactions) {
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getChild(@PathParam("signature") String signature58, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
// Decode signature
byte[] signature;
try {
signature = Base58.decode(signature58);
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_SIGNATURE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
@ -293,7 +166,7 @@ public class BlocksResource {
// Check block exists
if (blockData == null)
throw ApiErrorFactory.getInstance().createError(ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
BlockData childBlockData = repository.getBlockRepository().fromReference(signature);
@ -302,7 +175,7 @@ public class BlocksResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -311,18 +184,6 @@ public class BlocksResource {
@Operation(
summary = "Generating balance of next block",
description = "Calculates the generating balance of the block that will follow the last block",
extensions = @Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET generatingbalance"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
),
responses = {
@ApiResponse(
description = "the generating balance",
@ -331,21 +192,11 @@ public class BlocksResource {
schema = @Schema(
implementation = BigDecimal.class
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public BigDecimal getGeneratingBalance() {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
@ -354,7 +205,7 @@ public class BlocksResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -363,28 +214,6 @@ public class BlocksResource {
@Operation(
summary = "Generating balance of block after specific block",
description = "Calculates the generating balance of the block that will follow the block that matches the signature",
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET generatingbalance:signature"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
), @Extension(
properties = {
@ExtensionProperty(
name = "apiErrors",
value = "[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]",
parseValue = true
),
}
)
},
responses = {
@ApiResponse(
description = "the block",
@ -393,28 +222,18 @@ public class BlocksResource {
schema = @Schema(
implementation = BigDecimal.class
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BigDecimal getGeneratingBalance(@PathParam("signature") String signature58) {
// Decode signature
byte[] signature;
try {
signature = Base58.decode(signature58);
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_SIGNATURE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
@ -422,14 +241,14 @@ public class BlocksResource {
// Check block exists
if (blockData == null)
throw ApiErrorFactory.getInstance().createError(ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
Block block = new Block(repository, blockData);
return block.calcNextBlockGeneratingBalance();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -438,42 +257,19 @@ public class BlocksResource {
@Operation(
summary = "Estimated time to forge next block",
description = "Calculates the time it should take for the network to generate the next block",
extensions = @Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET time"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
),
responses = {
@ApiResponse(
description = "the time in seconds", // in
// seconds?
description = "the time in seconds",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "number"
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public long getTimePerBlock() {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().getLastBlock();
@ -481,7 +277,7 @@ public class BlocksResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -490,18 +286,6 @@ public class BlocksResource {
@Operation(
summary = "Estimated time to forge block given generating balance",
description = "Calculates the time it should take for the network to generate blocks based on specified generating balance",
extensions = @Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET time:generatingbalance"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
),
responses = {
@ApiResponse(
description = "the time", // in seconds?
@ -510,18 +294,7 @@ public class BlocksResource {
schema = @Schema(
type = "number"
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
@ -534,18 +307,6 @@ public class BlocksResource {
@Operation(
summary = "Current blockchain height",
description = "Returns the block height of the last block.",
extensions = @Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET height"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
),
responses = {
@ApiResponse(
description = "the height",
@ -554,28 +315,18 @@ public class BlocksResource {
schema = @Schema(
type = "number"
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public int getHeight() {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockchainHeight();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -584,28 +335,6 @@ public class BlocksResource {
@Operation(
summary = "Height of specific block",
description = "Returns the block height of the block that matches the given signature",
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET height:signature"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
), @Extension(
properties = {
@ExtensionProperty(
name = "apiErrors",
value = "[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]",
parseValue = true
),
}
)
},
responses = {
@ApiResponse(
description = "the height",
@ -614,28 +343,18 @@ public class BlocksResource {
schema = @Schema(
type = "number"
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public int getHeight(@PathParam("signature") String signature58) {
// Decode signature
byte[] signature;
try {
signature = Base58.decode(signature58);
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_SIGNATURE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
@ -643,13 +362,13 @@ public class BlocksResource {
// Check block exists
if (blockData == null)
throw ApiErrorFactory.getInstance().createError(ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
return blockData.getHeight();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -658,28 +377,6 @@ public class BlocksResource {
@Operation(
summary = "Fetch block using block height",
description = "Returns the block with given height",
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET byheight:height"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
), @Extension(
properties = {
@ExtensionProperty(
name = "apiErrors",
value = "[\"BLOCK_NO_EXISTS\"]",
parseValue = true
),
}
)
},
responses = {
@ApiResponse(
description = "the block",
@ -687,31 +384,19 @@ public class BlocksResource {
schema = @Schema(
implementation = BlockWithTransactions.class
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
public BlockWithTransactions getByHeight(@PathParam("height") int height, @Parameter(
ref = "includeTransactions"
) @QueryParam("includeTransactions") boolean includeTransactions) {
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public BlockWithTransactions getByHeight(@PathParam("height") int height, @Parameter(ref = "includeTransactions") @QueryParam("includeTransactions") boolean includeTransactions) {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(height);
return packageBlockData(repository, blockData, includeTransactions);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -720,28 +405,6 @@ public class BlocksResource {
@Operation(
summary = "Fetch blocks starting with given height",
description = "Returns blocks starting with given height.",
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET byheight:height"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
), @Extension(
properties = {
@ExtensionProperty(
name = "apiErrors",
value = "[\"BLOCK_NO_EXISTS\"]",
parseValue = true
),
}
)
},
responses = {
@ApiResponse(
description = "blocks",
@ -749,24 +412,12 @@ public class BlocksResource {
schema = @Schema(
implementation = BlockWithTransactions.class
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
public List<BlockWithTransactions> getBlockRange(@PathParam("height") int height, @Parameter(
ref = "count"
) @QueryParam("count") int count) {
@ApiErrors({ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public List<BlockWithTransactions> getBlockRange(@PathParam("height") int height, @Parameter(ref = "count") @QueryParam("count") int count) {
boolean includeTransactions = false;
try (final Repository repository = RepositoryManager.getRepository()) {
@ -785,13 +436,25 @@ public class BlocksResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
/**
* Returns block, optionally including transactions.
* <p>
* Throws ApiException using ApiError.BLOCK_NO_EXISTS if blockData is null.
*
* @param repository
* @param blockData
* @param includeTransactions
* @return packaged block, with optional transactions
* @throws DataException
* @throws ApiException ApiError.BLOCK_NO_EXISTS
*/
private BlockWithTransactions packageBlockData(Repository repository, BlockData blockData, boolean includeTransactions) throws DataException {
if (blockData == null)
throw ApiErrorFactory.getInstance().createError(ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
List<TransactionData> transactions = null;
if (includeTransactions) {

View File

@ -1,4 +1,4 @@
package api;
package api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
@ -22,15 +22,14 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import api.ApiError;
import api.ApiErrors;
import api.ApiExceptionFactory;
import data.transaction.RegisterNameTransactionData;
@Path("/names")
@Produces({
MediaType.TEXT_PLAIN
})
@Tag(
name = "Names"
)
@Produces({ MediaType.TEXT_PLAIN})
@Tag(name = "Names")
public class NamesResource {
@Context
@ -61,20 +60,21 @@ public class NamesResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String buildTransaction(RegisterNameTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValid();
if (result != ValidationResult.OK)
throw new ApiException(400, ApiError.INVALID_DATA.getCode(), "Transaction invalid: " + result.name());
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = RegisterNameTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}

View File

@ -1,4 +1,4 @@
package api;
package api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
@ -22,15 +22,14 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import api.ApiError;
import api.ApiErrors;
import api.ApiExceptionFactory;
import data.transaction.PaymentTransactionData;
@Path("/payments")
@Produces({
MediaType.TEXT_PLAIN
})
@Tag(
name = "Payments"
)
@Produces({MediaType.TEXT_PLAIN})
@Tag(name = "Payments")
public class PaymentsResource {
@Context
@ -61,20 +60,21 @@ public class PaymentsResource {
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String buildTransaction(PaymentTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValid();
if (result != ValidationResult.OK)
throw new ApiException(400, ApiError.INVALID_DATA.getCode(), "Transaction invalid: " + result.name());
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = PaymentTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}

View File

@ -1,9 +1,7 @@
package api;
package api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
@ -30,10 +28,15 @@ import javax.ws.rs.core.MediaType;
import com.google.common.primitives.Bytes;
import api.ApiError;
import api.ApiErrors;
import api.ApiException;
import api.ApiExceptionFactory;
import api.models.SimpleTransactionSignRequest;
import data.transaction.GenesisTransactionData;
import data.transaction.PaymentTransactionData;
import data.transaction.TransactionData;
import globalization.Translator;
import repository.DataException;
import repository.Repository;
import repository.RepositoryManager;
@ -41,22 +44,9 @@ import transform.TransformationException;
import transform.transaction.TransactionTransformer;
import utils.Base58;
@Path("transactions")
@Produces({
MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN
})
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "/Api/TransactionsResource"
)
}
)
@Tag(
name = "Transactions"
)
@Path("/transactions")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Tag(name = "Transactions")
public class TransactionsResource {
@Context
@ -67,17 +57,6 @@ public class TransactionsResource {
@Operation(
summary = "Fetch transaction using transaction signature",
description = "Returns transaction",
extensions = {
@Extension(
properties = {
@ExtensionProperty(
name = "apiErrors",
value = "[\"INVALID_SIGNATURE\", \"TRANSACTION_NO_EXISTS\"]",
parseValue = true
),
}
)
},
responses = {
@ApiResponse(
description = "a transaction",
@ -85,39 +64,29 @@ public class TransactionsResource {
schema = @Schema(
implementation = TransactionData.class
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.TRANSACTION_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public TransactionData getTransactions(@PathParam("signature") String signature58) {
byte[] signature;
try {
signature = Base58.decode(signature58);
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_SIGNATURE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
if (transactionData == null)
throw ApiErrorFactory.getInstance().createError(ApiError.TRANSACTION_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_NO_EXISTS);
return transactionData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -126,28 +95,6 @@ public class TransactionsResource {
@Operation(
summary = "Fetch transactions using block signature",
description = "Returns list of transactions",
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "path",
value = "GET block:signature"
), @ExtensionProperty(
name = "description.key",
value = "operation:description"
)
}
), @Extension(
properties = {
@ExtensionProperty(
name = "apiErrors",
value = "[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]",
parseValue = true
),
}
)
},
responses = {
@ApiResponse(
description = "list of transactions",
@ -159,31 +106,17 @@ public class TransactionsResource {
}
)
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature58, @Parameter(
ref = "limit"
) @QueryParam("limit") int limit, @Parameter(
ref = "offset"
) @QueryParam("offset") int offset) {
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.BLOCK_NO_EXISTS, ApiError.REPOSITORY_ISSUE})
public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature58, @Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
byte[] signature;
try {
signature = Base58.decode(signature58);
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_SIGNATURE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
@ -191,7 +124,7 @@ public class TransactionsResource {
// check if block exists
if (transactions == null)
throw ApiErrorFactory.getInstance().createError(ApiError.BLOCK_NO_EXISTS);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_NO_EXISTS);
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, transactions.size());
@ -202,7 +135,7 @@ public class TransactionsResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -220,28 +153,18 @@ public class TransactionsResource {
implementation = TransactionData.class
)
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<TransactionData> getUnconfirmedTransactions() {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getTransactionRepository().getAllUnconfirmedTransactions();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -271,35 +194,25 @@ public class TransactionsResource {
implementation = TransactionData.class
)
)
),
extensions = {
@Extension(
name = "translation",
properties = {
@ExtensionProperty(
name = "description.key",
value = "success_response:description"
)
}
)
}
)
)
}
)
public List<TransactionData> searchTransactions(@QueryParam("startBlock") Integer startBlock, @QueryParam("blockLimit") Integer blockLimit,
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
public List<TransactionData> searchTransactions(@QueryParam("startBlock") Integer startBlock, @QueryParam("blockLimit") Integer blockLimit,
@QueryParam("txType") Integer txTypeNum, @QueryParam("address") String address, @Parameter(
ref = "limit"
) @QueryParam("limit") int limit, @Parameter(
ref = "offset"
) @QueryParam("offset") int offset) {
if ((txTypeNum == null || txTypeNum == 0) && (address == null || address.isEmpty()))
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_CRITERIA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
TransactionType txType = null;
if (txTypeNum != null) {
txType = TransactionType.valueOf(txTypeNum);
if (txType == null)
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_CRITERIA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
try (final Repository repository = RepositoryManager.getRepository()) {
@ -319,7 +232,7 @@ public class TransactionsResource {
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -348,6 +261,7 @@ public class TransactionsResource {
)
}
)
@ApiErrors({ApiError.TRANSFORMATION_ERROR})
public String signTransaction(SimpleTransactionSignRequest signRequest) {
try {
// Append null signature on the end before transformation
@ -364,7 +278,7 @@ public class TransactionsResource {
return Base58.encode(signedBytes);
} catch (TransformationException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
}
}
@ -395,6 +309,7 @@ public class TransactionsResource {
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String processTransaction(String rawBytes58) {
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] rawBytes = Base58.decode(rawBytes58);
@ -402,11 +317,11 @@ public class TransactionsResource {
Transaction transaction = Transaction.fromData(repository, transactionData);
if (!transaction.isSignatureValid())
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_SIGNATURE);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE);
ValidationResult result = transaction.isValid();
if (result != ValidationResult.OK)
throw new ApiException(400, ApiError.INVALID_DATA.getCode(), "Transaction invalid: " + result.name());
throw createTransactionInvalidException(request, result);
repository.getTransactionRepository().save(transactionData);
repository.getTransactionRepository().unconfirmTransaction(transactionData);
@ -414,11 +329,11 @@ public class TransactionsResource {
return "true";
} catch (TransformationException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@ -448,6 +363,7 @@ public class TransactionsResource {
)
}
)
@ApiErrors({ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public TransactionData decodeTransaction(String rawBytes58) {
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] rawBytes = Base58.decode(rawBytes58);
@ -465,23 +381,28 @@ public class TransactionsResource {
Transaction transaction = Transaction.fromData(repository, transactionData);
if (hasSignature && !transaction.isSignatureValid())
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_SIGNATURE);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE);
ValidationResult result = transaction.isValid();
if (result != ValidationResult.OK)
throw new ApiException(400, ApiError.INVALID_DATA.getCode(), "Transaction invalid: " + result.name());
throw createTransactionInvalidException(request, result);
if (!hasSignature)
transactionData.setSignature(null);
return transactionData;
} catch (TransformationException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.UNKNOWN, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
public static ApiException createTransactionInvalidException(HttpServletRequest request, ValidationResult result) {
String translatedResult = Translator.INSTANCE.translate("TransactionValidity", request.getLocale().getLanguage(), result.name());
return ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID, null, translatedResult);
}
}

View File

@ -1,4 +1,4 @@
package api;
package api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
@ -31,6 +31,10 @@ import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
import api.ApiError;
import api.ApiErrors;
import api.ApiExceptionFactory;
@Path("/utils")
@Produces({
MediaType.TEXT_PLAIN
@ -67,11 +71,12 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String fromBase64(String base64) {
try {
return HashCode.fromBytes(Base64.getDecoder().decode(base64.trim())).toString();
} catch (IllegalArgumentException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
}
@ -99,11 +104,12 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String base64from58(String base58) {
try {
return HashCode.fromBytes(Base58.decode(base58.trim())).toString();
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
}
@ -189,6 +195,7 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String getMnemonic(@QueryParam("entropy") String suppliedEntropy) {
/*
* BIP39 word lists have 2048 entries so can be represented by 11 bits.
@ -201,12 +208,12 @@ public class UtilsResource {
try {
entropy = Base58.decode(suppliedEntropy);
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
// Must be 16-bytes
if (entropy.length != 16)
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
} else {
// Generate entropy internally
UUID uuid = UUID.randomUUID();
@ -296,16 +303,17 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String privateKey(@PathParam("entropy") String entropy58) {
byte[] entropy;
try {
entropy = Base58.decode(entropy58);
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
if (entropy.length != 16)
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
byte[] privateKey = Crypto.digest(entropy);
@ -328,16 +336,17 @@ public class UtilsResource {
)
}
)
@ApiErrors({ApiError.INVALID_DATA})
public String publicKey(@PathParam("privateKey") String privateKey58) {
byte[] privateKey;
try {
privateKey = Base58.decode(privateKey58);
} catch (NumberFormatException e) {
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
if (privateKey.length != 32)
throw ApiErrorFactory.getInstance().createError(ApiError.INVALID_DATA);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
byte[] publicKey = new PrivateKeyAccount(null, privateKey).getPublicKey();

View File

@ -30,7 +30,6 @@ import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.StoredBlock;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.Utils;
import org.bitcoinj.core.VerificationException;
import org.bitcoinj.core.listeners.NewBestBlockListener;
import org.bitcoinj.net.discovery.DnsDiscovery;
@ -109,6 +108,7 @@ public class BTC {
}
}
@SuppressWarnings("unused")
public void saveAsBinary(File file) throws IOException {
try (final FileOutputStream fileOutputStream = new FileOutputStream(file, false)) {
MessageDigest digest = Sha256Hash.newDigest();

View File

@ -0,0 +1,52 @@
package globalization;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** Providing multi-language BIP39 word lists, downloaded from https://github.com/bitcoin/bips/tree/master/bip-0039 */
public enum BIP39WordList {
INSTANCE;
private Logger LOGGER = LogManager.getLogger(BIP39WordList.class);
private Map<String, List<String>> wordListsByLang;
private BIP39WordList() {
wordListsByLang = new HashMap<>();
}
public synchronized List<String> getByLang(String lang) {
List<String> wordList = wordListsByLang.get(lang);
if (wordList == null) {
ClassLoader loader = this.getClass().getClassLoader();
try (InputStream inputStream = loader.getResourceAsStream("BIP39/wordlist_" + lang + ".txt")) {
if (inputStream == null) {
LOGGER.warn("Can't locate '" + lang + "' BIP39 wordlist");
return null;
}
wordList = new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.toList());
} catch (IOException e) {
LOGGER.warn("Error reading '" + lang + "' BIP39 wordlist", e);
return null;
}
wordListsByLang.put(lang, wordList);
}
return Collections.unmodifiableList(wordList);
}
}

View File

@ -0,0 +1,52 @@
package globalization;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public enum Translator {
INSTANCE;
private final Logger LOGGER = LogManager.getLogger(Translator.class);
private final String DEFAULT_LANG = Locale.getDefault().getLanguage();
private final Map<String, ResourceBundle> resourceBundles = new HashMap<>();
private synchronized ResourceBundle getOrLoadResourceBundle(String className, String lang) {
final String bundleKey = className + ":" + lang;
ResourceBundle resourceBundle = resourceBundles.get(bundleKey);
if (resourceBundle != null)
return resourceBundle;
try {
resourceBundle = ResourceBundle.getBundle("i18n." + className, Locale.forLanguageTag(lang));
} catch (MissingResourceException e) {
LOGGER.warn("Can't locate '" + lang + "' translation resource bundle for " + className, e);
return null;
}
resourceBundles.put(bundleKey, resourceBundle);
return resourceBundle;
}
public String translate(final String className, final String key) {
return this.translate(className, DEFAULT_LANG, key);
}
public String translate(final String className, final String lang, final String key, final Object... args) {
ResourceBundle resourceBundle = getOrLoadResourceBundle(className, lang);
if (resourceBundle == null || !resourceBundle.containsKey(key))
return "!!" + lang + ":" + className + "." + key + "!!";
return String.format(resourceBundle.getString(key), args);
}
}

Some files were not shown because too many files have changed in this diff Show More