The development is moved to GitHub https://github.com/sculptor and the new blog is hosted there as well.
So take a look at Sculptors new home!
Sculptor is an open source productivity tool. You express your design intent in a textual DSL, from which Sculptor generates high quality Java code and configuration.
Entity Person {
String firstName
String lastName
Date birthDate
PersonRepository {
findBy(String firstName);
}
}
PersonRepository {
findBornBefore(Date bd) condition="birthDate < :bd" orderBy="firstName";
findByName(String name) condition="lastName i= :name or firstName i= :name";
}
Book book = book()
.createdBy("me")
.createdDate(now)
.title("Ender's Game")
.isbn("Some-ISBN")
.addMediaCharacter(mediaCharacter()
.name("Ender")
.build())
.build();
Trait Ordered {
def boolean greaterThan(@Ordered other);
def boolean greaterThanOrEquals(@Ordered other);
def boolean lessThan(@Ordered other);
def boolean lessThanOrEquals(@Ordered other);
def abstract protected int compare(@Ordered other);
}
Entity Product with Ordered {
String name
}
Trait PriceTag {
- protected Money normalPrice
protected double discount
def Money price;
def Money price(String currency);
}
Entity Product with PriceTag {
String name
}
BasicType Money with Ordered {
BigDecimal amount
String currency
}
Trait PriceTag {
- protected Money normalPrice
protected double discount
def Money price;
def Money price(String currency);
def protected int compare(Ordered other);
}
Entity Product with Ordered with PriceTag {
String name
}
@Override
public InventoryItem findByKey(String itemId)
throws InventoryItemNotFoundException {
InventoryItem result = super.findByKey(itemId);
loadFromHistory(result);
return result;
}
private void loadFromHistory(InventoryItem entity) {
InventoryItemSnapshot snapshot = getInventoryItemSnapshotRepository()
.getLatestSnapshot(entity.getItemId());
entity.applySnapshot(snapshot);
long snapshotVersion = snapshot == null ? 0 : snapshot.getVersion();
List<InventoryItemEvent> history = getInventoryItemEventRepository()
.findAllAfter(entity.getItemId(), snapshotVersion);
entity.loadFromHistory(history);
}
ValueObject InventoryItemSnapshot {
String itemId index
boolean activated
Long version
Repository InventoryItemSnapshotRepository {
@InventoryItemSnapshot getLatestSnapshot(String itemId);
protected findByCondition(PagingParameter pagingParameter);
save;
}
}
Service InventoryItemSnapshotter {
subscribe to inventoryItemTopic
inject @InventoryItemRepository
inject @InventoryItemSnapshotRepository
}
public void receive(Event event) {
if (!(event instanceof InventoryItemEvent)) {
return;
}
InventoryItemEvent inventoryItemEvent = (InventoryItemEvent) event;
String itemId = inventoryItemEvent.getItemId();
InventoryItemSnapshot snapshot = getInventoryItemSnapshotRepository()
.getLatestSnapshot(itemId);
long snapshotVersion = snapshot == null ? 1 : snapshot.getVersion();
long eventVersion = inventoryItemEvent.getAggregateVersion() == null ? 1
: inventoryItemEvent.getAggregateVersion();
if (eventVersion - snapshotVersion >= VERSION_DELTA) {
takeSnapshot(itemId);
}
}
private void takeSnapshot(String itemId) {
InventoryItem item;
try {
item = getInventoryItemRepository().findByKey(itemId);
} catch (InventoryItemNotFoundException e) {
log.warn("takeSnapshot failed: " + e.getMessage());
return;
}
InventoryItemSnapshot snapshot = item.createSnapshot();
getInventoryItemSnapshotRepository().save(snapshot);
}
Service InventoryFacade {
inject @InventoryItemRepository
createInventoryItem(String itemId, String name);
deactivateInventoryItem(String itemId);
renameInventoryItem(String itemId, String newName);
checkInItemsToInventory(String itemId, int count);
removeItemsFromInventory(String itemId, int count);
}
public void deactivateInventoryItem(String itemId) {
InventoryItem item = tryGetItem(itemId);
item.deactivate();
getInventoryItemRepository().save(item);
}
public void deactivate() {
if (!isActivated())
throw new IllegalStateException("already deactivated");
applyChange(new InventoryItemDeactivated(new Date(), getItemId()));
}
private void applyChange(InventoryItemEvent event) {
applyChange(event, true);
}
private void applyChange(InventoryItemEvent event, boolean isNew) {
DynamicMethodDispatcher.dispatch(this, event, "apply");
if (isNew) {
changes.add(event);
} else {
setVersion(event.getAggregateVersion());
}
}
public void apply(InventoryItemDeactivated event) {
setActivated(false);
}
@Override
public InventoryItem save(InventoryItem entity) {
InventoryItem saved = super.save(entity);
List<InventoryItemEvent> changes = entity.getUncommittedChanges();
changes = applyVersionToChanges(changes, saved.getVersion());
for (InventoryItemEvent each : changes) {
getInventoryItemEventRepository().save(each);
}
entity.markChangesAsCommitted();
return saved;
}
private List<InventoryItemEvent> applyVersionToChanges(
List<InventoryItemEvent> changes, long version) {
List<InventoryItemEvent> result = new ArrayList<InventoryItemEvent>();
long sequence = version * 1000;
for (InventoryItemEvent each : changes) {
result.add(each.withAggregateVersion(version).withChangeSequence(
sequence));
sequence++;
}
return result;
}
abstract DomainEvent InventoryItemEvent {
persistent
String itemId index
Long aggregateVersion nullable
Long changeSequence nullable
Repository InventoryItemEventRepository {
save publish to inventoryItemTopic;
List<@InventoryItemEvent> findAllForItem(String itemId);
protected findByCondition;
}
}
DomainEvent InventoryItemDeactivated extends @InventoryItemEvent {
}
Service InventoryListView {
subscribe to inventoryItemTopic
inject @InventoryItemListRepository
}
Service InventoryItemDetailView {
subscribe to inventoryItemTopic
inject @InventoryItemDetailsRepository
}
@Override
public InventoryItem findByKey(String itemId)
throws InventoryItemNotFoundException {
InventoryItem result = super.findByKey(itemId);
loadFromHistory(result);
return result;
}
private void loadFromHistory(InventoryItem entity) {
List<InventoryItemEvent> history = getInventoryItemEventRepository()
.findAllForItem(entity.getItemId());
entity.loadFromHistory(history);
}
public List<InventoryItemEvent> findAllForItem(String itemId) {
List<ConditionalCriteria> criteria = criteriaFor(
InventoryItemEvent.class).withProperty(itemId()).eq(itemId)
.orderBy(changeSequence()).build();
return findByCondition(criteria);
}
public void loadFromHistory(List<InventoryItemEvent> history) {
for (InventoryItemEvent each : history) {
applyChange(each, false);
}
}
private void applyChange(InventoryItemEvent event, boolean isNew) {
DynamicMethodDispatcher.dispatch(this, event, "apply");
if (isNew) {
changes.add(event);
} else {
setVersion(event.getAggregateVersion());
}
}
public void apply(InventoryItemCreated event) {
setActivated(true);
}
public void apply(InventoryItemDeactivated event) {
setActivated(false);
}
public void apply(Object other) {
// ignore
}
ValueObject Bet {
String betOfferId
String customerId
Double amount
Repository BetRepository {
save;
}
}
Service BettingEngine {
inject @BetRepository
placeBet(@Bet bet);
}

DomainEvent BetPlaced {
- Bet bet
}
Service BettingPublisher {
publishEvent(@BetPlaced betEvent) publish to jms:topic:bet;
}
Module customer {
Consumer BettingConsumer {
inject @CustomerStatisticsRepository
subscribe to jms:topic:bet
}
Service BettingQueryService {
getHighBetters => CustomerStatisticsRepository.findHighAverageCustomers;
}
Entity CustomerStatistics {
gap
String customerId key
int numberOfBets
double averageAmount index
Repository CustomerStatisticsRepository {
findByKey;
save;
List<@CustomerStatistics> findHighAverageCustomers(double limit);
protected findByCondition;
}
}
}
<repository>
<id>Akka</id>
<name>Akka Maven2 Repository</name>
<url>http://www.scalablesolutions.se/akka/repository/ </url>
</repository>
<repository>
<id>Multiverse</id>
<name>Multiverse Maven2 Repository</name>
<url>http://multiverse.googlecode.com/svn/maven-repository/releases/</url>
</repository>
<repository>
<id>GuiceyFruit</id>
<name>GuiceyFruit Maven2 Repository</name>
<url>http://guiceyfruit.googlecode.com/svn/repo/releases/ </url>
</repository>
<repository>
<id>JBoss</id>
<name>JBoss Maven2 Repository</name>
<url>https://repository.jboss.org/nexus/content/groups/public/ </url>
</repository>
...
<dependency>
<groupId>se.scalablesolutions.akka</groupId>
<artifactId>akka-core_2.8.0</artifactId>
<version>0.10</version>
</dependency>
package org.foo;
import org.fornax.cartridges.sculptor.framework.event.Event;
import org.fornax.cartridges.sculptor.framework.event.EventBus;
import org.fornax.cartridges.sculptor.framework.event.EventSubscriber;
import se.scalablesolutions.akka.actor.ActorRef;
import se.scalablesolutions.akka.actor.ActorRegistry;
import se.scalablesolutions.akka.actor.UntypedActor;
import se.scalablesolutions.akka.actor.UntypedActorFactory;
public class AkkaEventBus implements EventBus {
public boolean subscribe(final String topic, final EventSubscriber subscriber) {
UntypedActor.actorOf(new UntypedActorFactory() {
public UntypedActor create() {
return new ActorListener(topic, subscriber);
}
}).start();
return true;
}
public boolean unsubscribe(String topic, EventSubscriber subscriber) {
// TODO : implement mapping between arbitrary subscriber and actor in registry
return true;
}
public boolean publish(String topic, Event event) {
ActorRef[] actorsForTopic = ActorRegistry.actorsFor(topic);
for (int i = 0; i < actorsForTopic.length; i++) {
actorsForTopic[i].sendOneWay(event);
}
return true;
}
@SuppressWarnings("unchecked")
private class ActorListener extends UntypedActor {
final String topic;
final EventSubscriber subscriber;
ActorListener(String topic, EventSubscriber subscriber) {
this.topic = topic;
this.subscriber = subscriber;
this.getContext().setId(topic);
}
@Override
public void onReceive(Object message) throws Exception {
this.subscriber.receive((Event) message);
}
}
}
<bean id="akkaEventBus" class="org.foo.AkkaEventBus"/>
<alias name="akkaEventBus" alias="eventBus"/>
Application Universe {
basePackage=org.helloworld
Module milkyway {
Service PlanetService {
@BigLandingSuccess landOnPlanet(String planetName, String astronautName)
publish to milkywayChannel;
}
DomainEvent BigLandingSuccess {
String planetName
String astronautName
}
}
Module houston {
Service GroundControlService {
subscribe to milkywayChannel
bringOutTheChampagne(int noOfBottles);
}
}
}
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-jms</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-spring</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-camel</artifactId>
<version>5.3.2</version>
</dependency>
<!-- xbean is required for ActiveMQ broker configuration in the spring xml file -->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.7</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.1</version>
</dependency>
<camel:route>
<camel:from uri="direct:shippingChannel"/>
<camel:to uri="jms:topic:shippingEvent"/>
</camel:route>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:camel="http://camel.apache.org/schema/spring" xmlns:broker="http://activemq.apache.org/schema/core" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd">
<bean id="camelEventBusImpl" class="org.fornax.cartridges.sculptor.framework.event.CamelEventBusImpl"/>
<alias name="camelEventBusImpl" alias="eventBus"/>
<camel:camelContext id="camel">
<camel:package>org.sculptor.shipping</camel:package>
<camel:template id="producerTemplate"/>
</camel:camelContext>
<!--
Camel ActiveMQ to use the ActiveMQ broker
-->
<bean id="jms" class="org.apache.activemq.camel.component.ActiveMQComponent">
<property name="brokerURL" value="tcp://localhost:61616"/>
</bean>
</beans>
Application Universe {
basePackage=org.helloworld
Module milkyway {
Service PlanetService {
@BigLandingSuccess landOnPlanet(String planetName, String astronautName)
publish to milkywayChannel;
}
DomainEvent BigLandingSuccess {
String planetName
String astronautName
}
}
Module houston {
Service GroundControlService {
subscribe to milkywayChannel
bringOutTheChampagne(int noOfBottles);
}
}
}<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>org.springframework.integration</artifactId>
<version>1.0.4.RELEASE</version>
</dependency>
<jms:outbound-channel-adapter id="publicMilkywayChannel" destination="publicMilkywayTopic" channel="milkywayChannel"/>
<object-to-string-transformer input-channel="milkywayChannel" output-channel="milkywayMessagesAsStringChannel"/>And change the jms outbound channel to wire up with the new channel:
<jms:outbound-channel-adapter id="publicMilkywayChannel" destination="publicMilkywayTopic" channel="milkywayMessagesAsStringChannel"/>Spring-integration has a lot of transformers you can use. The above just transforms a java object to a string through its toString method. That is of course not always what you want, but it works for this example.
And for the other end, the subscriber:
@Service
public class GroundControlService {
@Autowired
@Qualifier("eventBus")
private EventBus eventBus;
@Override
public void receive(Event event) {
if (event instanceof BigLandingSuccess) {
eventBus.publish("earthChannel", new Celebration("We did it!"));
this.bringOutTheChampagne(ALL);
eventBus.publish("supplierChannel", new MissingResource("Champagne"));
}
}
}
@Service
public class SupplierService {
@Autowired
@Qualifier("eventBus")
private EventBus eventBus;
public void init() {
eventBus.subscribe("supplierChannel", new EventSubscriber() {
@Override
public void receive(Event event) {
if (event instanceof MissingResource) {
raceForContract(event);
}
}
});
}
}
Application Universe {
basePackage=org.helloworld
Module milkyway {
Service PlanetService {
@BigLandingSuccess landOnPlanet(String planetName, String astronautName)
publish to milkywayChannel;
}
DomainEvent BigLandingSuccess {
String planetName
String astronautName
}
}
Module houston {
Service GroundControlService {
subscribe to milkywayChannel
bringOutTheChampagne(int noOfBottles);
}
}
}
@Override
public void receive(Event event) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("receive not implemented");
}
@Override
public void receive(Event event) {
if (event instanceof BigLandingSuccess) {
this.bringOutTheChampagne(999999);
}
}
That is our model. So, what might the rest of the 'universe' be interested of here? Well, if NASA sent out a space ship with an astronaut on it who's mission was to land on some far far away planet, wouldn't they be interested in the event of a landing? I think so. So, when a astronaut lands on a planet, we would like the service to publish an event of this happening.
Application Universe {
basePackage=org.helloworld
Module milkyway {
Service PlanetService {
void landOnPlanet(String planetName, String astronautName);
}
}
}
Application Universe {
basePackage=org.helloworld
Module milkyway {
Service PlanetService {
void landOnPlanet(String planetName, String astronautName);
}
DomainEvent BigLandingSuccess {
String planetName
String astronautName
}
}
}
Application Universe {
basePackage=org.helloworld
Module milkyway {
Service PlanetService {
@BigLandingSuccess landOnPlanet(String planetName, String astronautName)
publish to milkywayChannel;
}
DomainEvent BigLandingSuccess {
String planetName
String astronautName
}
}
}DomainEvent ShipHasArrived {
- ShipId ship
- UnLocode port
}
DomainEvent ShipHasDepartured {
- ShipId ship
- UnLocode port
}
As you can see, you define a domain event in the same way as for entities or value objects.Service TrackingService {
@ShipHasArrived recordArrival(DateTime occurred, @Ship ship, @Port port)
publish to shippingChannel;
}
Service Statistics {
subscribe to shippingChannel
int getShipsInPort(@UnLocode port);
reset;
}
That was all for today, next time will it be more concrete with examples of how to use it.