From 6eb352029563b68fa9d5095724683d32c617f94e Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 25 Jan 2019 15:22:56 +0000 Subject: [PATCH] Asset-related transactions API + performance improvements + fix Moved as much reflection out to class-static initializers as possible. Renamed some classes to fall in line with transaction type name to class name conversion, e.g. DEPLOY_AT -> DeployAt --- .../org/qora/api/resource/AssetsResource.java | 46 +++++ .../repository/TransactionRepository.java | 12 ++ .../qora/repository/hsqldb/HSQLDBSaver.java | 5 +- ...ava => HSQLDBAtTransactionRepository.java} | 4 +- ...ancelAssetOrderTransactionRepository.java} | 4 +- ...reateAssetOrderTransactionRepository.java} | 4 +- ... HSQLDBDeployAtTransactionRepository.java} | 4 +- .../HSQLDBTransactionRepository.java | 175 +++++++++++++++--- .../org/qora/transaction/ATTransaction.java | 4 +- .../org/qora/transaction/Transaction.java | 38 ++-- ...mer.java => AtTransactionTransformer.java} | 2 +- .../transaction/TransactionTransformer.java | 146 ++++++++++----- src/main/java/org/qora/v1feeder.java | 4 +- 13 files changed, 350 insertions(+), 98 deletions(-) rename src/main/java/org/qora/repository/hsqldb/transaction/{HSQLDBATTransactionRepository.java => HSQLDBAtTransactionRepository.java} (94%) rename src/main/java/org/qora/repository/hsqldb/transaction/{HSQLDBCancelOrderTransactionRepository.java => HSQLDBCancelAssetOrderTransactionRepository.java} (90%) rename src/main/java/org/qora/repository/hsqldb/transaction/{HSQLDBCreateOrderTransactionRepository.java => HSQLDBCreateAssetOrderTransactionRepository.java} (92%) rename src/main/java/org/qora/repository/hsqldb/transaction/{HSQLDBDeployATTransactionRepository.java => HSQLDBDeployAtTransactionRepository.java} (95%) rename src/main/java/org/qora/transform/transaction/{ATTransactionTransformer.java => AtTransactionTransformer.java} (98%) diff --git a/src/main/java/org/qora/api/resource/AssetsResource.java b/src/main/java/org/qora/api/resource/AssetsResource.java index 9ea8b33b..d721cd84 100644 --- a/src/main/java/org/qora/api/resource/AssetsResource.java +++ b/src/main/java/org/qora/api/resource/AssetsResource.java @@ -29,6 +29,7 @@ import org.qora.api.ApiErrors; import org.qora.api.ApiException; import org.qora.api.ApiExceptionFactory; import org.qora.api.model.TradeWithOrderInfo; +import org.qora.api.resource.TransactionsResource.ConfirmationStatus; import org.qora.crypto.Crypto; import org.qora.data.account.AccountBalanceData; import org.qora.data.account.AccountData; @@ -38,6 +39,7 @@ import org.qora.data.asset.TradeData; import org.qora.data.transaction.CancelAssetOrderTransactionData; import org.qora.data.transaction.CreateAssetOrderTransactionData; import org.qora.data.transaction.IssueAssetTransactionData; +import org.qora.data.transaction.TransactionData; import org.qora.repository.DataException; import org.qora.repository.Repository; import org.qora.repository.RepositoryManager; @@ -482,6 +484,50 @@ public class AssetsResource { } } + @GET + @Path("/transactions/{assetid}") + @Operation( + summary = "Transactions related to asset", + responses = { + @ApiResponse( + description = "Asset transactions", + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = TransactionData.class + ) + ) + ) + ) + } + ) + @ApiErrors({ + ApiError.INVALID_ADDRESS, ApiError.INVALID_ASSET_ID, ApiError.REPOSITORY_ISSUE + }) + public List getAssetTransactions(@Parameter( + ref = "assetid" + ) @PathParam("assetid") int assetId, @Parameter( + description = "whether to include confirmed, unconfirmed or both", + required = true + ) @QueryParam("confirmationStatus") ConfirmationStatus confirmationStatus, @Parameter( + ref = "limit" + ) @QueryParam("limit") Integer limit, @Parameter( + ref = "offset" + ) @QueryParam("offset") Integer offset, @Parameter( + ref = "reverse" + ) @QueryParam("reverse") Boolean reverse) { + try (final Repository repository = RepositoryManager.getRepository()) { + if (!repository.getAssetRepository().assetExists(assetId)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID); + + return repository.getTransactionRepository().getAssetTransactions(assetId, confirmationStatus, limit, offset, reverse); + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + @POST @Path("/order/delete") @Operation( diff --git a/src/main/java/org/qora/repository/TransactionRepository.java b/src/main/java/org/qora/repository/TransactionRepository.java index de4260dc..25dbed5c 100644 --- a/src/main/java/org/qora/repository/TransactionRepository.java +++ b/src/main/java/org/qora/repository/TransactionRepository.java @@ -32,6 +32,18 @@ public interface TransactionRepository { public List getSignaturesMatchingCriteria(Integer startBlock, Integer blockLimit, TransactionType txType, String address, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException; + /** + * Returns list of transactions relating to specific asset ID. + * + * @param assetId + * @param limit + * @param offset + * @param reverse + * @return list of transactions, or empty if none + */ + public List getAssetTransactions(int assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) + throws DataException; + /** * Returns list of unconfirmed transactions in timestamp-else-signature order. *

diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBSaver.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBSaver.java index 27b76aee..f34d5dc4 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBSaver.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBSaver.java @@ -5,8 +5,8 @@ import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; import java.util.List; /** @@ -77,8 +77,7 @@ public class HSQLDBSaver { * @return String */ private String formatInsertWithPlaceholders() { - String[] placeholders = new String[this.columns.size()]; - Arrays.setAll(placeholders, (int i) -> "?"); + List placeholders = Collections.nCopies(this.columns.size(), "?"); StringBuilder output = new StringBuilder(); output.append("INSERT INTO "); diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBATTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBAtTransactionRepository.java similarity index 94% rename from src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBATTransactionRepository.java rename to src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBAtTransactionRepository.java index 0fea82c1..8e8431ab 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBATTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBAtTransactionRepository.java @@ -10,9 +10,9 @@ import org.qora.repository.DataException; import org.qora.repository.hsqldb.HSQLDBRepository; import org.qora.repository.hsqldb.HSQLDBSaver; -public class HSQLDBATTransactionRepository extends HSQLDBTransactionRepository { +public class HSQLDBAtTransactionRepository extends HSQLDBTransactionRepository { - public HSQLDBATTransactionRepository(HSQLDBRepository repository) { + public HSQLDBAtTransactionRepository(HSQLDBRepository repository) { this.repository = repository; } diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCancelOrderTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCancelAssetOrderTransactionRepository.java similarity index 90% rename from src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCancelOrderTransactionRepository.java rename to src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCancelAssetOrderTransactionRepository.java index 6d023cdc..22a1c254 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCancelOrderTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCancelAssetOrderTransactionRepository.java @@ -10,9 +10,9 @@ import org.qora.repository.DataException; import org.qora.repository.hsqldb.HSQLDBRepository; import org.qora.repository.hsqldb.HSQLDBSaver; -public class HSQLDBCancelOrderTransactionRepository extends HSQLDBTransactionRepository { +public class HSQLDBCancelAssetOrderTransactionRepository extends HSQLDBTransactionRepository { - public HSQLDBCancelOrderTransactionRepository(HSQLDBRepository repository) { + public HSQLDBCancelAssetOrderTransactionRepository(HSQLDBRepository repository) { this.repository = repository; } diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateOrderTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateAssetOrderTransactionRepository.java similarity index 92% rename from src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateOrderTransactionRepository.java rename to src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateAssetOrderTransactionRepository.java index f38b45c5..0ccbf2a7 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateOrderTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBCreateAssetOrderTransactionRepository.java @@ -10,9 +10,9 @@ import org.qora.repository.DataException; import org.qora.repository.hsqldb.HSQLDBRepository; import org.qora.repository.hsqldb.HSQLDBSaver; -public class HSQLDBCreateOrderTransactionRepository extends HSQLDBTransactionRepository { +public class HSQLDBCreateAssetOrderTransactionRepository extends HSQLDBTransactionRepository { - public HSQLDBCreateOrderTransactionRepository(HSQLDBRepository repository) { + public HSQLDBCreateAssetOrderTransactionRepository(HSQLDBRepository repository) { this.repository = repository; } diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBDeployAtTransactionRepository.java similarity index 95% rename from src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java rename to src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBDeployAtTransactionRepository.java index a9e51e3a..bcc4784e 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBDeployAtTransactionRepository.java @@ -10,9 +10,9 @@ import org.qora.repository.DataException; import org.qora.repository.hsqldb.HSQLDBRepository; import org.qora.repository.hsqldb.HSQLDBSaver; -public class HSQLDBDeployATTransactionRepository extends HSQLDBTransactionRepository { +public class HSQLDBDeployAtTransactionRepository extends HSQLDBTransactionRepository { - public HSQLDBDeployATTransactionRepository(HSQLDBRepository repository) { + public HSQLDBDeployAtTransactionRepository(HSQLDBRepository repository) { this.repository = repository; } diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index 21cd49e3..dc4b710d 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -8,8 +8,10 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.List; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -22,45 +24,90 @@ import org.qora.repository.hsqldb.HSQLDBRepository; import org.qora.repository.hsqldb.HSQLDBSaver; import org.qora.transaction.Transaction.TransactionType; +import static org.qora.transaction.Transaction.TransactionType.*; + public class HSQLDBTransactionRepository implements TransactionRepository { private static final Logger LOGGER = LogManager.getLogger(HSQLDBTransactionRepository.class); + public static class RepositorySubclassInfo { + public Class clazz; + public Constructor constructor; + public Method fromBaseMethod; + public Method saveMethod; + } + + private static final RepositorySubclassInfo[] subclassInfos; + static { + subclassInfos = new RepositorySubclassInfo[TransactionType.values().length + 1]; + + for (TransactionType txType : TransactionType.values()) { + RepositorySubclassInfo subclassInfo = new RepositorySubclassInfo(); + + try { + subclassInfo.clazz = Class.forName( + String.join("", HSQLDBTransactionRepository.class.getPackage().getName(), ".", "HSQLDB", txType.className, "TransactionRepository")); + } catch (ClassNotFoundException e) { + LOGGER.debug(String.format("HSQLDBTransactionRepository subclass not found for transaction type \"%s\"", txType.name())); + continue; + } + + try { + subclassInfo.constructor = subclassInfo.clazz.getConstructor(HSQLDBRepository.class); + } catch (NoSuchMethodException | IllegalArgumentException e) { + LOGGER.debug(String.format("HSQLDBTransactionRepository subclass constructor not found for transaction type \"%s\"", txType.name())); + continue; + } + + try { + subclassInfo.fromBaseMethod = subclassInfo.clazz.getDeclaredMethod("fromBase", byte[].class, byte[].class, byte[].class, long.class, + BigDecimal.class); + } catch (IllegalArgumentException | SecurityException | NoSuchMethodException e) { + LOGGER.debug(String.format("HSQLDBTransactionRepository subclass's \"fromBase\" method not found for transaction type \"%s\"", txType.name())); + } + + try { + subclassInfo.saveMethod = subclassInfo.clazz.getDeclaredMethod("save", TransactionData.class); + } catch (IllegalArgumentException | SecurityException | NoSuchMethodException e) { + LOGGER.debug(String.format("HSQLDBTransactionRepository subclass's \"save\" method not found for transaction type \"%s\"", txType.name())); + } + + subclassInfos[txType.value] = subclassInfo; + } + + LOGGER.trace("Static init reflection completed"); + } + private HSQLDBTransactionRepository[] repositoryByTxType; + protected HSQLDBRepository repository; public HSQLDBTransactionRepository(HSQLDBRepository repository) { this.repository = repository; - this.repositoryByTxType = new HSQLDBTransactionRepository[256]; + this.repositoryByTxType = new HSQLDBTransactionRepository[TransactionType.values().length + 1]; for (TransactionType txType : TransactionType.values()) { - Class repositoryClass = getClassByTxType(txType); - if (repositoryClass == null) + RepositorySubclassInfo subclassInfo = subclassInfos[txType.value]; + + if (subclassInfo == null) + continue; + + if (subclassInfo.constructor == null) continue; try { - Constructor constructor = repositoryClass.getConstructor(HSQLDBRepository.class); - HSQLDBTransactionRepository txRepository = (HSQLDBTransactionRepository) constructor.newInstance(repository); - this.repositoryByTxType[txType.value] = txRepository; - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException e) { + this.repositoryByTxType[txType.value] = (HSQLDBTransactionRepository) subclassInfo.constructor.newInstance(repository); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException e) { continue; } } } + // Never called protected HSQLDBTransactionRepository() { } - private static Class getClassByTxType(TransactionType txType) { - try { - return Class.forName( - String.join("", HSQLDBTransactionRepository.class.getPackage().getName(), ".", "HSQLDB", txType.className, "TransactionRepository")); - } catch (ClassNotFoundException e) { - return null; - } - } - @Override public TransactionData fromSignature(byte[] signature) throws DataException { try (ResultSet resultSet = this.repository.checkedExecute("SELECT type, reference, creator, creation, fee FROM Transactions WHERE signature = ?", @@ -132,9 +179,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { throw new DataException("Unsupported transaction type [" + type.name() + "] during fetch from HSQLDB repository"); try { - Method method = txRepository.getClass().getDeclaredMethod("fromBase", byte[].class, byte[].class, byte[].class, long.class, BigDecimal.class); - return (TransactionData) method.invoke(txRepository, signature, reference, creatorPublicKey, timestamp, fee); - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + return (TransactionData) subclassInfos[type.value].fromBaseMethod.invoke(txRepository, signature, reference, creatorPublicKey, timestamp, fee); + } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { throw new DataException("Unsupported transaction type [" + type.name() + "] during fetch from HSQLDB repository"); } } @@ -343,6 +389,92 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } } + @Override + public List getAssetTransactions(int assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) + throws DataException { + TransactionType[] transactionTypes = new TransactionType[] { + ISSUE_ASSET, TRANSFER_ASSET, CREATE_ASSET_ORDER, CANCEL_ASSET_ORDER + }; + List typeValueStrings = Arrays.asList(transactionTypes).stream().map(type -> String.valueOf(type.value)).collect(Collectors.toList()); + + String sql = "SELECT Transactions.signature FROM Transactions"; + + // BlockTransactions if we want confirmed transactions + switch (confirmationStatus) { + case BOTH: + break; + + case CONFIRMED: + sql += " JOIN BlockTransactions ON BlockTransactions.transaction_signature = Transactions.signature"; + break; + + case UNCONFIRMED: + sql += " LEFT OUTER JOIN BlockTransactions ON BlockTransactions.transaction_signature = Transactions.signature"; + break; + } + + for (TransactionType type : transactionTypes) + sql += " LEFT OUTER JOIN " + type.className + "Transactions USING (signature)"; + + // assetID isn't in Cancel Asset Order so we need to join to the order + sql += " LEFT OUTER JOIN AssetOrders ON AssetOrders.asset_order_id = CancelAssetOrderTransactions.asset_order_id"; + + sql += " WHERE Transactions.type IN (" + String.join(", ", typeValueStrings) + ")"; + + // BlockTransactions if we want confirmed transactions + switch (confirmationStatus) { + case BOTH: + break; + + case CONFIRMED: + break; + + case UNCONFIRMED: + sql += " AND BlockTransactions.transaction_signature IS NULL"; + break; + } + + sql += " AND ("; + sql += "IssueAssetTransactions.asset_id = " + assetId; + sql += " OR "; + sql += "TransferAssetTransactions.asset_id = " + assetId; + sql += " OR "; + sql += "CreateAssetOrderTransactions.have_asset_id = " + assetId; + sql += " OR "; + sql += "CreateAssetOrderTransactions.want_asset_id = " + assetId; + sql += " OR "; + sql += "AssetOrders.have_asset_id = " + assetId; + sql += " OR "; + sql += "AssetOrders.want_asset_id = " + assetId; + sql += ") GROUP BY Transactions.signature, Transactions.creation ORDER BY Transactions.creation"; + + sql += (reverse == null || !reverse) ? " ASC" : " DESC"; + sql += HSQLDBRepository.limitOffsetSql(limit, offset); + + List transactions = new ArrayList(); + + try (ResultSet resultSet = this.repository.checkedExecute(sql)) { + if (resultSet == null) + return transactions; + + do { + byte[] signature = resultSet.getBytes(1); + + TransactionData transactionData = this.fromSignature(signature); + + if (transactionData == null) + // Something inconsistent with the repository + throw new DataException("Unable to fetch asset-related transaction from repository?"); + + transactions.add(transactionData); + } while (resultSet.next()); + + return transactions; + } catch (SQLException | DataException e) { + throw new DataException("Unable to fetch asset-related transactions from repository", e); + } + } + @Override public List getUnconfirmedTransactions(Integer limit, Integer offset, Boolean reverse) throws DataException { String sql = "SELECT signature FROM UnconfirmedTransactions ORDER BY creation"; @@ -417,9 +549,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository { throw new DataException("Unsupported transaction type [" + type.name() + "] during save into HSQLDB repository"); try { - Method method = txRepository.getClass().getDeclaredMethod("save", TransactionData.class); - method.invoke(txRepository, transactionData); - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + subclassInfos[type.value].saveMethod.invoke(txRepository, transactionData); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new DataException("Unsupported transaction type [" + type.name() + "] during save into HSQLDB repository"); } } diff --git a/src/main/java/org/qora/transaction/ATTransaction.java b/src/main/java/org/qora/transaction/ATTransaction.java index bdbfe24c..92ce728c 100644 --- a/src/main/java/org/qora/transaction/ATTransaction.java +++ b/src/main/java/org/qora/transaction/ATTransaction.java @@ -15,7 +15,7 @@ import org.qora.data.transaction.TransactionData; import org.qora.repository.DataException; import org.qora.repository.Repository; import org.qora.transform.TransformationException; -import org.qora.transform.transaction.ATTransactionTransformer; +import org.qora.transform.transaction.AtTransactionTransformer; import com.google.common.primitives.Bytes; @@ -38,7 +38,7 @@ public class ATTransaction extends Transaction { if (this.atTransactionData.getSignature() == null) { // Signature is SHA2-256 of serialized transaction data, duplicated to make standard signature size of 64 bytes. try { - byte[] digest = Crypto.digest(ATTransactionTransformer.toBytes(transactionData)); + byte[] digest = Crypto.digest(AtTransactionTransformer.toBytes(transactionData)); byte[] signature = Bytes.concat(digest, digest); this.atTransactionData.setSignature(signature); } catch (TransformationException e) { diff --git a/src/main/java/org/qora/transaction/Transaction.java b/src/main/java/org/qora/transaction/Transaction.java index a68a4b85..d72aebfe 100644 --- a/src/main/java/org/qora/transaction/Transaction.java +++ b/src/main/java/org/qora/transaction/Transaction.java @@ -35,6 +35,7 @@ public abstract class Transaction { // Transaction types public enum TransactionType { + // NOTE: must be contiguous or reflection fails GENESIS(1), PAYMENT(2), REGISTER_NAME(3), @@ -71,6 +72,8 @@ public abstract class Transaction { public final int value; public final String valueString; public final String className; + public final Class clazz; + public final Constructor constructor; private final static Map map = stream(TransactionType.values()).collect(toMap(type -> type.value, type -> type)); @@ -84,6 +87,25 @@ public abstract class Transaction { classNameParts[i] = classNameParts[i].substring(0, 1).toUpperCase().concat(classNameParts[i].substring(1)); this.className = String.join("", classNameParts); + + Class clazz = null; + try { + clazz = Class.forName(String.join("", Transaction.class.getPackage().getName(), ".", this.className, "Transaction")); + } catch (ClassNotFoundException e) { + LOGGER.debug(String.format("Transaction subclass not found for transaction type \"%s\"", this.name())); + this.clazz = null; + this.constructor = null; + return; + } + this.clazz = clazz; + + Constructor constructor = null; + try { + constructor = this.clazz.getConstructor(Repository.class, TransactionData.class); + } catch (NoSuchMethodException | SecurityException e) { + LOGGER.debug(String.format("Transaction subclass constructor not found for transaction type \"%s\"", this.name())); + } + this.constructor = constructor; } public static TransactionType valueOf(int value) { @@ -91,14 +113,6 @@ public abstract class Transaction { } } - public static Class getClassByTxType(TransactionType txType) { - try { - return Class.forName(String.join("", Transaction.class.getPackage().getName(), ".", txType.className, "Transaction")); - } catch (ClassNotFoundException e) { - return null; - } - } - // Validation results public enum ValidationResult { OK(1), @@ -207,13 +221,13 @@ public abstract class Transaction { TransactionType type = transactionData.getType(); try { - Class transactionClass = Transaction.getClassByTxType(type); - if (transactionClass == null) + Constructor constructor = type.constructor; + + if (constructor == null) throw new IllegalStateException("Unsupported transaction type [" + type.value + "] during fetch from repository"); - Constructor constructor = transactionClass.getConstructor(Repository.class, TransactionData.class); return (Transaction) constructor.newInstance(repository, transactionData); - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException e) { + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException e) { throw new IllegalStateException("Internal error with transaction type [" + type.value + "] during fetch from repository"); } } diff --git a/src/main/java/org/qora/transform/transaction/ATTransactionTransformer.java b/src/main/java/org/qora/transform/transaction/AtTransactionTransformer.java similarity index 98% rename from src/main/java/org/qora/transform/transaction/ATTransactionTransformer.java rename to src/main/java/org/qora/transform/transaction/AtTransactionTransformer.java index 570a9a4a..bd399c1c 100644 --- a/src/main/java/org/qora/transform/transaction/ATTransactionTransformer.java +++ b/src/main/java/org/qora/transform/transaction/AtTransactionTransformer.java @@ -14,7 +14,7 @@ import org.qora.utils.Serialization; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; -public class ATTransactionTransformer extends TransactionTransformer { +public class AtTransactionTransformer extends TransactionTransformer { // Property lengths private static final int SENDER_LENGTH = ADDRESS_LENGTH; diff --git a/src/main/java/org/qora/transform/transaction/TransactionTransformer.java b/src/main/java/org/qora/transform/transaction/TransactionTransformer.java index 326c7f5a..edbbdca4 100644 --- a/src/main/java/org/qora/transform/transaction/TransactionTransformer.java +++ b/src/main/java/org/qora/transform/transaction/TransactionTransformer.java @@ -35,6 +35,7 @@ public abstract class TransactionTransformer extends Transformer { protected static final int FEE_LENGTH = BIG_DECIMAL_LENGTH; protected static final int BASE_TYPELESS_LENGTH = TIMESTAMP_LENGTH + REFERENCE_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH; + /** Description of one component of raw transaction layout */ public enum TransformationType { TIMESTAMP("milliseconds (long)", TIMESTAMP_LENGTH), SIGNATURE("transaction signature", SIGNATURE_LENGTH), @@ -57,6 +58,7 @@ public abstract class TransactionTransformer extends Transformer { } } + /** Description of one component of raw transaction layout for API use */ @XmlAccessorType(XmlAccessType.NONE) public static class Transformation { @XmlElement @@ -71,17 +73,22 @@ public abstract class TransactionTransformer extends Transformer { this.transformation = format; } - @XmlElement(name = "format") + @XmlElement( + name = "format" + ) public String getFormat() { return this.transformation.description; } - @XmlElement(name = "length") + @XmlElement( + name = "length" + ) public Integer getLength() { return this.transformation.length; } } + /** Container for raw transaction layout */ public static class TransactionLayout { private List layout = new ArrayList<>(); @@ -94,23 +101,87 @@ public abstract class TransactionTransformer extends Transformer { } } - private static Class getClassByTxType(TransactionType txType) { - try { - return Class.forName(String.join("", TransactionTransformer.class.getPackage().getName(), ".", txType.className, "TransactionTransformer")); - } catch (ClassNotFoundException e) { - return null; + /** Container for cache of transformer subclass reflection info */ + public static class TransformerSubclassInfo { + public Class clazz; + public TransactionLayout transactionLayout; + public Method fromByteBufferMethod; + public Method getDataLengthMethod; + public Method toBytesMethod; + public Method toBytesForSigningImplMethod; + public Method toJSONMethod; + } + + /** Cache of transformer subclass info, keyed by transaction type */ + private static final TransformerSubclassInfo[] subclassInfos; + static { + subclassInfos = new TransformerSubclassInfo[TransactionType.values().length + 1]; + + for (TransactionType txType : TransactionType.values()) { + TransformerSubclassInfo subclassInfo = new TransformerSubclassInfo(); + + try { + subclassInfo.clazz = Class + .forName(String.join("", TransactionTransformer.class.getPackage().getName(), ".", txType.className, "TransactionTransformer")); + } catch (ClassNotFoundException e) { + LOGGER.debug(String.format("TransactionTransformer subclass not found for transaction type \"%s\"", txType.name())); + continue; + } + + try { + Field layoutField = subclassInfo.clazz.getDeclaredField("layout"); + subclassInfo.transactionLayout = ((TransactionLayout) layoutField.get(null)); + } catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException | SecurityException e) { + LOGGER.debug(String.format("TransactionTransformer subclass's \"layout\" field not found for transaction type \"%s\"", txType.name())); + } + + try { + subclassInfo.fromByteBufferMethod = subclassInfo.clazz.getDeclaredMethod("fromByteBuffer", ByteBuffer.class); + } catch (IllegalArgumentException | SecurityException | NoSuchMethodException e) { + LOGGER.debug(String.format("TransactionTransformer subclass's \"fromByteBuffer\" method not found for transaction type \"%s\"", txType.name())); + } + + try { + subclassInfo.getDataLengthMethod = subclassInfo.clazz.getDeclaredMethod("getDataLength", TransactionData.class); + } catch (IllegalArgumentException | SecurityException | NoSuchMethodException e) { + LOGGER.debug(String.format("TransactionTransformer subclass's \"getDataLength\" method not found for transaction type \"%s\"", txType.name())); + } + + try { + subclassInfo.toBytesMethod = subclassInfo.clazz.getDeclaredMethod("toBytes", TransactionData.class); + } catch (IllegalArgumentException | SecurityException | NoSuchMethodException e) { + LOGGER.debug(String.format("TransactionTransformer subclass's \"toBytes\" method not found for transaction type \"%s\"", txType.name())); + } + + try { + Class transformerClass = subclassInfo.clazz; + + // Check method is actually declared in transformer, otherwise we have to call superclass version + if (Arrays.asList(transformerClass.getDeclaredMethods()).stream().noneMatch(method -> method.getName().equals("toBytesForSigningImpl"))) + transformerClass = transformerClass.getSuperclass(); + + subclassInfo.toBytesForSigningImplMethod = transformerClass.getDeclaredMethod("toBytesForSigningImpl", TransactionData.class); + } catch (IllegalArgumentException | SecurityException | NoSuchMethodException e) { + LOGGER.debug(String.format("TransactionTransformer subclass's \"toBytesFromSigningImp\" method not found for transaction type \"%s\"", + txType.name())); + } + + try { + subclassInfo.toJSONMethod = subclassInfo.clazz.getDeclaredMethod("toJSON", TransactionData.class); + } catch (IllegalArgumentException | SecurityException | NoSuchMethodException e) { + LOGGER.debug(String.format("TransactionTransformer subclass's \"toJSON\" method not found for transaction type \"%s\"", txType.name())); + } + + subclassInfos[txType.value] = subclassInfo; } + + LOGGER.trace("Static init reflection completed"); } public static List getLayoutByTxType(TransactionType txType) { try { - Class transformerClass = TransactionTransformer.getClassByTxType(txType); - if (transformerClass == null) - return null; - - Field layoutField = transformerClass.getDeclaredField("layout"); - return ((TransactionLayout) layoutField.get(null)).getLayout(); - } catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException | SecurityException e) { + return subclassInfos[txType.value].transactionLayout.getLayout(); + } catch (ArrayIndexOutOfBoundsException | NullPointerException e) { return null; } } @@ -130,16 +201,15 @@ public abstract class TransactionTransformer extends Transformer { if (type == null) return null; - try { - Class transformerClass = TransactionTransformer.getClassByTxType(type); - if (transformerClass == null) - throw new TransformationException("Unsupported transaction type [" + type.value + "] during conversion from bytes"); + Method method = subclassInfos[type.value].fromByteBufferMethod; + if (method == null) + throw new TransformationException("Unsupported transaction type [" + type.value + "] during conversion from bytes"); - Method method = transformerClass.getDeclaredMethod("fromByteBuffer", ByteBuffer.class); + try { return (TransactionData) method.invoke(null, byteBuffer); } catch (BufferUnderflowException e) { throw new TransformationException("Byte data too short for transaction type [" + type.value + "]"); - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new TransformationException("Internal error with transaction type [" + type.value + "] during conversion from bytes"); } } @@ -148,13 +218,9 @@ public abstract class TransactionTransformer extends Transformer { TransactionType type = transactionData.getType(); try { - Class transformerClass = TransactionTransformer.getClassByTxType(type); - if (transformerClass == null) - throw new TransformationException("Unsupported transaction type [" + type.value + "] when requesting byte length"); - - Method method = transformerClass.getDeclaredMethod("getDataLength", TransactionData.class); + Method method = subclassInfos[type.value].getDataLengthMethod; return (int) method.invoke(null, transactionData); - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new TransformationException("Internal error with transaction type [" + type.value + "] when requesting byte length"); } } @@ -163,13 +229,9 @@ public abstract class TransactionTransformer extends Transformer { TransactionType type = transactionData.getType(); try { - Class transformerClass = TransactionTransformer.getClassByTxType(type); - if (transformerClass == null) - throw new TransformationException("Unsupported transaction type [" + type.value + "] during conversion to bytes"); - - Method method = transformerClass.getDeclaredMethod("toBytes", TransactionData.class); + Method method = subclassInfos[type.value].toBytesMethod; return (byte[]) method.invoke(null, transactionData); - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new TransformationException("Internal error with transaction type [" + type.value + "] during conversion to bytes"); } } @@ -187,17 +249,9 @@ public abstract class TransactionTransformer extends Transformer { TransactionType type = transactionData.getType(); try { - Class transformerClass = TransactionTransformer.getClassByTxType(type); - if (transformerClass == null) - throw new TransformationException("Unsupported transaction type [" + type.value + "] during conversion to bytes for signing"); - - // Check method is actually declared in transformer, otherwise we have to call superclass version - if (Arrays.asList(transformerClass.getDeclaredMethods()).stream().noneMatch(method -> method.getName().equals("toBytesForSigningImpl"))) - transformerClass = transformerClass.getSuperclass(); - - Method method = transformerClass.getDeclaredMethod("toBytesForSigningImpl", TransactionData.class); + Method method = subclassInfos[type.value].toBytesForSigningImplMethod; return (byte[]) method.invoke(null, transactionData); - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new TransformationException("Internal error with transaction type [" + type.value + "] during conversion to bytes for signing"); } } @@ -226,13 +280,9 @@ public abstract class TransactionTransformer extends Transformer { TransactionType type = transactionData.getType(); try { - Class transformerClass = TransactionTransformer.getClassByTxType(type); - if (transformerClass == null) - throw new TransformationException("Unsupported transaction type [" + type.value + "] during conversion to JSON"); - - Method method = transformerClass.getDeclaredMethod("toJSON", TransactionData.class); + Method method = subclassInfos[type.value].toJSONMethod; return (JSONObject) method.invoke(null, transactionData); - } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new TransformationException("Internal error with transaction type [" + type.value + "] during conversion to JSON"); } } diff --git a/src/main/java/org/qora/v1feeder.java b/src/main/java/org/qora/v1feeder.java index eebc4cbb..8160669f 100644 --- a/src/main/java/org/qora/v1feeder.java +++ b/src/main/java/org/qora/v1feeder.java @@ -45,7 +45,7 @@ import org.qora.repository.RepositoryManager; import org.qora.repository.hsqldb.HSQLDBRepositoryFactory; import org.qora.transform.TransformationException; import org.qora.transform.block.BlockTransformer; -import org.qora.transform.transaction.ATTransactionTransformer; +import org.qora.transform.transaction.AtTransactionTransformer; import org.qora.utils.Base58; import org.qora.utils.Pair; import org.qora.utils.Triple; @@ -498,7 +498,7 @@ public class v1feeder extends Thread { TransactionData transactionData = new ATTransactionData(sender, recipient, amount, Asset.QORA, message, fee, timestamp, reference); byte[] digest; try { - digest = Crypto.digest(ATTransactionTransformer.toBytes(transactionData)); + digest = Crypto.digest(AtTransactionTransformer.toBytes(transactionData)); byte[] signature = Bytes.concat(digest, digest); transactionData = new ATTransactionData(sender, recipient, amount, Asset.QORA, message, fee, timestamp, reference, signature);