forked from Qortal/qortal
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:
parent
aab6b69da1
commit
c4ed4b378c
26
.classpath
26
.classpath
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
37
NOTES.md
37
NOTES.md
@ -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.
|
@ -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>
|
@ -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
11
pom.xml
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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("/");
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
18
src/main/java/api/ApiErrors.java
Normal file
18
src/main/java/api/ApiErrors.java
Normal 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 {};
|
||||
}
|
@ -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);
|
20
src/main/java/api/ApiExceptionFactory.java
Normal file
20
src/main/java/api/ApiExceptionFactory.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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() {
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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"))
|
||||
)
|
||||
}
|
||||
)
|
114
src/main/java/api/resource/AnnotationPostProcessor.java
Normal file
114
src/main/java/api/resource/AnnotationPostProcessor.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
37
src/main/java/api/resource/BlockExplorerResource.java
Normal file
37
src/main/java/api/resource/BlockExplorerResource.java
Normal 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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
|
@ -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();
|
52
src/main/java/globalization/BIP39WordList.java
Normal file
52
src/main/java/globalization/BIP39WordList.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
52
src/main/java/globalization/Translator.java
Normal file
52
src/main/java/globalization/Translator.java
Normal 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
Loading…
Reference in New Issue
Block a user