Java/Qi4j Money Transfer example in 6 versions

389 views
Skip to first unread message

Marc Grue

unread,
Oct 28, 2010, 11:41:00 AM10/28/10
to object-composition
Hi all,

After reading Jim and Gertruds excellent Lean Architecture book, I
wanted to experiment with and code some of the different approaches
described and I ended up with 6 variations of the MoneyTransfer
example with java/qi4j. I'm going from simple to more advanced
implementations ending with dynamic balance calculation from money
transaction entries.

Code is available at:
https://docs.google.com/leaf?id=0Bx54yxX0qaXIMmY0Njc2NTEtZjAxMC00MWYwLTgyYTYtNDcyZjhlMGViMDI1&sort=name&layout=list&num=50
(qi4j-depency is available at http://www.qi4j.org/qi4j/downloads.html)

Below you'll find an overview of the different versions and some of
the reasoning behind. Might be interesting for others digging into
DCI. If you want to skip my introductory steps, you can go straight to
version 5 (with static balance) or version 6 (with money transaction
entries).

Am I on right track? Any comments are most welcome :)

Cheers,
Marc Grue
====================================
Examples overview:

v1
- Simple initial naive implementation with only a Transfer Money use
case.
- Entities are retrieved from the UnitOfWork and casted to a Role.
- Names of files/identifiers don't match the use case text (made it
before analyzing name discrepencies).

v2
- Names changed to map directly to the use case (as described in Lean
Architecture).
- SavingsAccount and CheckingAccount now extend a common AccountData
class.
- Logging moved from Data to Roles - Data shouldn't know about logging
needs.

v3
- API introduced making the Context less cluttered with
infrastructure.
- Use case now orchestrated in SourceAccount.
- Explicit post condition check in Context.
- AccountData replaced by BalanceData (simple balance data shouldn't
know about accounts).

v4
- Pay Bills use case introduced. It uses the MoneyTransferContext in a
loop - "nested contexts"?
- Role playing objects are now also passed as method arguments to a
Context (and not only data object ids).
- Notice how flexible we can combine data and roles in the three
entities and that CheckingAccountEntity can now both play a
DestinationAccount and a SourceAccount.
- SourceAccount#payBillsTo(creditors) stays readable as a Use case
algorithm by delegating checkAvailableFundsForAllBills(..) into a
separate method. TransferMoneyContext initialization inside the loop
could also have been splitted out for readability. I think its
valuable if a domain expert can immediately understand the meaning of
the code in the trigger method that orchestrates the Use case. That
should be where we can read and reason about the Use case undisturbed
by technical details.

v5
- RoleMap introduced. Both Contexts and Roles can access the RoleMap
(through ContextMixin and RoleMixin). Is this similar to the 4th type
of Context reference described on page 275 in Lean Architecture?
- Role objects are no longer passed as method arguments but looked up
within roles.
- No need to save Role objects as properties of the Context anymore.
- Roles now extend RoleMixin to have access to common services/
structures. This makes the Roles less cluttered with "infrastructure"
elements.

v6
- BalanceData is now calculated from a more elaborate money
transactions model with transactionEntries. I'm using the account
domain modelling ideas from Martin Fowlers "Analysis Patterns". This
is not a real world example and as such not at all a complete model.
Its purpose is to show a dynamic retrieval of the balance for accounts
and to demonstrate a more "near-world" example of DCI. "Transactions"
here are money transactions - hm, I hope people don't find this
confusing.
- SourceAccountRole holds a reference to a Transactions object for
balance lookup. The Transactions object holds a private
tempTransaction which is a buildup of a money transaction containing
TransactionEntries. By the end of the money transfer, all entries of
the tempTransaction is check to see if it balances, and if so is
saved.


Conceptual comments:

Data integrity and access from Role
--------------------------------------------------------
v1: MoneySourceRole extends SavingsAccountData directly and therefore
exposes Data directly to any client of the role.
v2: SourceAccount.Mixin has SavingsAccountData injected and saved in
the private variable "data".
v3: We are currently working with a simple balance that shouldn't know
anything about accounts, so I call it "BalanceData" instead.
SourceAccount.Mixin and SavingsAccountEntity now extends BalanceData.
We could imagine extending several atomic dumb Data parts
(AccountHolderData, InterestData etc), so it doesn't make sense to
point to a single "data" variable as in v2. Role and entity can be
composed of smaller atomic Data parts in a flexible and independent
way like this. Constraint: Role should only extend Data parts present
in the Entity in order. This should probably be enforced somehow (by
some annotation possibly). Otherwise the Role could be working on some
injected atomic Data part not in the entity and it wouldn't be saved
with the entity.
v4: BalanceData now protects the balance in an private interface, so
that clients can't set the balance directly (to an invalid amount).
The property allowedMinimum is still public - we can mix "private" and
"public" properties in an atomic Data part and use the private option
when constraints have to be enforced.
Now that the sensitive data is more protected, we can let the
SourceAccount extend BalanceData so that we have direct access in the
Role to the public Data methods instead of making proxy methods in the
Role to get access to the data (getBalance() for instance).
v5: No change.
v6:
- Balance is now calculated as the sum of TransactionEntries with the
given account id. Transactions is treated as a role, although it
contains data. Is this a sin?
- Note how Transaction extends TransactionEntries: an example of
composed Data objects.
- TransactionEntries has a reference to the Transaction (with the
@This annotation) that they belong to - they are both part of
TransactionEntity.
- In TransferMoneyContext we now have more generic account number
arguments and a "unified" accountEntity.
- In TransferMoneyContext we have skipped the init method. This
implementation doesn't seem to need it.

Directory organization and file naming
--------------------------------------------------------
v1: Two main directories: fast changing Behavior part (with context
and role subdirectories) and slowly changing Domain part (with data
and entity subdirectories). All files have a postfix: SomeContext,
SomeRole, SomeData, SomeEntity. We can see immediately what the file
is about.
v2: Accomodation to exact Use case naming by ommitting the Role
postfix from the Role files, so that we have SourceAccount instead of
SourceAccountRole.
v3: Api introduced in a separate main directory.
v4: Okay, now we have an issue: In the old days we had domain objects
having both state and behavior, and now we refer to Roles, Contexts
and dumb Domain Objects. I would say that Roles/Contexts/Use cases are
at least equally as much (if not more!) about the domain as the data
objects. So why exclude them from "domain" by assigning that name to
only the dumb data objects?! I therefore moved the behavior directory
into a main domain directory now containing behavior and structure.
Behavior contains context and role sub directories as before.
Structure contains data and entity sub directories.
v5: No change.
v6:
- In some ways we also have some behavior in the Data objects. We
increase a balance etc. So I moved data and entity a level up, and
skipped the structure/behavior organization. Context and Roles are
only about Use cases, right? So why not organize them as such. So now,
the domain contains three directories: data, entity and usecase (with
context and role sub directories). Those three concepts evolve
independently and would therefore have different sub-organizing in
real projects. (DDD) Bounded contexts would probably be containers for
the three.
- Decided to skip the Data postfix for Data files and instead add the
Role postfix to Roles again. I definitely want to be able to
distinguish between Data and Role objects, so that we don't have
"SourceAccount" and "SavingsAccount" causing Data/Role confusion.

Raoul Duke

unread,
Oct 28, 2010, 2:41:44 PM10/28/10
to object-co...@googlegroups.com
hi Marc,

wow.

Ant Kutschera

unread,
Oct 28, 2010, 4:04:55 PM10/28/10
to object-composition
Hi Marc,

Looks good - nice evolution in each step. I can't spot anything
strange. But I must say, it is hard to read... Part of that is the
Qi4J and me not being used to it.

The nested contexts look good too, IMHO.

Cheers,
Ant

Marc Grue

unread,
Oct 28, 2010, 6:29:02 PM10/28/10
to object-co...@googlegroups.com
Hi Ant and Raoul,

Thanks :)

Yeah, Qi4j can seem daunting at first, but you get to love it more and more. I'm only about to grasp the powers of the whole composite idea that makes it so cool. I'll continue with more advanced examples, hopefully demonstrating this more clearly. I only got the yellow, maybe orange belt in Qi4j ;)

Cheers,
Marc

> --
> You received this message because you are subscribed to the Google Groups "object-composition" group.
> To post to this group, send email to object-co...@googlegroups.com.
> To unsubscribe from this group, send email to object-composit...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/object-composition?hl=en.
>

Rickard Öberg

unread,
Oct 29, 2010, 12:43:04 AM10/29/10
to object-co...@googlegroups.com
On 2010-10-28 23.41, Marc Grue wrote:
> Hi all,
>
> After reading Jim and Gertruds excellent Lean Architecture book, I
> wanted to experiment with and code some of the different approaches
> described and I ended up with 6 variations of the MoneyTransfer
> example with java/qi4j. I'm going from simple to more advanced
> implementations ending with dynamic balance calculation from money
> transaction entries.

Awesome start! I have now added two more versions, based on all the
discussions and feedback from this list. I have sent it to you for
review and hopefully you can upload it to replace the one that is there now.

Description of v7:
* TransferMoneyContext is a plain class, no superclass or interfaces
required. In the constructor it gets the objects and then converts them
to roles:
public TransferMoneyContext( BalanceData source, BalanceData
destination )
{
this.source = (SourceAccountRole) source;
this.destination = (DestinationAccountRole) destination;
}
---
* The roles/default implementations are defined WITHIN the
TransferMoneyContext as inner interfaces/classes, so that it is clear
that they are only relevant within that context:
public class TransferMoneyContext
{
... interactions goes here ...

interface SourceAccountRole
{
... role methods goes here ...
class SourceAccountMixin
implements SourceAccountRole
{
... default impl goes here ...
}
}
... more roles goes here ...
}

* The context has two interactions:
public Integer availableFunds()
{
return source.availableFunds();
}

public void transfer( Integer amount )
throws IllegalArgumentException
{
source.transfer( amount );
}
---
One query and one command. I follow CQRS, so an enactment can only do
query OR command, not both.

* The entities (SavingsAccountEntity, CheckingAccountEntity,
CreditorEntity) extend the BalanceData and role interfaces (v8 fixes
this so that the entities in the domain does not have to know about
contexts and roles in which they are used), so that the casting in the
constructor works properly.

* To enact an interaction, the objects are looked up using data
interfaces, and the context is instantiated with these (NOTE: the
environment does NOT have to know about the roles!). Then an explicit
enactment (query or command) is started, which pushes the context onto
the contexts stack:
TransferMoneyContext context = new TransferMoneyContext(source,
destination);

// Query for half the balance
final Integer amountToTransfer = Contexts.withContext( context, new
Contexts.Query<Integer, TransferMoneyContext, RuntimeException>()
{
public Integer query( TransferMoneyContext transferMoneyContext )
throws RuntimeException
{
// Look up available funds from the source account
return transferMoneyContext.availableFunds()/2;
}
});

// Transfer from savings to checking
Contexts.withContext( context, new
Contexts.Command<TransferMoneyContext, IllegalArgumentException>()
{
public void command( TransferMoneyContext transferMoneyContext )
throws IllegalArgumentException
{
// Transfer from source to destination
transferMoneyContext.transfer( amountToTransfer );
}
});
---
The "Contexts.withContext" static method handles the proper
pushing/popping of the context on a stack, so that code within the
query/command methods can access the current context easily and
correctly. Generics is used to maximize static typing of context, return
value of query, and exceptions thrown.

In this case one context is used for two interactions, first a query and
then a command. The reuse of the context shows how there is less to do
for each interaction as they can focus solely on what is done with the
context, rather than how the context is set up.

* Since the context with the bindings is on a stack, it can be accessed
by any role, statically typed. From SourceAccountMixin:
public void transfer( Integer amount )
throws IllegalArgumentException
{
// Validate command
if (!(data.getBalance() >= amount))
throw new IllegalArgumentException("Not enough available funds");

// Command is ok - create events in the data
data.decreasedBalance( amount );

// Look up the destination account from the current transfer context
Contexts.context( TransferMoneyContext.class ).destination.deposit(
amount );
}
---
The context is looked up on the stack, and the first one with the given
type is returned. From the context we can look up the bound object, and
invoke role methods. If this is a stacked context, it is possible to
call Contexts.context(<othercontexttype>) to find previously "pushed"
contexts which this context is executing within.

* Data methods are considered events, and are hence in past tense. They
don't do any validation, apart from the bare input checking (in this
case, if the amount is positive). Any more complex logic is in the role.
This makes it easy to use EventSourcing with this setup.

* Data (as in the above case) is accessed through "@This BalanceData
data" injections, i.e. the role is considered to be a part of the object.

More details, but that's about it. AFAICT this setup complies with all
the rules outlined in articles and this mailing list.

The only remaining issue is that the domain entity explicitly extends
the role interfaces, thus knows about the contexts in which it is used.
v8 fixes this by separating the entity declarations into two layers, one
for the domain and one for the context. The domain one looks like this:
public interface CheckingAccountEntity
extends EntityComposite,
// Data
BalanceData
{}
---
And then the application layer, where the contexts reside, extends this
with:
public interface CheckingAccountATM
extends CheckingAccountEntity,
// Roles
TransferMoneyContext.SourceAccountRole,
TransferMoneyContext.DestinationAccountRole
{}
---
The binding from domain entity to context roles is now properly
separated from the domain, and yet centralized so that this
understanding does not have to be shown anywhere except in the assembly
code of the application. You can now statically analyze what objects can
do what within the application. There is no runtime dynamism going on.

That's pretty much it. This is, AFAICT, the most DCI-compliant way to
implement Qi4j applications. And also brutally simple, and provides easy
to read code that is properly separated in terms of responsibilities.

/Rickard

Rickard Öberg

unread,
Oct 29, 2010, 1:27:05 AM10/29/10
to object-co...@googlegroups.com
On 2010-10-29 12.43, Rickard �berg wrote:
> On 2010-10-28 23.41, Marc Grue wrote:
>> Hi all,
>>
>> After reading Jim and Gertruds excellent Lean Architecture book, I
>> wanted to experiment with and code some of the different approaches
>> described and I ended up with 6 variations of the MoneyTransfer
>> example with java/qi4j. I'm going from simple to more advanced
>> implementations ending with dynamic balance calculation from money
>> transaction entries.
>
> Awesome start! I have now added two more versions, based on all the
> discussions and feedback from this list. I have sent it to you for
> review and hopefully you can upload it to replace the one that is there
> now.

This was so inspiring that I have replaced the old DCI sample in Qi4j,
and added this instead. You can either get it from the qi4j-samples
module, or look at it on the web through our GitWeb here:
http://bit.ly/bYAENU

Very very nice. The simplicity of the code is very refreshing.

/Rickard

Petter Måhlén

unread,
Oct 29, 2010, 3:35:25 AM10/29/10
to object-co...@googlegroups.com
> The only remaining issue is that the domain entity explicitly extends the
> role interfaces, thus knows about the contexts in which it is used. v8 fixes
> this by separating the entity declarations into two layers, one for the
> domain and one for the context. The domain one looks like this:
> public interface CheckingAccountEntity
>   extends EntityComposite,
>   // Data
>      BalanceData
> {}
> ---
> And then the application layer, where the contexts reside, extends this
> with:
> public interface CheckingAccountATM
>   extends CheckingAccountEntity,
>   // Roles
>   TransferMoneyContext.SourceAccountRole,
>   TransferMoneyContext.DestinationAccountRole
> {}
> ---
> The binding from domain entity to context roles is now properly separated
> from the domain, and yet centralized so that this understanding does not
> have to be shown anywhere except in the assembly code of the application.
> You can now statically analyze what objects can do what within the
> application. There is no runtime dynamism going on.

Nice - it looks like this invalidates my comment in another thread
about 'the problem with Qi4j being static class composition'. I take
that back. :)

This is very cool, I think! I have two comments/questions, though,
which may be the same:

1. Semantically, this example is great, I think:

// Query for half the balance
final Integer amountToTransfer = Contexts.withContext( context, new
Contexts.Query<Integer, TransferMoneyContext, RuntimeException>()
{
public Integer query( TransferMoneyContext transferMoneyContext )
throws RuntimeException
{
// Look up available funds from the source account

return transferMoneyContext.availableFunds()/2; //
HERE IT HAPPENS
}
});

However, from a syntactic/readability perspective, it kind of sucks.
There's just so much text there that's in the way of understanding
what is actually going on. This is probably largely or only due to
Java as a language - I love it, but it has weaknesses with regard to
handling generics and anonymous inner classes/closures, and these
weaknesses are highlighted in that example. The only really relevant
line is the one I flagged as 'HERE IT HAPPENS'. Have you done any
experimentation with trying to reduce the amount of noise in the code?

2. I'm not a fan of static methods as they introduce close coupling.
Would it be possible to inject the current context instead of asking
for it using Contexts.context() in mixins? That would reduce the "text
overhead" in a statement like:


Contexts.context( TransferMoneyContext.class ).destination.deposit( amount );

to
transferMoneyContext.destination.deposit( amount );

Maybe that would introduce undesirable state into mixins or just be
bad from some other perspective? It feels like it would make it easier
to unit test them at least, since you can instantiate them directly
with your desired current context rather than having to set up your
static Context class.

Cheers,
Petter

Rickard Öberg

unread,
Oct 29, 2010, 3:42:11 AM10/29/10
to object-co...@googlegroups.com
On 2010-10-29 15.35, Petter M�hl�n wrote:
> Nice - it looks like this invalidates my comment in another thread
> about 'the problem with Qi4j being static class composition'. I take
> that back. :)

Exactly!

> However, from a syntactic/readability perspective, it kind of sucks.
> There's just so much text there that's in the way of understanding
> what is actually going on. This is probably largely or only due to
> Java as a language - I love it, but it has weaknesses with regard to
> handling generics and anonymous inner classes/closures, and these
> weaknesses are highlighted in that example. The only really relevant
> line is the one I flagged as 'HERE IT HAPPENS'. Have you done any
> experimentation with trying to reduce the amount of noise in the code?

I only wrote it this morning, so no. Better closure support would
definitely help tremendously here.

> 2. I'm not a fan of static methods as they introduce close coupling.
> Would it be possible to inject the current context instead of asking
> for it using Contexts.context() in mixins? That would reduce the "text
> overhead" in a statement like:
>
>
> Contexts.context( TransferMoneyContext.class ).destination.deposit( amount );
>
> to
> transferMoneyContext.destination.deposit( amount );
>
> Maybe that would introduce undesirable state into mixins or just be
> bad from some other perspective? It feels like it would make it easier
> to unit test them at least, since you can instantiate them directly
> with your desired current context rather than having to set up your
> static Context class.

What I *could* do is make a custom dependency injection annotation+provider:
@Context TransferMoneyContext context;
That would be no problem at all actually. Whether it's better... not
sure really.

/Rickard

Marc Grue

unread,
Oct 29, 2010, 3:41:25 AM10/29/10
to object-co...@googlegroups.com
Added Rickards two new Java/Qi4j versions of the money transfer example to the dci-qi4j Goggle Docs repository:

You'll find qi4j_money_transfer_02.zip in the moneytransfer folder. More examples are probably being added here...

Have fun!

Cheers,
Marc

Ant Kutschera

unread,
Oct 29, 2010, 7:24:51 AM10/29/10
to object-composition
On Oct 29, 9:35 am, Petter Måhlén <pettermah...@gmail.com> wrote:
> However, from a syntactic/readability perspective, it kind of sucks.

My two cents, not that anyone seems to listen to what I say or
understand it:

"kind of" is an understatement.

Rickard Öberg

unread,
Oct 29, 2010, 7:32:00 AM10/29/10
to object-co...@googlegroups.com
On 2010-10-29 15.35, Petter M�hl�n wrote:
> // Query for half the balance
> final Integer amountToTransfer = Contexts.withContext( context, new
> Contexts.Query<Integer, TransferMoneyContext, RuntimeException>()
> {
> public Integer query( TransferMoneyContext transferMoneyContext )
> throws RuntimeException
> {
> // Look up available funds from the source account
> return transferMoneyContext.availableFunds()/2; //
> HERE IT HAPPENS
> }
> });
>
> However, from a syntactic/readability perspective, it kind of sucks.

Ok, to improve this, the easiest solution would be to have a Concern
(aka "Advice" in AOP lingo) on the availableFunds() method that pushes
the context onto the stack and pops it when done. Then the code becomes:
Integer amountToTransfer = context.availableFunds() / 2;
which is better.

Unfortunately Qi4j does not support Concerns on POJO's right now, so
TransferMoneyContext would have to be a composite. We have planned to
add support for concerns on POJO's for next version though, which would
fix it.

/Rickard

Ant Kutschera

unread,
Oct 29, 2010, 7:56:55 AM10/29/10
to object-composition
On Oct 29, 1:32 pm, Rickard Öberg <rickardob...@gmail.com> wrote:
> On 2010-10-29 15.35, Petter M hl n wrote:
>
> > // Query for half the balance
> > final Integer amountToTransfer = Contexts.withContext( context, new
> > Contexts.Query<Integer, TransferMoneyContext, RuntimeException>()
> > {
> >    public Integer query( TransferMoneyContext transferMoneyContext )
> > throws RuntimeException
> >    {
> >       // Look up available funds from the source account
> >       return transferMoneyContext.availableFunds()/2;                //
> > HERE IT HAPPENS
> >    }
> > });
>
> > However, from a syntactic/readability perspective, it kind of sucks.
>
> Ok, to improve this, the easiest solution would be to have a Concern
> (aka "Advice" in AOP lingo) on the availableFunds() method that pushes
> the context onto the stack and pops it when done.

Is this a role-method (sorry, just looking at a snippet here).

In that case, this is exactly my criticism, and Trygve's too, that
there is a cyclic dependency between the role-method and the
context.

That is what I was trying to (unsuccessfully) explain in the
frontloading thread.

I suggested that the solution is to cast all objects into the right
roles before the interaction starts (not sure it would work in the
example snippet above...).

You cannot however cast child objects into the correct roles in pure
DCI, because it involves needing a place to store the child objects in
those roles. You could put them in the context, but then you are back
to the cyclic dependency problem.

To be able to cast all objects into roles up front, I suggested using
a service-style approach, by having things called role-objects. No
one liked that tho :-(

It's not just that the cyclic dependency is un-nice, its that it is
also not necessary. role-objects remove the need for the role to know
the context *entirely*.

Rickard Öberg

unread,
Oct 29, 2010, 10:31:50 AM10/29/10
to object-co...@googlegroups.com
On 2010-10-29 19.56, Ant Kutschera wrote:
>> Ok, to improve this, the easiest solution would be to have a Concern
>> (aka "Advice" in AOP lingo) on the availableFunds() method that pushes
>> the context onto the stack and pops it when done.
>
> Is this a role-method (sorry, just looking at a snippet here).

Nope, it's an interaction method on the context. Environment code cannot
call role methods, only interactions in the context can.

/Rickard

Ant Kutschera

unread,
Oct 29, 2010, 10:50:40 AM10/29/10
to object-composition
On Oct 29, 4:31 pm, Rickard Öberg <rickardob...@gmail.com> wrote:
> Nope, it's an interaction method on the context. Environment code cannot
> call role methods, only interactions in the context can.

What is it actually doing (in english)? I can't understand it, sorry.

Rickard Öberg

unread,
Oct 29, 2010, 10:54:45 AM10/29/10
to object-co...@googlegroups.com
On 2010-10-29 22.50, Ant Kutschera wrote:

> On Oct 29, 4:31 pm, Rickard �berg<rickardob...@gmail.com> wrote:
>> Nope, it's an interaction method on the context. Environment code cannot
>> call role methods, only interactions in the context can.
>
> What is it actually doing (in english)? I can't understand it, sorry.

"it"? You mean the interaction method? It takes the selected objects and
starts off the interaction.

Or what did you refer to? Be specific.

/Rickard

Ant Kutschera

unread,
Oct 29, 2010, 11:03:55 AM10/29/10
to object-composition
Sorry,

I meant the following code snippet:

> // Query for half the balance
> final Integer amountToTransfer = Contexts.withContext( context, new
> Contexts.Query<Integer, TransferMoneyContext, RuntimeException>()
> {
> public Integer query( TransferMoneyContext transferMoneyContext )
> throws RuntimeException
> {
> // Look up available funds from the source account
> return transferMoneyContext.availableFunds()/2; //
> HERE IT HAPPENS
> }
> });

You said it was outside of the context?

Rickard Öberg

unread,
Oct 29, 2010, 11:32:34 AM10/29/10
to object-co...@googlegroups.com

Yes, all of that was just to invoke an interaction in a context, in
order to properly push the context onto the context stack. But if I put
an advice/concern/interceptor (whatever you want to call it) on the
interaction that does it, it simple becomes:
Integer amountToTransfer = context.availableFunds()/2;

/Rickard

Ant Kutschera

unread,
Oct 29, 2010, 1:55:16 PM10/29/10
to object-composition
Great improvement :-)
Reply all
Reply to author
Forward
0 new messages