Showing posts with label tapestry 5. Show all posts
Showing posts with label tapestry 5. Show all posts

Wednesday, March 25, 2009

Tapestry 5 - Scala : view article in HTML, JSON or XML

We must have the REST in our blog, it will attract a lot of investors


The last time we saw how to make basic CRUD functionalities for the blog. That was quite a long time, because I started to encounter some rough edges that make Scala and T5 don't go together as smoothly as expected. Nevertheless, all problems were worked around.

This article is about how simply one can customized the output format of her URL in a RESTful way (see that for more details about REST, but whatever says this too academic article, we all well know that "REST" means "pretty shiny URLs" and "being 2.0, not like whose outdated web service" ;).

So, basically, what we will provide is two different ways of viewing an article:
  • A standard, HTML view with this kind of URLs: http://localhost:8080/blog/article/view/1


  • An XML or JSON output, with this kind of URLs: http://localhost:8080/blog/article/view:xml/1 or blog/article/view:json/1


    <article title="First article" id="1">
    <published>true</published>
    <content>Content for the first article</content>
    <creationDate>2009-03-24 16:16:31.865 CET</creationDate>
    </article>

    And:

    {"article": {
    "@published": "true",
    "@title": "First article",
    "@id": "1",
    "content": "Content for the first article",
    "creationDate": "2009-03-24 16:16:31.865 CET"
    }}

The "view article" page


The first step here is to build a page that will show an article based on it's id.
We want that an URL like: http://localhost:8080/blog/article/view/1
Return the page:


For that, we add a ViewArticle class and ViewArticle template in the package "org.example.blog.pages.article".

The template is trivial, we simply display meaningful information of a backed article object:
org.example.blog.pages.article.ViewArticle.tml
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">

<h2>${article.title}</h2>
<h4>by ${authorname}, on ${article.creationDate}</h4>

<div>
${article.content}
</div>

</t:layout>


And that's all.

The Scala class is not much more complicated:
org.example.blog.pages.article.ViewArticle.scala
class ViewArticle {

@Inject
var articleDao : ReadDao[Article,String] = _

@Inject
var conf : BlogConfiguration = _

@Property
var article : Article = _

var id : String = _

def onActivate(id:String) { this.id = id }

def getAuthorName() = this.conf.getAuthor.getLogin

def setupRender {
this.article = this.get(id)
}

private def get(id:String) =
if(null == id ) NoneArticle
else this.articleDao.get(id).getOrElse(NoneArticle)
}


We see that I use two services, the Article DAO that allows to retrieve article from their store, and the blog's configuration that is used to display to author name: the article object it accessible from the template because of the @Property annotation (this T5 annotation is in fact the same as the Scala @BeanProperty one, it generates the pair of getter/setter for the attribute), getAuthorName() is called in the template by ${authorname}.

The logic of the page is almost inexistent: when the page is activated (what means an HTTP request is handled by that page), we store the "id" parameter (in our example, it's "1") for when the page is rendered. There is more about Tapestry 5 page activation and parameters here.
When the page is rendered, we ask the article DAO to retrieve the article with the given ID. If none is found, or if the given id was null (no parameter was found in the URL), we used a special "NoneArticle" object for the rendering. This object is a placeholder that stands for "null" for article type, and is defined as follow:

org.example.blog.data.Article.scala
object NoneArticle extends Article(None) {
creationDate = new Date(0L);
title = "No such article"
}


And with that, the HTML view is over.

Marshalling article


Here we deal with the transformation of article object into XML or Json, and how we make a service for that task available in our blog.

XStream, the swiss-knife of serialization



Well, in fact I will confess something: I did almost nothing to make the other two output renderings works, it's basically only the mapping of an XStream output to some Tapestry 5 event handlers.

If you don't already know XStream, stop reading here, and just go and look for its 2 minutes tutorial - it's actually a two minutes thing to read and shows how powerful Xstream is.

For the lazy clicker, there's a summary: XStream is an amazing XML and JSON marshalling and unmarshalling library for Java. It's fast, it's powerful, it's broadly used with all the good that that carries, and its learning curve is counted in seconds.

Basically, without any configuration, you can use it like that:

Person joe = new Person("Joe", "Walnes");
XStream xstream = new XStream();
String xml = xstream.toXML(joe);


And it will output something like that:
<org.foo.domain.person>
<firstname>Joe</firstname>
<lastname>Walnes</lastname>
</org.foo.domain.person>


With one more config line for the Xstream object:
xstream.alias("person", Person.class);


You get a prettier output:
<person>
<firstname>Joe</firstname>
<lastname>Walnes</lastname>
</person>


Doesn't it look simple? And if I say that it also works for JSON output, and also in the reverse way (from JSON/XML to Object), that output customization is almost infinite, you're likely to fall in love with this little, cute library.

Marshalling services for our blog



In the pages, we only need a simple service that transform object to a String representation. As always, we define an interface for that service:

org.example.blog.services.Marshaller.scala
trait Marshaller { 
def to(o:Any) : String
}


Next, we are going to build two implementation for that service, one for XML output and one other for the JSON output. The two of them will extend a common Xstream based Marshaller:

org.example.blog.services.impl.XstreamMarshaller.scala
class XstreamMarshaller(val xstream:XStream) extends Marshaller  {
override def to(o : Any) = this.xstream.toXML(o)
}

class XmlXstreamMarshaller() extends XstreamMarshaller(new XStream(new DomDriver()))

class JsonXstreamMarshaller() extends XstreamMarshaller(new XStream(new JsonHierarchicalStreamDriver()))


There is only one step remaining to enable the service in our blog: binding interface and implementation in the Tapestry module definition:
org.example.blog.services.AppModule.scala
object AppModule {
//bind is a conventionnal method name for binding interface and implementation when no special configuration is needed
def bind(binder : ServiceBinder) {
binder.bind[Marshaller](classOf[Marshaller],classOf[impl.XmlXstreamMarshaller]) withId "xmlMarshaller"
binder.bind[Marshaller](classOf[Marshaller],classOf[impl.JsonXstreamMarshaller]) withId "jsonMarshaller"
}


Note that we assign a name (an Id) to each marshaller. That's because we now have two implementations for a given interface declared in the IoC registry. Tapestry 5 will not be able to know automatically what we really want to use when we will require a service injection for that interface, and so we will have to precise the name of the actual service we really want.

An now, let's use these marshallers to render JSONinfied and XMLified articles.

Binding URL to marshaller output


This second part aims to produce the correct output based on the URL.

T5 event handling system


Again, I did nothing special to bind URL and XML/JSON output since I used the standard Tapestry 5 event system: Url with ":" after a component or page name are interpreted as "component:eventname/eventparams".

For example, http://localhost:8080/blog/article/view:xml/1 means: throws the "xml" event on page "article", with the "1" parameter.

For our use case, the only thing to do is to handle "Xml" and "Json" events in the Scala back end of the "Article/View" page. Like most of the time in Tapestry 5, these event handlers are based on convention, and here the convention is to use "onEVENT" for the method name, where "EVENT" is the name of the event to handle.

The modified ViewArticle class looks like:

org.example.blog.pages.article.ViewArticle.scala
class ViewArticle {

@Inject
var articleDao : ReadDao[Article,String] = _

@Inject
var conf : BlogConfiguration = _

@Inject @Service("xmlMarshaller")
var xmlMarshaller : Marshaller = _

@Inject @Service("jsonMarshaller")
var jsonMarshaller : Marshaller = _

@Property
var article : Article = _

var id : String = _

def onActivate(id:String) { this.id = id }

def getAuthorName() = this.conf.getAuthor.getLogin

def setupRender {
this.article = this.get(id)
}

def onXml = new TextStreamResponse("text/xml",this.xmlMarshaller.to(NoneArticle))
def onJson = new TextStreamResponse("text/plain",this.jsonMarshaller.to(NoneArticle))

def onXml(id:String) = {
new TextStreamResponse("text/xml",this.xmlMarshaller.to(this.get(id)))
}

def onJson(id:String) = {
new TextStreamResponse("text/plain",this.jsonMarshaller.to(this.get(id)))
}

private def get(id:String) =
if(null == id ) NoneArticle
else this.articleDao.get(id).getOrElse(NoneArticle)
}


Compared to the first version, we just injected two marshallers and used them in event handlers. I set-up specific handlers for the case when no parameter is given in the URL.

Note that we solved the injection ambiguity problem on "Marshaller" injection by naming the service we really want to be injected thanks to the "@Service("name")" annotation after the @Inject one.

The only other remarkable thing is the TextStreamResponse object, that allows to return arbitrary text output for rendering. It's an implementation of the StreamResponse interface, that allows to return arbitrary content - yes, returning a PDF for a given URL is a matter of returning the correct StreamResponse in a handler method. Simple, no ?

With that, we have a first promising result.

http://localhost:8080/blog/article/view:xml/1 leads to:

<org.example.blog.data.Article>
<published>true</published>
<comments class="scala.Nil$"/>
<content>Content for the first article</content>
<title>First article</title>
<creationDate>2009-03-25 14:19:41.940 CET</creationDate>
<id class="scala.Some">
<x class="string">1</x>
</id>
</org.example.blog.data.Article>


And http://localhost:8080/blog/article/view:json/1 leads to:

{"org.example.blog.data.Article": {
"published": true,
"comments": {
"@class": "scala.Nil$"
},
"content": "Content for the first article",
"title": "First article",
"creationDate": "2009-03-25 14:19:41.940 CET",
"id": {
"@class": "scala.Some",
"x": {
"@class": "string",
"$": "1"
}
}
}}


And non existing article or no parameter, like in http://localhost:8080/blog/article/view:xml/foobar leads to:

<org.example.blog.data.NoneArticle_->
<published>false</published>
<comments class="scala.Nil$"/>
<content/>
<title>No such article</title>
<creationDate>1970-01-01 01:00:00.0 CET</creationDate>
<id class="scala.None$"/>
</org.example.blog.data.NoneArticle_->


That's not too bad for a couple of line of code, but output should be nicer.

Configuring output


Actually, we don't want to show the qualified class name in the output, nor we need the comments here, and it will be cool to have "id" and "title" as attributes of the "article" node.

For that, we will customized Xstream marshaller.

The first step is to change the marshaller implementation registration in AppModule to a more configurable one:

org.example.blog.services.AppModule.scala
  def bind(binder : ServiceBinder) {
//remove marshaller from here
}

def buildXmlMarshaller(): Marshaller = {
val m = new XmlXstreamMarshaller
ConfigureArticle4Xstream.configure(m.xstream)
m
}

def buildJsonMarshaller(): Marshaller = {
val m = new JsonXstreamMarshaller
ConfigureArticle4Xstream.configure(m.xstream)
m
}


These new definition also build two marshallers, named "XmlMarshaller" and "JsonMarshaller", but with that way we are able to configure the Xstream instance of each implementation.
Since our case is really simple, the two configurations are the same, and look like that:

org.example.blog.services.impl.XstreamMarshaller.scala
object ConfigureArticle4Xstream {
def configure(x:XStream) {
import org.example.blog.data.{Article,NoneArticle}
//output "article" in place of org.example.blog.data.Article
x.alias("article",classOf[Article])

//title and id will be output as attributes of article...
x.useAttributeFor(classOf[Article],"title")
x.useAttributeFor(classOf[Article],"id")

//...and since Id is not a simple string, we have to provide a converter to make it works
x.registerLocalConverter(classOf[Article],"id",new ArticleIdConverter())

//we don't want to display comments
x.omitField(classOf[Article],"comments")

//special config for NoneArticle
x.alias("article",NoneArticle.getClass)
}
}


Each line provides the commented explanation.
The id converter simply output the id if available, or "none":
org.example.blog.services.impl.XstreamMarshaller.scala
class ArticleIdConverter extends SingleValueConverter {
def fromString(s:String) = if("none" == s) None else Some(s)
def toString(a:Object) = a match {
case None => "none"
case Some(x) => x.toString
case _ => error("Not supported type: " + a.getClass.getName)
}
def canConvert(c:Class[_]) = {
if(c == classOf[scala.Option[_]]) true
else false
}
}


With this configuration, outputs are much nicer:

http://localhost:8080/blog/article/view:xml/1 leads to:

<article title="First article" id="1">
<published>true</published>
<content>Content for the first article</content>
<creationDate>2009-03-25 15:09:20.476 CET</creationDate>
</article>


And http://localhost:8080/blog/article/view:json/1 leads to:

{"article": {
"@title": "First article",
"@id": "1",
"published": true,
"content": "Content for the first article",
"creationDate": "2009-03-25 15:09:20.476 CET"
}}


And non existing article or no parameter, like in http://localhost:8080/blog/article/view:xml/foobar leads to:

{"article": {
"@title": "No such article",
"@id": "none",
"published": false,
"content": "",
"creationDate": "1970-01-01 01:00:00.0 CET"
}}


Now, we can say too our chief that he can go and look for investor, our blog has the REST (well, almost ;).
And that's all for today !

Next time


We reach a rather satisfying result with really few lines of code, but I have to say that our implementation is bad.

The main reason for that is that there is a bunch of coupling and code duplication at the moment:
- from the page designer point of vue, it's crappy to have to inject several marshallers, and choose the correct one for each event;
- moreover, addind a new output (for example, plain text) would mean modifying *all* pages that use marshallers;
- with the actual configuration, each time that we want to customize an output for a given object, we have to actually modify the code of XstreamMarshaller core implementation, or in the core module definition !

So the next time, we will see how we can improve these points. And again, it will be easy, thanks to Tapestry IoC provided features.

As always, the real full code is available at Github at this tag :
http://github.com/fanf/scala-t5-blog/tree/article4_20090325

Thursday, February 12, 2009

Tapestry 5 - Scala : start of delusion

I didn't write anything for a long time on my Tapestry 5 / Scala experiment. Things are starting to go wrong.

From one part, I found what may have been a bug in Scala ( https://lampsvn.epfl.ch/trac/scala/ticket/1695 ) and that delayed me lot in the use of T5 IoC binder.

For the other part, T5 IoC does not seems to support parameterized types at more than one level, and so I can't use parameterized class in contributions.

For example, I can't contribute Tuple, or (and that's quite a big limitation) class because in Scala, you can't use the unparameterized "Class" argument, it must be "Class[Something]", and here lies the hell of generics...

So for now, I'm beginning to thing that T5 and Scala don't feet so well together, or at least that I have some nuts to scratch and some "best practices" to find before going further in the test.


EDIT: as proposed by Ricky Clarkson, for the class problem, just use Class[_]

Monday, January 19, 2009

Tapestry 5 - Scala : a layout and basic CRUD for the blog

After setting up a hello world application in Scala and Tapestry, and testing how Tapestry IoC / property access works in this environment, it's time to actually do something.

So, this article will explain how I set up the basic fonctionnalities for our blog:

  • build a "Layout" component, so that the development will hurt our eyes a little less;
  • add an Article object, and the standards pages to use it:
    • the "home page" page will show all the published articles;
    • a "create" page will allow to add new article;
    • a "view" page will show only one article and its comments
    • a "manage" page will allows to view a list of all the article with some actions, like publish, delete, edit.




Setting up a layout


Using "layout" is a common pattern to apply the same design for all the site. In tapestry, no need to use an external template engine like sitemesh, you can simply build a "layout component" that you will use on all page that have to be decorated.

This pattern is so common that Tapestry 5 has a documentation page on how to do it with T5, so you just have to follow it : the harder part is to design the layout, or if, like me, you are a dumb in design, find a cool CSS/HTML template on internet (thank you Free CSS Templates for the one I chose.

As it's the first time that we will build a Tapestry 5 component, I will details a little what we need. In Tapestry 5, component are simple POJOs. They go under the ${t5-root}/components package, and are coupled with a Template (.tml) file.

  • create the ${root}/components/Layout.tml and ${root}/components/Layout.java couple of files for our component;
  • add the needed CSS/images in the webapp directory.


The code for the layout is here:

@IncludeStylesheet(Array("context:css/red/style.css"))
class Layout {

@Inject @Property
var conf : BlogConfiguration = _

}


The interesting part is the @IncludeStyleSheet annotation, that say to T5 ti add it the given CSS into the header of the pages where this component is used, what is handy for a layout component.

We can see that in Scala, we can't simply use the
@IncludeStylesheet({"context:css/red/style.css"})
notation, and have to explicitly build a new array - hopefully, it's trivial in Scala.

The template part of the component is bigger, since it's there that goes all the common HTML declaration.


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>${conf.blogTitle}</title>
<meta http-equiv="Content-Language" content="English" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>

<div id="wrap">

<div id="header">
<h1><a t:type="pagelink" t:page="index">${conf.blogTitle}</a></h1>
<h2>${conf.blogDescription}</h2>
</div>

<div id="menu">
<ul>
<li><a t:type="pagelink" t:page="index">Home</a></li>
[...]
</ul>
</div>

<div id="content">
<div class="left">

<t:body />

</div>

<div class="right">

<h2>Archives</h2>
<ul>
[... here will go the archives ...]
</ul>

<h2>Tags :</h2>
<ul>
[... here will go tags link ...]
</ul>

</div>

<div style="clear: both;"> </div>

</div>

<div id="bottom"> </div>
<div id="footer">
Designed by <a href="http://www.free-css-templates.com/">Free CSS Templates</a>
</div>
</div>
</body>
</html>


The important part it the <t:body /> tag, that says "here will go what will be find between the <t:layout> and </t:layout> tags. So, in the page where we want to use this layout, we will use template code like:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h2>New article</h2>
[....]

</t:layout>


And now, our blog is prettier:




Note: I equally use <htmlTag t:type="T5_component"...> and <t:="T5_component"> notation to include T5 component in templates. The first one is better for designer, as template preview is correct, the second one is a little shorter. They are equivalent, and I should homogeneously use one or the other...


Article's CRUD


All this blog application is a pretext for that very part, so let's take a look at it. Reminder: "CRUD" stands for "Create, Retrieve, Update, Delete", and is a well know acronym that is used for simple application, with very little domain logic, and which are almost a front-end for datas, persisted in some layer (MySQL comes to mind, but CouchDB is a better buzz word today ;)

To sum-up, we have Article objects, a DAO to persist them, and a couple of pages to enable human I/O.

The "Article" domain object


An article is an object with an id, a title, a content, a list of comments and a creation date.

The "id" is unique identifier for an article among all the blog's article. Most of the time, it will be used to identity a particular article in the persistence layer or in URL.
I take the choice to let the persistance layer assigned the id, because we really are in a CRUD application, and no business logic can provide a realiable unique identifier. That means that a not yet persisted article has no id. In the same time, I want to enforce the fact that Id are imutable datas, that given a Article object, the id won't ever change.

In java, this kind of requirement can't easily be done in an other way than usgin "null". In Scala as in many other language, we have an Option type that has two kind of value:
  • the "None" value, which means "there is no data here";
  • the "Some[TypeOfData]" value, which means "there is a data here, and that is it's value".

I use this type for the article "Id", and the "Id" is a constructor param value of article, so that:
  • article id is immutable: an living object can't have it's id change;
  • an article with "None" id has never been saved ;

  • the persistence layer will assign IDs and return article with there definitive ID


Given that, that's the code for the Article object:

org.example.blog.data.Article.scala

class Article(val id:Option[String]) {
/*
* "none" is not allowed for article ID
*/
assert( id.forall( (s:String)=> !(s.toLowerCase == "none" )))

@BeanProperty var creationDate = new Date()

@BeanProperty var title = ""

@BeanProperty var content = ""

@BeanProperty var comments = List[Comment]()


@BeanProperty var published = false

def getId = id

/*
* String translation of the ID. Of course, it
* requires that id can't be the "None" String,
* and that's why we add the "assert" requirement
*/
def getDisplayId = this.id.getOrElse("None")
}

object Article {

/*
* Create a copy Article from a source one,
* setting the ID to a new value.
*/
def apply(id:String, source:Article) : Article = {
val a = new Article(Some(id))
a.title = source.title
a.content = source.content
a.comments = source.comments
a.creationDate = source.creationDate
a.published = source.published
a
}
}


Apart from the Article constructor and "Id" type explained before, there is three other notable things in this class:
  • A getDisplayId that is a simple method to give the article Id to other layer, especially the presentation layer. I chose to map "Some(id)" to the given id string, and the "None" type to the "None" string, what implies that I should never accept the "None" id;
  • since I can't accept "None" string as id, I added a requirement through an assertion (and no, performances don't matter, it's a tool blog application ;)
  • finally, there is a strange "object" definition after my class with the same name as it, that needs its own paragraph


"objects" are the Scala way to define static methods and contents, and it's why I used the "object" key word in the AppModule.
When paired with a class, it is called a companion object for the class. They have some special rights, in particular regarding visibilyity, that goes beyond this article.

I used this one to create a "copy" method that allows to create an article with a given id, different from the source - remember, I want to have immutable ids for an Article for all is life in memory.

The question is "why do I called this method apply, what is a name that carry almost zero information ?". Simply because it's a Scala magic word, that can be zapped when used ! Just writing "Article(id, article)" will call Article.apply(...). If you remember the "Array" in the @IncludeStyleSheet for the layout... That was the same principle. Array is the companion object of the Array class, and "Array("...","...")" secretly call the "apply" method of Array that take a list of String as parameter.

And we are done with Articles.

DAO



A DAOs ("Data Access Objects") are objects responsible for accessing and persisting (or delegate persistence of) other objects. They are the heart of CRUD application.

The first paragraph explain our generic DAO API, the second will show a naive, in memory implementation of the generic DAO for article, and a third one will explain how to use it with Tapestry IoC.

Generic DAO API


We will define DAO by their API, the set of method exposed by these service objects. In Java, API means "Interfaces", in Scala they means "Traits". In our example, they are the same things. We will define a real implementation


org.example.blog.services.DAO.scala

/**
* A read-only dao
* @param T is the type of entities handled by this DAO
* @param K is the type of the key that is used to identify entities.
*/
trait ReadDao[T, K <: Serializable] {

/*
* Retrieve an entity from its key. If no
* entities are known for the given key, None is returned.
* @param K the unique id of the entity to fetch
* @return None if no entity match that key,
* Some[entity] else
*/
def get(id:K) : Option[T]

/**
* Retrieve all the entities known by that DAO.
* Be carefull, that may be a huge number.
* @return the list of all entities for that DAO
*/
def getAll() : List[T]

/**
* Find all the entities that match the given requirement
* @param T => Boolean : the function to apply to find if
* an entity should be included in the returned list.
* (on a "true" return) or excluded (on a "false" return)
* @return the list of enetities matching the filter.
*/
def find(filter: T => Boolean) : List[T]
}

/**
* A write-only dao
* @param T is the type of entities handled by this DAO
* @param K is the type of the key that is used to identify entities.
*/
trait WriteDao[T, K <: Serializable] {

/**
* Persist the given entity: create a new one if
* the entity wasn't know in that DAO (entity
* key was None), or update an existing entity
* (it's key was Some[K]).
* @param T the entity to persist
* @return the key of the persisted entity if the process
* succeded, or None the the peristence failed.
*/
def save(entity:T) : Option[K]

/**
* Delete the article matching this id.
* If no article match this id, does nothing.
* Return true is the deletion is successful
*/
def delete(id:K) : Boolean

}

/**
* A read-write DAO, that combine read and write DAO traits.
*/
trait ReadWriteDao[T, K <: Serializable] extends ReadDao[T,K] with WriteDao[T,K]


Comments speak for themselves here, and "Option" is well-known now.
The only surprises are the "<:" which means that the K type has to be Serializable, and more notably for a Java guy, the "find" method: it takes a method as parameter !
The signature of the filter method is "I take an entity of type T as parameter, and return a Boolean".

For example, if we take an Article DAO, we may use this to find all the article whose title begin with "The" like that:


val articles = dao.find( (a:Article) => a.title.startsWith("The") == true )


That's all. It means "the method find takes as parameter a method that take an article as parameter, and return the result of the evaluation of "a.title.startsWith("The") == true".

It can even be simpler, because Scala allows to use "_" as a place-holder for parameter when there is no ambiguities, like here (in our case, find can't take anything but a method which has the "Article => Boolean" signature):


val articles = dao.find( _.title.startsWith("The") == true )


If you don't see why "closure" are so useful, look how simple that declaration is compared to the burden to declare a filter interface and used it, even with anonymous class in place of real instances...

In memory Article DAO



Now that we have our generic DAO API, we are ready to implement a version for Articles. This first DAO will be a really simple one, where articles are persisted in memory (in a Map).

org.example.blog.services.impl.dao.InmemoryDao.scala

import org.example.blog.data.Article
import scala.collection.mutable

import org.example.blog.services.ReadWriteDao

/**
* A simple, naive implementation of the Article DAO.
* In particular, this implementation is
* NOT AT ALL THREADSAFE
*/
class InmemoryArticleDao extends ReadWriteDao[Article, String] {

private val memory : mutable.Map[String, Article] = new mutable.HashMap()
private var id = 0
private def newId = { id = id + 1 ; id }

def get(id:String) = this.memory.get(id)

def getAll() = this.memory.values.toList

def find(filter: Article => Boolean) =
(for {
a <- this.memory.values
if(filter(a))
} yield a).toList

def save(article:Article) = {
val a = article.id match {
case None =>
val i = this.newId
Article(i.toString,article)
case Some(id) => article
}
assert(a.id.isDefined)
this.memory.put(a.id.get,a)
//check if article is in map for the id, return id if OK
this.memory.get(a.id.get).map( _.id.get)
}

def delete(id:String) = !( (this.memory - id).isDefinedAt(id) )
}


We can see that the "extends ReadWriteDao[Article, String]" is really like Java with generics.

The implementation is a basic mapping between our DAO API and the Map used as a back-end. Article Ids are generated by an incrementing number, but there's no lock against thread concurrency. Say that for now, it's really a tool example, and all in all, our blog can have only one author (reminds the configuration object of the previous post).

The save method is the most complex, because we have to process the update and the create case, based on the fact that id is None or Some(value).
We also see the use of the Article companion object "apply", that allows to copy the given article to a new one, but with it's freshly created id.

The filter method the Scala for comprehension loop that automatically concatenates yielded elements, but it could have been done with an even more imperative "for" loop too:

def find(filter: Article => Boolean) = {
var articles = List[Article]()
for(a <- this.memory.values) {
if(filter(a)) articles = a :: articles
}
articles
}


I believe that the first method is better, because the two are almost as inefficient, and the latter use a mutable variable.

That's all for the implementation of our DAO !

Enable Article DAO service thanks to T5 IoC



Now that we have a DAO API, and a DAO implementation for Article, we want to let our application know that when we use a DAO on articles, what we really want is to use the "In Memory Article DAO".

As for the Configuration service on the previous post, we just have to add a "build" method into the AppModule object to bind the ReadWrite[String,Article] DAO to its implementation:


def buildArticleDao = new InmemoryArticleDao()


Hum. Yeah, there is nothing to do but that, but as we are Professional, we can't let our bosses think that our job is so simple, so let's add some boilerplate to make the code seems harder, more complex:


def buildArticleDao = {
val a1 = new Article(None)
a1.title = "First article"
a1.content = "Content for the first article"
a1.published = true

val a2 = new Article(None)
a2.title = "Second article"
a2.content = "Content for the second article"

val m = new InmemoryArticleDao()
m.save(a1)
m.save(a2)
m
}


OK, that's better ! Actually, I just initialized the DAO with two articles (one published, not the other one), but it's far more impressive like that ;)

Putting the parts together : the presentation layer



Now that we (finally !) have all the services to create and retrieve articles set-up, we can switch to the only part in which customers have some interests, the presentation layer (that's because code screenshots look bad on powerpoint).

The Index page


The first page that will use our new services is the home page. On this page, we just want to display all published articles.

For that, we will use the Loop component:

org.example.blog.pages.Index.tml

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">

<div t:type="loop" t:source="articles" t:value="article" class="article">
<h2><a t:type="pagelink" t:page="article/view" t:context="article.displayid">
${article.title}
</a></h2>

<div>${article.content}</div>
</div>

</t:layout>


We see in this template that we link to the "article/view" page, in which we will display the article with its comments. But as it's for an other day, for now we just create a org.example.blog.page.article.ViewArticle.{scala,tml} couple so that Tapestry 5 don't stop on a broken link (it validate that all lins are correct at start time), and we will take care of them the next time.

We also see that we need a "source" from where articles are taken, and a "value" to hold the current article of the loop. Of course, these objects are provided by the Scala part of the couple:

org.example.blog.pages.Index.scala

class Index {

@Inject
var readArticleDao : ReadDao[Article, String] = _

@Cached
def getArticles = readArticleDao.find( _.published == true ).toArray

@Property
var article : Article = _

}


The code stands for itself : we inject a read-only DAO for article - we don't need more, and even if actually we get the in memory read write implementation, this code only care for the read part ; we have a "getArticles" method that retrieve all publish articles from the DAO (remember: closures are great), and we have an Article "@Property" annotated to receive the current article from the loop.

As we have initialized the DAO with two articles (one published among them), as soon as we start the application, we can see the first, published article on the home page:



Isn't that great ?

Create new articles



Now that we can see our article, we may want to add some new ones - well, it's aim to be a blog, and what the point if I can't talk about how cute is my ickle hamster ?

For that, let's build the "create article" page.
Let's begin with the template. In this first version, I won't rely on the magc BeanEditForm, and we need to be able to set a title, a content, and choose to publish or not the article:


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<h2>Create a new article</h2>

<t:form t:id="NewArticleForm" class="new-article">

<p>
<t:label for="publish" />&nbsp;<t:checkbox t:id="publish" t:value="article.published" />
</p>

<p>
<t:label for="title"/><br/>
<t:textfield class="large" t:id="title" value="article.title" validate="required"/>
</p>

<p>
<t:label for="content" /><br/>
<t:textarea class="large" t:id="content" value="article.content" />
</p>
<t:submit />
</t:form>
</t:layout>


This template is a little boring, but it's the first time we see a constructed form and it's different input. The title is required, and the form will be refused as long as it is no provided. There is other predifined validator, like min/man, regexp... And... well, there's nothing more to say : all input has a value parameter that is the link to the server-side java^W Scala object that will handle it, and there is a submit button. And that's all.

That looks like that:



Ok, so how do we handle that on the server side ?

org.example.blog.page.article.CreateArticle.scala

import org.apache.tapestry5.annotations.{Property,Persist,InjectPage}
import org.apache.tapestry5.ioc.annotations.Inject

import org.example.blog.data.Article
import org.example.blog.services.WriteDao

import java.text.DateFormat

class CreateArticle {

@Inject
var articleDao : WriteDao[Article,String] = _

@Persist @Property
var article : Article = _


@InjectPage
var redirectPage : ManageArticle = _

def setupRender {
if(null == this.article) {
this.article = new Article(None)
}
}

def onSuccessFromNewArticleForm = {
articleDao.save(article) match {
case None => error("Dao error ! Please retry.")
case _ => this.article = null ; redirectPage
}
}
}


This time we need the "Write" part of the DAO (and a again, no need for the other, so I injected the minimal aspect), and an Article that will back the the new article.

The article is annotated @Persist, so that it will be stored in session. We need that because the form has to be validated, and perhaps show again: we don't want to loose what was written in this case.

The setupRender method is a conventional name that matches a component rendering phase. We will take advantage of this method to initialized a new Article, if needed.

The onSuccessFromNewArticleForm is again a conventional method name to handle a component event. On that case, the event is "success", and we await it from the "articleForm" component... which is our form's "t:id". So, you get it, when the form succeed, we go into that method, in which we try to save the Article. If the DAO failed in its job, we raise an error, and else we redirect into the ManageArticle page. There is a lot of way to redirect to a page in T5: you can use URL, page class, the string page name, or the InjectPage annotated page. This method is handy when you need to init some value in the page before redirecting to it. Well, here it's just because it's cool ;).

And now, we are redirected to the "manate" page.

Manage all articles



The goal of this page is to give the author the possibility to view the list of all existing articles. Moreover, we want to be able to go there detail page, to publish or unpublish, edit and delete them.

In this case, the perfect component is the grid component.

This component allows to display a list of beans, one column by property, and allows to add, remove, reorder columns.

The template looks like that:


<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">

<h2>List, modify, publish article</h2>

<t:grid source="articles" row="article"
reorder="title" exclude="content" add="comments,publish,edit,delete">

<t:parameter name="titleCell">
<t:pagelink t:page="article/view" t:context="article.displayid">
${article.title}
</t:pagelink>
</t:parameter>

<t:parameter name="publishCell">
<t:actionlink t:id="changePublication" t:context="article.displayid">
${changePublication}
</t:actionlink>
</t:parameter>

<t:parameter name="commentsCell">
${article.comments.size()}
</t:parameter>

<t:parameter name="editCell">
<t:pagelink t:page="article/edit" t:context="article.displayid">
edit
</t:pagelink>
</t:parameter>

<t:parameter name="deleteCell">
<t:actionlink t:id="delete" t:context="article.displayid">
delete
</t:actionlink>
</t:parameter>

</t:grid>
</t:layout>


What looks in real like:



There is some interesting things in this template:
  • we have a source of bean, and the current Article is handle in the "row" parameter;
  • the "t:parameter" is special invocation that allows to bind a block of template code to component parameter. In our case, each t:parameter is used to replace the content of a column cell;
  • the pagelink component has already been seen several times, but here can see how easy it is to map page to URL: it's simply the list of directories from the "pages" package to the page object. We also see how we can pass a some context (variable) into the targeted page;
  • lastly, the "t:actionlink" is a component that allows to fire an event on the server side;


So, how we handle all that on the server side ?

org.example.blog.pages.article.ManageArticle.scala

class ManageArticle {

@Inject
var rwDao : ReadWriteDao[Article, String] = _

@Property
var article : Article = _

def getArticles = this.rwDao.getAll.sort( _.creationDate.getTime > _.creationDate.getTime ).toArray

def getChangePublication = if(article.published) "Un-published" else "Published"

def onActionFromChangePublication(id:String) {
val a = this.rwDao.get(id).getOrElse(error("No such article, id: " + id) )
a.published = !a.published
this.rwDao.save(a)
}

def onActionFromDelete(id:String) {
if(!this.rwDao.delete(id)) {
error("Can not delete this article")
}
}
}


The code is fairly simple and clear:

  • we need the ReadWriteDao, so we inject it;
  • we need an article to keep the current row, so we add an @Property annotated Article;
  • the source of all bean is provided by the "getArticles()" method, that simply retrieve all the articles from DAO (and sort them by date);
  • the "getChangePublication()" return the good text given the status of the article;
  • the "onActionFromChangePublication" method handle the event from the actionLink with "t:id" changePublication. The method await a parameter which is given by the context of action link (and is the article id). We react at this event by changing the status of the matching article, and save it back into the DAO;
  • finally, the "onActionFromDelete" handle the event from the "delete" link, and react to it by deleting the article from the DAO.


Note that all event handler that does not return anything redirect to the calling page.

Final words


Conclusion



And that's done ! The goal perimeter is reached, we can see, add, manage articles in a not too ugly blog.

Now, a lot of things remained:
  • Articles are not really persisted, and are lost on server shutdown. What abot saving them into a database, or even better into a more "text oriented" storage, has XML files, or something like a Java Content Repository (or a CouchDB ?)
  • Where are the comments ?
  • And the article details ? And all the formating of articles is lost in rendering ! That's awful ! Perhaps we need a smarter rendering component... and so we may used it at several place around the site (Index, article details...)
  • And what about a better text editor, that allows some kind of rich UI ? Personally, I really like showdown editor
  • And tags, hu ? We are building the tomorrow blog plateform for web 3.0 and don't even have tags ? That can't be serious !
  • oups, somebody just pointed that for now, there is no way to protect the manager area from the simple user... No authentication, no authorizations...
  • I also said that I will attempt to use easy-ant as build tool, in place of maven
  • and the list is almost infinite !


So, there is still some room to a little more experiment with T5 and Scala !

Source code



As always, the source code is available on the GitHub repository of the project.

To download and test the code for that articles, simply executes these commands:

% git clone git://github.com/fanf/scala-t5-blog.git
% cd scala-t5-blog
% git checkout -b test article3_20090119
% mvn jetty:run


Enjoy, and see you next time !

Sunday, January 11, 2009

T5 - Scala : first injection and property access

The last time, we set-up the Tapestry 5 quickstart application to run with Scala in place of Java. Now, it's time to add our first service and data, to see what are the common idiom we will have to follow to make them work together.

Target scope of this iteration



We want to add only two features:

  • Two domain objects:


    • the author for our blog,

    • a configuration object, which will be a container for blog's parameters (title, author, etc)


  • a service to access this configuration, injected where needed



After the domain object will be added, I will mmodify the Index page to use them.

User, configuration and service, first attempt



User and configuration objects


So, we want an user with a non-modifiable login, a name/surname, and an email. For now, the configuration will only have the main author (an user), and a title for the blog.
They will go in the same file as they are both related to configuration data:

org.example.blog.data.BlogConfiguration

package org.example.blog.data

class BlogConfiguration(val author : User) {
var blogTitle = "%s's blog".format(author.login)
}

class User(val login : String) {
var commonname = ""
var surname = ""
var email = ""
}


And that's it. For the user, commonname, surname, and email are "var", so we will be able to get them with user.email, and set them with user.email = "newValue".
We can note that the type of this properties is inferred by Scala to String. Remember, Scala is statically typed, but that doesn't imply

login (for user) and author (for configuration) are constructor's parameter. As they are tagged "val", they will be read-only, and viewable from other class.

Configuration service



Now, we have to build a service for configuration so that we will be able to inject it where we need it.

Thank's to Tapestry 5 powerful and super-easy IoC feature, it's a matter of adding some lines in our application module:

org.example.blog.services.AppModule

def buildBlogConfiguration = {
val author = new User("jondoe")
author.commonname = "Jon"
author.surname = "Doe"
author.email = "jondoe@foo.bar"

new BlogConfiguration(author)
}


And it's all what we need. Now, let's use this service in our Index page to display some info about the user !


Index modification



Index.scala


The index page modification is really trivial. That's the code for the Scala part:
org.example.blog.pages.Index

class Index {

@Inject @Property
var conf : BlogConfiguration = _

}


We want to access our BlogConfiguration service, so we just declare a variable with the wanted type. The really important thing here is to initialized it with "_"

Without that (for example, if you initialized the var with null), you will get silly error about read-only variable not modifiable.

As we want T5 to inject the real BlogConfiguration service, we annotated the var with @Inject, and as we want to access it from the template and I'm lazy, we also used the @Property annotation, that save us from writing the getter for "conf".

Index.tml



The template part isn't much more thrilling. We want to use the blog title in the page's title, display information about the author, and even update them. That's the full template code:


<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Tapestry 5 - Scala blog</title>
</head>
<body>
<h1>${conf.blogTitle}</h1>

<p>Author info:</p>
<t:beandisplay t:object="conf.author" />
<p>Update author info:</p>
<t:beaneditform t:object="conf.author" />
</body>
</html>


"${}" is the T5 syntax to say "look into my class, take the blogTitle object in conf object".
"beandisplay" is a component that simply display all the properties of an object, and "beaneditform" is a magic component that fully build a create/update form for the given object.

And now, we test our blog... And that's not at all what we want.

T5 shows us a really insightful error message:


Clearly, Tapestry does not understand that our "conf" object has a "blogTitle" property.


User, configuration and service, second attempt



What happened is that T5 relies on Java Bean conventions to see what properties of an object are available. And Scala does not follow that convention with the auto-generated accessors. As we see in at the begining, Scala use the directly the property name as getter and setter.

So, we have to add traditional JavaBean getter/setter to our User and BlogConfiguration classes, or use the scala.reflect.BeanProperty where possible. This annotation is the Scala equivalent to T5's Property annotation and generate the getter/setter pair for us :

org.example.blog.data.BlogConfiguration

package org.example.blog.data

import scala.reflect.BeanProperty

class BlogConfiguration(val author:User) {

@BeanProperty
var blogTitle = "%s's blog".format(author.login)

def getAuthor = author
}


class User(val login : String) {
@BeanProperty
var commonname = ""

@BeanProperty
var surname = ""

@BeanProperty
var email = ""

def getLogin = login
}


Sadly, the updated classes look a little more bloated, but now, it works as expected.
If you go to "http://localhost:8080/blog/", you should see :



Of course, if you modify User info and click on "create/update", the user info are really updated in the BlogConfiguration.

And that's over for the time being.

Edit: oups, the code is available here: http://github.com/fanf/scala-t5-blog/tree/article2_20090111

To get the code and run it:

% git clone git://github.com/fanf/scala-t5-blog.git
% cd scala-t5-blog
% git checkout -b test article2_20090111
% mvn jetty:run

And go to http://localhost:8080/blog/

Summary



For now, Tapestry 5 and Scala work together, as soon as you know that "_" is to be used as init placeholder for Injected var, and that T5 use Java Bean convention for getter/setter.

The problem is that we don't really see what Scala brings in place of Java, so why switching ?

We can see it the other way: as everything works at least as well as with Java, I think I will continue this experiment a little bit more : I'm using eclipse with Scala plugin and OK, it's not as good as Java plugin, but it's ok, RunJettyRun has no problem to launch the application, T5 live class reloading works fine, etc.

Moreover, I suspect that as soon as I will come down a little in the application layer, some benefits will emerge - just think to real DAO that take a closure as filter, without having to deal with Java anonymous classes, generic mess, and all that stuff...

Wednesday, January 7, 2009

Tapestry 5 with Scala - Get the code !

A Scala / Tapestry 5 blog



In the next days/weeks, I will try to see a little more precisely how Tapestry 5 and Scala get together. I chose to start with a little blog application (how imaginative I am ;)

If your are interested to see how it works, I've set-up a github repository to keep a trace of the evolution.

So, the repository is here: http://github.com/fanf/scala-t5-blog/

Step one : an hello world application



The first step is to have the equivalent of the Tapestry 5 quickstart application, but with a Scala back end. That suppose:

  • to change Java class to scala one;

  • to add the correct dependencies and plugins (I used the scala-maven-plugin) for Maven, so that it will be able to compile Scala files.


I also renamed the standard directory structure for source classes from src/main/java to src/main/scala.

And that's it !

Test it yourself



So, if you want to see and test a Tapestry "hello world" application with Scala in place of Java, simply:

  • install Git if you don't have it already

  • get the source with the command:
    % git clone git://github.com/fanf/scala-t5-blog.git

  • Go to the scala-t5-blog directory, and switch to the tag for the "hello world" application with the command:
    % git checkout -b test helloworld-scala-t5 


  • You're done ! You can compile and run it with the standard "mvn jetty:run" command, or make a war to use it with your prefered web application with "mvn package"



That's it for this first step, next I will try to make a more beautiful, perhaps configurable skin for the blog.

Enjoy !

Sunday, January 4, 2009

Tapestry 5 with Scala

I just tested to use Scala for the code used in components for Tapestry 5. For now, all seems to work great !

I just created a default project with the quick-start archetype, changed Index.java and AppModule.java to Index.scala and AppModule.scala respectively, added some render-phase handler in Index, add a filter in AppModule, and all worked fine...

Expect one crucial thing: T5 requires that properties annotated @Persist are not initialized with a default value.

This is due to the fact that T5 components / pages are pooled, and so are instantiated prior to their used, and after use are cleaned and send back to the pool.
The problem is the clean stage: T5 as to know what is a "clean" value for the one managed by the framework - @Persist-ed one the first. That's hard to know for statefull object... as for example a collection of phone numbers, or some credential: T5 can not know effectively if a statefull object state changed, so it can only let it as it is, or nullify it. Nullify it expose you to NPE (because as a developer, you thought the initialization was done on class instantiation), and you don't want to see this kind of information to be badly cleaned and so, maybe leaked elsewhere, when the class in pool is used again.

OK, so it's a good thing that T5 does not let you fire on your foot, and throws an exception if you try to set a default value to such a managed property.
Except... that Scala requires that variable or values have a default value. So for now, I can't use persisted field for components, what is quite a limit :/

I'm going to continue the experiment, to if Scala and Tapestry 5 can be used together, or if I will have to use Scala only for deeper layers of my application (and well, perhaps at last, I will have to try Groovy for the upper layer...)


UPDATE: there seems to be some problem with the IOC, so that will be hard...
Symptoms are that the field is read-only, I will have to look more carefully at the difference between Scala var and Java properties...

EDIT: Thanks a lot too James Iry who pointed to me that the variable initialization should be done with "_" : that's the solution. So just use "Persist var name:String = _", and it works.

Thursday, October 9, 2008

Metting Tapestry father - Howard Lewis Ship in Paris

Thuesday the 7 october, I met Howard Lewis Ship, the father of Tapestry 5 Java web framework in Paris.
He was here for a 3 weeks trip in Europe, far from its home, Portland, USA.

There were Howard, his wife Suzan, and almost all my developers team, so 5 people from Linagora, all of them T5 addicts.

Evening part I : bar and T5

Imminent release of 5.0 final....

The evening began with a beer (or two...) in a bar near Linagora, and the discussion almost immediately started on Tapestry 5. Howard wanted to know how we came to T5, so I explained that I'm following its development since 5.0.1 and that I didn't chose at all by luck T5. Now, every developer in my team use T5, and quite like it.

We chatted about the iminent release of T5.0 final (YES !), the beauty of the framework thanks to the IoC framework, its scalabity, not only in performance, but in all thinkable meaning of the term : scalabitly in component from technical ones to business ones, scalabilty of the T5 user target from the business oriented people to the the technical nerd, etc.

We also discussed the missing points, around the documentation and some little things (what bring me to a ml post on the subject).

... and Tapestry 5.1 project...

The discussion followed on the 5.1 release, it's short, time-based and not functionnality-based release cycle, every 3 to max 5 month.

I think this is a really good new, because it was one of the most recurrent criticism against Tapestry : its really really long release cycle, along with the compatibility issue between them. Even if Howard came back several times on the last point (it's even explained in the T5 home page, at the bottom), upgrades would be even smoother if there is less time between them. Moreover, from a customer point of view, it's always best to have his product based on a stable release. I mean, a release tagged stable, because T5 is already one of the most stable soft I used, but the "alpha" or "beta" status is something quite frightening for a customer...

Afterwards, Howard gives us the possibility to argue for our "most wanted feature".
This is our wish-list, along with what Howard expects :

  • Portlets
That would be a great feature, because the component approach to the web really goes together with the portlet world. Howard thinks that it should be rather simple to make T5 portlet aware, because most of the needed abstractions are already here. And it's one of the things that should go in T5.1, so, we may be dreaming to cool webadmin interface with T5 in a portail in a near futur
  • Webflow
That's another point that came back several time in the mailing list, and that is already in the T5.1 scope. Howard seems to be in discussion with Keith Donald to integrate Spring Web 2 and Tapestry 5 together.
Plus, this is a really good feature to ship, especially directed to the IT managers. It's the kind of tool that make them think they can actually develop using boxes and arrows. I'm not completely sure I want Spring web flow integration into T5, but I definitely want a conversation persistence scope, along with a clean way to manage "wizards". So, if it has to come with Spring Web Flow integration, so be it...
  • skinning / themes
Howards was quite enthousiastic about the skinning and theming possibilities he envisions for 5.1. It would be a kind of sitemesh, for free, thanks to a T5 component.
  • OSGi
We chatted a little about OSGi. Several people on the ml already tried to integrate T5 and OSGi together, with more or less success. For now, I'm not quite sure of OSGi, I think it lacks a lot of all the plumbery that T5 so nicely hide to developpers... Perhaps Spring Dynamic Modules is the way to go, but for now, I think its use cases should remain in the proof of concept level.

That was the main points, but we also talked about other framework as Terracotta, and ideas as ""CRUD should be free" (I would just add "as long as it is a side module, and not in the core". That's not really a problem with T5 and it's pluggable architecture, but for example, all the CRUD stuff is of no use in InterLDAP).

...in an evolving Java ecosystem

The last discussions in the bar where less focused on Tapestry, and more on the Java ecosystem. Java 7 came in the discussion, and I was happy to discover that Howard seems to share most of my view on the subject. He agree that the JVM need optimizations (faster, more plugable, more versatile, etc), but that Java the language should not evolve anymore...
We talked about Scala and Groovy, his preference going to the second one (obviously, mine is in the first :), but he really likes the lazy evaluation possibilities that Scala bring.

Here, there is something that I don't understand... For a functional developer, all Tapestry architecture is built around functional concept, I see map/fold and pattern matching every where in Tapestry :)

Jean-Louis, one of my co-worker, talked about his "easy ant" project - take the best of Ant and Maven dependency management thanks to Ivy. A to-be-followed project.

Evening part II : "La Casita" restaurant

For the end of the evening, we moved to "La Casita", a french restaurant near Les Champs Elysées. It's a typical France South-West cooking restaurant, with Champagne, Foie Gras, snails, duck, wine, cheese... And to my great pleasure, Suzan and Howard seemed to enjoy it !
So much for our French a priori and cliché about American taste :)

There, we talked less about Tapestry, and more on general subjects. We chatted about the difference on open source in USA (more pragmatic, due to cost efficiency) and in France (more philosophical and political).
We also had an input around the use of IRC, and the complementarity this communication channel can have with the traditionnal mailing list (folks, come in the Tapestry irc chan : #tapestry on freenode).

See you next time !

Along the night, I tested the Ballmer Peak applied to English spoken skills, and went at least as far in the curve as Millenium developpers...


For me, it was a really really great evening, Howard and Suzan are really nice people.
Moreover, Howard is one of the best architect I know in the Java world, it was a great pleasure to meet him. Suzy is realy nice, the next time I will have to invite my girlfriend too :)

So, thank you Howard and Suzy for this meeting, and see next time (in Portland ?)

  © Blogger template 'Minimalist G' by Ourblogtemplates.com 2008

Back to TOP