I have been trying to simulate a Banking System in r3 Corda. My project can be found Here .
The components of my system are:
Central Bank [ Issues tokens for other Banks ]
BankA
BankB
Notary
I can deploy and run the nodes in my system. Then I can create brunches under these Banks. The following commands have been run in BankA and BankB terminal respectively:
flow start CreateAndShareAccountFlow accountName: brunchA1, partyToShareAccountInfoToList: [CentralBank, BankA, BankB]
flow start CreateAndShareAccountFlow accountName: brunchB1, partyToShareAccountInfoToList: [CentralBank, BankB, BankA]
I can issue tokens for a brunch from the Central Bank's Terminal
start IssueCashFlow accountName : brunchA1 , currency : USD , amount : 80
Now , I try to move tokens from brunchA1 to brunchB1 using the following command.
start MoveTokensBetweenAccounts senderAccountName : brunchA1, rcvAccountName : brunchB1 , currency : USD , amount : 10
But after running the vaultQuery in BankA and BankB , it's not at all transferred!
run vaultQuery contractStateType : com.r3.corda.lib.tokens.contracts.states.FungibleToken
Here's the code snippet for my MoveTokensBetweenAccounts:
import co.paralleluniverse.fibers.Suspendable;
import com.r3.corda.lib.accounts.contracts.states.AccountInfo;
import com.r3.corda.lib.accounts.workflows.UtilitiesKt;
import com.r3.corda.lib.accounts.workflows.flows.RequestKeyForAccount;
import com.r3.corda.lib.tokens.contracts.states.FungibleToken;
import com.r3.corda.lib.tokens.contracts.types.TokenType;
import com.r3.corda.lib.tokens.selection.TokenQueryBy;
import com.r3.corda.lib.tokens.selection.database.config.DatabaseSelectionConfigKt;
import com.r3.corda.lib.tokens.selection.database.selector.DatabaseTokenSelection;
import com.r3.corda.lib.tokens.workflows.flows.move.MoveTokensUtilities;
import com.r3.corda.lib.tokens.workflows.utilities.QueryUtilities;
import kotlin.Pair;
import net.corda.core.contracts.Amount;
import net.corda.core.contracts.CommandData;
import net.corda.core.contracts.CommandWithParties;
import net.corda.core.contracts.StateAndRef;
import net.corda.core.flows.*;
import net.corda.core.identity.AbstractParty;
import net.corda.core.identity.AnonymousParty;
import net.corda.core.identity.Party;
import net.corda.core.node.services.vault.QueryCriteria;
import net.corda.core.transactions.SignedTransaction;
import net.corda.core.transactions.TransactionBuilder;
import java.security.PublicKey;
import java.util.*;
#StartableByRPC
#InitiatingFlow
public class MoveTokensBetweenAccounts extends FlowLogic<String> {
private final String senderAccountName;
private final String rcvAccountName;
private final String currency;
private final Long amount;
public MoveTokensBetweenAccounts(String senderAccountName, String rcvAccountName, String currency, Long amount) {
this.senderAccountName = senderAccountName;
this.rcvAccountName = rcvAccountName;
this.currency = currency;
this.amount = amount;
}
#Override
#Suspendable
public String call() throws FlowException {
AccountInfo senderAccountInfo = UtilitiesKt.getAccountService(this).accountInfo(senderAccountName).get(0).getState().getData();
AccountInfo rcvAccountInfo = UtilitiesKt.getAccountService(this).accountInfo(rcvAccountName).get(0).getState().getData();
AnonymousParty senderAccount = subFlow(new RequestKeyForAccount(senderAccountInfo)); AnonymousParty rcvAccount = subFlow(new RequestKeyForAccount(rcvAccountInfo));
Amount<TokenType> amount = new Amount(this.amount, getInstance(currency));
QueryCriteria queryCriteria = QueryUtilities.heldTokenAmountCriteria(this.getInstance(currency), senderAccount).and(QueryUtilities.sumTokenCriteria());
List<Object> sum = getServiceHub().getVaultService().queryBy(FungibleToken.class, queryCriteria).component5();
if(sum.size() == 0)
throw new FlowException(senderAccountName + " has 0 token balance. Please ask the Central Bank to issue some cash.");
else {
Long tokenBalance = (Long) sum.get(0);
if(tokenBalance < this.amount)
throw new FlowException("Available token balance of " + senderAccountName + " is less than the cost of the ticket. Please ask the Central Bank to issue some cash ");
}
Pair<AbstractParty, Amount<TokenType>> partyAndAmount = new Pair(rcvAccount, amount);
DatabaseTokenSelection tokenSelection = new DatabaseTokenSelection(
getServiceHub(),
DatabaseSelectionConfigKt.MAX_RETRIES_DEFAULT,
DatabaseSelectionConfigKt.RETRY_SLEEP_DEFAULT,
DatabaseSelectionConfigKt.RETRY_CAP_DEFAULT,
DatabaseSelectionConfigKt.PAGE_SIZE_DEFAULT
);
Pair<List<StateAndRef<FungibleToken>>, List<FungibleToken>> inputsAndOutputs =
tokenSelection.generateMove(Arrays.asList(partyAndAmount), senderAccount, new TokenQueryBy(), getRunId().getUuid());
Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
TransactionBuilder transactionBuilder = new TransactionBuilder(notary);
MoveTokensUtilities.addMoveTokens(transactionBuilder, inputsAndOutputs.getFirst(), inputsAndOutputs.getSecond());
Set<PublicKey> mySigners = new HashSet<>();
List<CommandWithParties<CommandData>> commandWithPartiesList = transactionBuilder.toLedgerTransaction(getServiceHub()).getCommands();
for(CommandWithParties<CommandData> commandDataCommandWithParties : commandWithPartiesList) {
if(((ArrayList<PublicKey>)(getServiceHub().getKeyManagementService().filterMyKeys(commandDataCommandWithParties.getSigners()))).size() > 0) {
mySigners.add(((ArrayList<PublicKey>)getServiceHub().getKeyManagementService().filterMyKeys(commandDataCommandWithParties.getSigners())).get(0));
}
}
FlowSession rcvSession = initiateFlow(rcvAccountInfo.getHost());
SignedTransaction selfSignedTransaction = getServiceHub().signInitialTransaction(transactionBuilder, mySigners);
subFlow(new FinalityFlow(selfSignedTransaction, Arrays.asList(rcvSession)));
return null;
}
public TokenType getInstance(String currencyCode) {
Currency currency = Currency.getInstance(currencyCode);
return new TokenType(currency.getCurrencyCode(), 0);
}
}
#InitiatedBy(MoveTokensBetweenAccounts.class)
class MoveTokensBetweenAccountsResponder extends FlowLogic<Void> {
private final FlowSession otherSide;
public MoveTokensBetweenAccountsResponder(FlowSession otherSide) {
this.otherSide = otherSide;
}
#Override
#Suspendable
public Void call() throws FlowException {
subFlow(new ReceiveFinalityFlow(otherSide));
return null;
}
}
Am I missing anything fundamental while writing this MoveTokensBetweenAccounts Contract? I followed the official Github Samples
Any concrete suggestion to implement this Token-Movement would be of a great help!
Thank You!
QueryUtilities.heldTokenAmountCriteria() doesn't work with accounts; it only works with parties, instead you must use the following:
// Query vault for balance.
QueryCriteria heldByAccount = new QueryCriteria.VaultQueryCriteria().withExternalIds(Collections.singletonList(accountInfo.getIdentifier().getId()));
QueryCriteria queryCriteria = QueryUtilitiesKt
// Specify token type and issuer.
.tokenAmountWithIssuerCriteria(tokenTypePointer, issuer)
// Specify account.
.and(heldByAccount)
// Group by token type and aggregate.
.and(QueryUtilitiesKt.sumTokenCriteria());
Vault.Page<FungibleToken> results = proxy.vaultQueryByCriteria(queryCriteria, FungibleToken.class);
Amount<TokenType> totalBalance = QueryUtilitiesKt.rowsToAmount(tokenTypePointer, results);
Depending on the type of your token (fungible or non-fungible); I would use addMoveFungibleTokens() or addMoveNonFungibleTokens() instead of addMoveTokens().
Honestly I don't understand why you used the utility functions (i.e. DatabaseTokenSelection.generateMove() and addMoveTokens()); you use those if your transaction has multiple types of states as inputs/outputs (e.g. a car token and a US dollar token) and you want the swap of tokens to be atomic (either everything succeeds or everything fails). In your case, your transaction only has one type of states which is your token. You don't need all that complexity; just use the Tokens SDK out-of-the-box MoveFungibleTokensFlow.
Also, in your question you don't share how you found out that the tokens didn't move; did you create a flow test? How did that test query the accounts for their balance before and after the move?
Here's a simple example about moving tokens between 2 accounts; only thing that need to be changed in that example is to replace null here with a query criteria to only consume the tokens of the sender (see below); otherwise that move will consume any tokens that are held on the source node (you might end up moving tokens that belong to a different account; that's why you have to specify that query criteria):
// Query vault for balance.
QueryCriteria heldBySender = new QueryCriteria.VaultQueryCriteria().withExternalIds(Collections.singletonList(accountInfo.getIdentifier().getId()));
I highly recommend that you read my article on Tokens SDK, and even more important; go through the official free Corda course from R3; they have a big section on libraries (Tokens and Accounts), see here.
Also you have a typo, it's branch; not brunch.
Related
I am new to objectbox in flutter and already getting an error while trying to put object in the store. I have the following code:
objectbox class
import 'package:finsec/features/income/data/models/Income.dart';
import '../../../../objectbox.g.dart';
import 'dart:async';
class ObjectBox {
/// The Store of this app.
late final Store store;
late final Box<Income> incomeBox;
/// A stream of all notes ordered by date.
late final Stream<Query<Income>> incomeQueryStream;
ObjectBox._create(this.store) {
// Add any additional setup code, e.g. build queries.
incomeBox = Box<Income>(store);
final qBuilder = incomeBox.query(Income_.monthNumber.equals(1) & Income_.calendarYear.equals(2022));
incomeQueryStream = qBuilder.watch(triggerImmediately: true);
// Stream<Query<Income>> watchedQuery = incomeBox.query().watch();
}
/// Create an instance of ObjectBox to use throughout the app.
static Future<ObjectBox> create() async {
// Future<Store> openStore() {...} is defined in the generated objectbox.g.dart
final store = await openStore();
return ObjectBox._create(store);
}
}
then on my main.dart file i have the following
/// Provides access to the ObjectBox Store throughout the app.
late ObjectBox objectBox;
late SyncClient _syncClient;
bool hasBeenInitialized = false;
Future<void> main() async {
// This is required so ObjectBox can get the application directory
// to store the database in.
WidgetsFlutterBinding.ensureInitialized();
objectBox = await ObjectBox.create();
runApp(new MyHomePage( initialDate: DateTime.now()));
}
class MyHomePage extends StatefulWidget {
final DateTime initialDate;
const MyHomePage({required this.initialDate}) ;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
//some code here
}
in another class called incomeModel.dart, i am trying to call the putMany function. here is partial code from the other class
void saveIncome(String status) {
final isoCalendar = IsoCalendar.fromDateTime(this.form.value[datePaidLabel]);
int groupID = Utilities.getUniqueCode();
List<Income> incomeList = <Income>[];
Income income = new Income(
groupID: groupID,
expectedAmount: double.parse(this.form.value[incomeAmountLabel]),
actualAmount: double.parse(this.form.value[incomeAmountLabel]),
frequency: this.form.value[frequencyLabel],
dateReceived: this.form.value[datePaidLabel].toString(),
category: this.form.value[categoryLabel],
depositAcct: this.form.value[depositToLabel],
description: this.form.value[descriptionLabel],
status: status,
weekNumber: isoCalendar.weekNumber,
monthNumber: Utilities.epochConverter("MONTH", this.form.value[datePaidLabel]),
calendarYear: isoCalendar.year,
isActive: isActiveY,
groupName: currentTransactions,
);
incomeList.add(income);
DateTime dateDerivedValue = this.form.value[datePaidLabel];
for (int i = 1; i <= Utilities.getFrequency(this.form.value[frequencyLabel]); i++) {
dateDerivedValue = Utilities.getDate(
this.form.value[frequencyLabel], dateDerivedValue, incomeTransaction, i
);
incomeList.add(new Income(
groupID: groupID,
expectedAmount: double.parse(this.form.value[incomeAmountLabel]),
actualAmount: double.parse(this.form.value[incomeAmountLabel]),
frequency: this.form.value[frequencyLabel],
dateReceived: dateDerivedValue.toString(),
category: this.form.value[categoryLabel],
depositAcct: this.form.value[depositToLabel],
description: this.form.value[descriptionLabel],
status: status,
weekNumber: isoCalendar.weekNumber,
monthNumber:
Utilities.epochConverter(
"MONTH", this.form.value[datePaidLabel]),
calendarYear: isoCalendar.year,
isActive: isActiveY,
groupName: currentTransactions,
)
);
}
objectBox.incomeBox.putMany(incomeList);
}
as you can see , i am calling objectBox.incomeBox.putMany(incomeList) from incomeModel.dart class. the objectBox object is in the main class so i am importing it in incomeModel so that i can access it. however, i am getting the following error
Bad state: failed to create cursor: 10001 Can not modify object of sync-enabled type "Income" because sync has not been activated for this store.
i am not sure what this means or what to do. I will have many classes that will inserting data and i need to access the store from any class so that i can insert,update,delete data.
can someone helpme fix this? how can i make this work? thanks in advance
I solved this problem by use objectbox_sync_flutter_libs instead of objectbox_flutter_libs in pubspec.yml
I am new with Axon and maybe I missed something, but need help to understand.
I have a simple food cart aggregate.
Here is example:
#Aggregate
class FoodCard {
#AggregateIdentifier
private lateinit var foodCardId: UUID
private lateinit var selectedProduct: MutableMap<UUID, Int>
constructor()
#CommandHandler
constructor(command: CreateFoodCartCommand) {
AggregateLifecycle.apply(FoodCartCreateEvent(
UUID.randomUUID()
))
}
#CommandHandler
fun handle(command: SelectProductCommand) {
AggregateLifecycle
.apply(ProductSelectedEvent(foodCardId, command.productId, command.quantity))
}
#CommandHandler
fun handle(command: DeleteFoodCartCommand) {
AggregateLifecycle
.apply(FoodCartDeleteEvent(foodCardId))
}
#CommandHandler
fun handle(command: DeselectProductCommand) {
val productId = command.productId
if (!selectedProduct.containsKey(productId)) {
throw ProductDeselectionException("ProductDeselectionException")
}
AggregateLifecycle
.apply(ProductDeselectEvent(foodCardId, productId, command.quantity))
}
#EventSourcingHandler
fun on(event: FoodCartCreateEvent) {
foodCardId = event.foodCardId
selectedProduct = mutableMapOf()
}
#EventSourcingHandler
fun on(event: ProductSelectedEvent) {
selectedProduct.merge(
event.productId,
event.quantity
) {a, b -> a + b}
}
}
As ES I am using Axon Server.
For FoodCard projector I am using JPA repository that connects to DB.
I want to get all foodcards that contain special product (concrete UUID) and change quantity to -1 for all of them.
I understood there are two types of actions -> read and write
So the question how to correctly implement this flow with Axon?
Thanks
from your explanation and code I feel that you will probably need to complete your implementation of DeselectProductCommand introducing an EventSourcingHandler for ProductDeselectEvent. If I understood correctly your "quantity" information is stored into the selectProduct Map. In this case, based on your code, I see that the information of the quantity that should be subtracted to your product is in the command.
You will also need a Query, such as FindAllFoodCardByProductId, that will retrieve the foodCardId aggregate identifier that contains a certain productId: this operation will be performed on your Projection through the jpa repository.
As a reference you can have a look at the ref guide here https://docs.axoniq.io/reference-guide/implementing-domain-logic/query-handling on how to use QueryGateway into your controller and implement a QueryHandler into your Projection.
Corrado.
Issuing some cash within a Flow test - the flow returns the transaction with the output showing the correct Cash state. However, when I vault query for cash states, nothing is returned. Am I missing something?
IssueTokensFlow
#StartableByRPC
public class IssueTokensFlow extends FlowLogic<SignedTransaction> {
private static Double amount;
public IssueTokensFlow(double amount) {
this.amount = amount;
}
#Suspendable
#Override
public SignedTransaction call() throws FlowException {
// We retrieve the notary identity from the network map.
final Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
// Issue cash tokens equal to transfer amount
AbstractCashFlow.Result cashIssueResult = subFlow(new CashIssueFlow(
Currencies.DOLLARS(amount), OpaqueBytes.of(Byte.parseByte("1")), notary)
);
return cashIssueResult.getStx();
} }
IssueTokenFlow Test
#Test
public void testIssueCash() throws Exception {
IssueTokensFlow flow =
new IssueTokensFlow(100.00);
SignedTransaction transaction = a.startFlow(flow).get();
network.waitQuiescent();
Cash.State state = (Cash.State) transaction.getTx().getOutputStates().get(0);
assertEquals(state.getOwner(), chooseIdentity(a.getInfo()));
assertEquals(state.getAmount().getQuantity(), Currencies.DOLLARS(100.00).getQuantity());
// Above assertions pass
QueryCriteria.VaultQueryCriteria criteria = new QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL);
Vault.Page<ContractState> results = a.getServices().getVaultService().queryBy(Cash.State.class, criteria);
assertTrue(results.getStates().size() > 0);
// ^ This assertion fails
}
In Corda 3, whenever you query a node’s database as part of a test (e.g. to extract information from the node’s vault), you must wrap the query in a database transaction, as follows:
node.transaction(tx -> {
// Perform query here.
}
So your test would become:
#Test
public void testIssueCash() throws Exception {
IssueTokensFlow2 flow = new IssueTokensFlow2(100.00);
SignedTransaction transaction = a.startFlow(flow).get();
network.waitQuiescent();
Cash.State state = (Cash.State) transaction.getTx().getOutputStates().get(0);
assertEquals(state.getOwner(), chooseIdentity(a.getInfo()));
assertEquals(state.getAmount().getQuantity(), Currencies.DOLLARS(100.00).getQuantity());
// Above assertions pass
QueryCriteria.VaultQueryCriteria criteria = new QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL);
a.transaction(() -> {
Vault.Page<ContractState> results = a.getServices().getVaultService().queryBy(Cash.State.class, criteria);
assertTrue(results.getStates().size() > 0);
// ^ This assertion doesn't fail :)
return null;
});
}
How to verify an attribute whether it present in table or not without using scan in dynamodb?
In my usecase, From client side, The customer request with their Customer_id for knowing the values of the product. In server side, have to check whether the entered customer_id already present in DynamoDB table or not. If not, have to make a new entry.
How can I implement this case without using SCAN operation to the table?
It sounds to me that you want to do a conditional PutItem on this table: put the item into the table if there is not another item with the same customer_id. This is easy enough to do because the customer_id is the hash key of the table. From the PutItem documentation:
Note
To prevent a new item from replacing an existing item, use a conditional put operation with ComparisonOperator set to NULL for the
primary key attribute, or attributes.
Here is a quick example I coded up using the Dynamo DB document API in the Java SDK and running against DynamoDB Local:
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Expected;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
import com.amazonaws.services.dynamodbv2.model.KeyType;
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
import com.amazonaws.services.dynamodbv2.util.Tables;
public class StackOverflow {
private static final String EXAMPLE_TABLE_NAME = "example_table";
public static void main(String[] args) {
AmazonDynamoDB client = new AmazonDynamoDBClient(new BasicAWSCredentials("accessKey", "secretKey"));
client.setEndpoint("http://localhost:4000");
DynamoDB dynamoDB = new DynamoDB(client);
if (Tables.doesTableExist(client, "example_table")) client.deleteTable(EXAMPLE_TABLE_NAME);
// Create table with hash key 'customer_id'
CreateTableRequest createTableRequest = new CreateTableRequest();
createTableRequest.withTableName(EXAMPLE_TABLE_NAME);
createTableRequest.withKeySchema(new KeySchemaElement("customer_id", KeyType.HASH));
createTableRequest.withAttributeDefinitions(new AttributeDefinition("customer_id", ScalarAttributeType.S));
createTableRequest.withProvisionedThroughput(new ProvisionedThroughput(15l, 15l));
dynamoDB.createTable(createTableRequest);
Tables.waitForTableToBecomeActive(client, EXAMPLE_TABLE_NAME);
Table exampleTable = dynamoDB.getTable(EXAMPLE_TABLE_NAME);
exampleTable.putItem(new Item()
.withPrimaryKey("customer_id", "ABCD")
.withString("customer_name", "Jim")
.withString("customer_email", "jim#gmail.com"));
System.out.println("After Jim:");
exampleTable.scan()
.forEach(System.out::println);
System.out.println();
try {
exampleTable.putItem(new Item()
.withPrimaryKey("customer_id", "EFGH")
.withString("customer_name", "Garret")
.withString("customer_email", "garret#gmail.com"), new Expected("customer_id").notExist());
} catch (ConditionalCheckFailedException e) {
System.out.println("Conditional check failed!");
}
System.out.println("After Garret:");
exampleTable.scan()
.forEach(System.out::println);
System.out.println();
try {
exampleTable.putItem(new Item()
.withPrimaryKey("customer_id", "ABCD")
.withString("customer_name", "Bob")
.withString("customer_email", "bob#gmail.com"), new Expected("customer_id").notExist());
} catch (ConditionalCheckFailedException e) {
System.out.println("Conditional check failed!");
}
System.out.println("After Bob:");
exampleTable.scan()
.forEach(System.out::println);
}
}
Output:
After Jim:
{ Item: {customer_email=jim#gmail.com, customer_name=Jim, customer_id=ABCD} }
After Garret:
{ Item: {customer_email=garret#gmail.com, customer_name=Garret, customer_id=EFGH} }
{ Item: {customer_email=jim#gmail.com, customer_name=Jim, customer_id=ABCD} }
Conditional check failed!
After Bob:
{ Item: {customer_email=garret#gmail.com, customer_name=Garret, customer_id=EFGH} }
{ Item: {customer_email=jim#gmail.com, customer_name=Jim, customer_id=ABCD} }
Say I have an enum
public enum E {A,B,C}
Is it possible to add another value, say D, by AspectJ?
After googling around, it seems that there used to be a way to hack the private static field $VALUES, then call the constructor(String, int) by reflection, but seems not working with 1.7 anymore.
Here are several links:
http://www.javaspecialists.eu/archive/Issue161.html (provided by #WimDeblauwe )
and this: http://www.jroller.com/VelkaVrana/entry/modify_enum_with_reflection
Actually, I recommend you to refactor the source code, maybe adding a collection of valid region IDs to each enumeration value. This should be straightforward enough for subsequent merging if you use Git and not some old-school SCM tool like SVN.
Maybe it would even make sense to use a dynamic data structure altogether instead of an enum if it is clear that in the future the list of commands is dynamic. But that should go into the upstream code base. I am sure the devs will accept a good patch or pull request if prepared cleanly.
Remember: Trying to avoid refactoring is usually a bad smell, a symptom of an illness, not a solution. I prefer solutions to symptomatic workarounds. Clean code rules and software craftsmanship attitude demand that.
Having said the above, now here is what you can do. It should work under JDK 7/8 and I found it on Jérôme Kehrli's blog (please be sure to add the bugfix mentioned in one of the comments below the article).
Enum extender utility:
package de.scrum_master.util;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import sun.reflect.ConstructorAccessor;
import sun.reflect.FieldAccessor;
import sun.reflect.ReflectionFactory;
public class DynamicEnumExtender {
private static ReflectionFactory reflectionFactory =
ReflectionFactory.getReflectionFactory();
private static void setFailsafeFieldValue(Field field, Object target, Object value)
throws NoSuchFieldException, IllegalAccessException
{
// let's make the field accessible
field.setAccessible(true);
// next we change the modifier in the Field instance to
// not be final anymore, thus tricking reflection into
// letting us modify the static final field
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
int modifiers = modifiersField.getInt(field);
// blank out the final bit in the modifiers int
modifiers &= ~Modifier.FINAL;
modifiersField.setInt(field, modifiers);
FieldAccessor fa = reflectionFactory.newFieldAccessor(field, false);
fa.set(target, value);
}
private static void blankField(Class<?> enumClass, String fieldName)
throws NoSuchFieldException, IllegalAccessException
{
for (Field field : Class.class.getDeclaredFields()) {
if (field.getName().contains(fieldName)) {
AccessibleObject.setAccessible(new Field[] { field }, true);
setFailsafeFieldValue(field, enumClass, null);
break;
}
}
}
private static void cleanEnumCache(Class<?> enumClass)
throws NoSuchFieldException, IllegalAccessException
{
blankField(enumClass, "enumConstantDirectory"); // Sun (Oracle?!?) JDK 1.5/6
blankField(enumClass, "enumConstants"); // IBM JDK
}
private static ConstructorAccessor getConstructorAccessor(Class<?> enumClass, Class<?>[] additionalParameterTypes)
throws NoSuchMethodException
{
Class<?>[] parameterTypes = new Class[additionalParameterTypes.length + 2];
parameterTypes[0] = String.class;
parameterTypes[1] = int.class;
System.arraycopy(additionalParameterTypes, 0, parameterTypes, 2, additionalParameterTypes.length);
return reflectionFactory.newConstructorAccessor(enumClass .getDeclaredConstructor(parameterTypes));
}
private static Object makeEnum(Class<?> enumClass, String value, int ordinal, Class<?>[] additionalTypes, Object[] additionalValues)
throws Exception
{
Object[] parms = new Object[additionalValues.length + 2];
parms[0] = value;
parms[1] = Integer.valueOf(ordinal);
System.arraycopy(additionalValues, 0, parms, 2, additionalValues.length);
return enumClass.cast(getConstructorAccessor(enumClass, additionalTypes).newInstance(parms));
}
/**
* Add an enum instance to the enum class given as argument
*
* #param <T> the type of the enum (implicit)
* #param enumType the class of the enum to be modified
* #param enumName the name of the new enum instance to be added to the class
*/
#SuppressWarnings("unchecked")
public static <T extends Enum<?>> void addEnum(Class<T> enumType, String enumName) {
// 0. Sanity checks
if (!Enum.class.isAssignableFrom(enumType))
throw new RuntimeException("class " + enumType + " is not an instance of Enum");
// 1. Lookup "$VALUES" holder in enum class and get previous enum
// instances
Field valuesField = null;
Field[] fields = enumType.getDeclaredFields();
for (Field field : fields) {
if (field.getName().contains("$VALUES")) {
valuesField = field;
break;
}
}
AccessibleObject.setAccessible(new Field[] { valuesField }, true);
try {
// 2. Copy it
T[] previousValues = (T[]) valuesField.get(enumType);
List<T> values = new ArrayList<T>(Arrays.asList(previousValues));
// 3. build new enum
T newValue = (T) makeEnum(
enumType, // The target enum class
enumName, // THE NEW ENUM INSTANCE TO BE DYNAMICALLY ADDED
values.size(), new Class<?>[] {}, // could be used to pass values to the enum constuctor if needed
new Object[] {} // could be used to pass values to the enum constuctor if needed
);
// 4. add new value
values.add(newValue);
// 5. Set new values field
setFailsafeFieldValue(valuesField, null, values.toArray((T[]) Array.newInstance(enumType, 0)));
// 6. Clean enum cache
cleanEnumCache(enumType);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage(), e);
}
}
}
Sample application & enum:
package de.scrum_master.app;
/** In honour of "The Secret of Monkey Island"... ;-) */
public enum Command {
OPEN, CLOSE, PUSH, PULL, WALK_TO, PICK_UP, TALK_TO, GIVE, USE, LOOK_AT, TURN_ON, TURN_OFF
}
package de.scrum_master.app;
public class Server {
public void executeCommand(Command command) {
System.out.println("Executing command " + command);
}
}
package de.scrum_master.app;
public class Client {
private Server server;
public Client(Server server) {
this.server = server;
}
public void issueCommand(String command) {
server.executeCommand(
Command.valueOf(
command.toUpperCase().replace(' ', '_')
)
);
}
public static void main(String[] args) {
Client client = new Client(new Server());
client.issueCommand("use");
client.issueCommand("walk to");
client.issueCommand("undress");
client.issueCommand("sleep");
}
}
Console output with original enum:
Executing command USE
Executing command WALK_TO
Exception in thread "main" java.lang.IllegalArgumentException: No enum constant de.scrum_master.app.Command.UNDRESS
at java.lang.Enum.valueOf(Enum.java:236)
at de.scrum_master.app.Command.valueOf(Command.java:1)
at de.scrum_master.app.Client.issueCommand(Client.java:12)
at de.scrum_master.app.Client.main(Client.java:22)
Now you can either add an aspect with an advice executed after the enum class was loaded or just call this manually in your application before extended enum values are to be used for the first time. Here I am showing how it can be done in an aspect.
Enum extender aspect:
package de.scrum_master.aspect;
import de.scrum_master.app.Command;
import de.scrum_master.util.DynamicEnumExtender;
public aspect CommandExtender {
after() : staticinitialization(Command) {
System.out.println(thisJoinPoint);
DynamicEnumExtender.addEnum(Command.class, "UNDRESS");
DynamicEnumExtender.addEnum(Command.class, "SLEEP");
DynamicEnumExtender.addEnum(Command.class, "WAKE_UP");
DynamicEnumExtender.addEnum(Command.class, "DRESS");
}
}
Console output with extended enum:
staticinitialization(de.scrum_master.app.Command.<clinit>)
Executing command USE
Executing command WALK_TO
Executing command UNDRESS
Executing command SLEEP
Et voilà! ;-)