You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2014/11/18 21:55:12 UTC

svn commit: r1640424 - in /isis/site/trunk/content: documentation.md tutorials/ tutorials/apacheconeu-2014.md

Author: danhaywood
Date: Tue Nov 18 20:55:12 2014
New Revision: 1640424

URL: http://svn.apache.org/r1640424
Log:
apachecon eu tutorial

Added:
    isis/site/trunk/content/tutorials/
    isis/site/trunk/content/tutorials/apacheconeu-2014.md
Modified:
    isis/site/trunk/content/documentation.md

Modified: isis/site/trunk/content/documentation.md
URL: http://svn.apache.org/viewvc/isis/site/trunk/content/documentation.md?rev=1640424&r1=1640423&r2=1640424&view=diff
==============================================================================
--- isis/site/trunk/content/documentation.md (original)
+++ isis/site/trunk/content/documentation.md Tue Nov 18 20:55:12 2014
@@ -307,10 +307,10 @@ For both:
 * [Unit Test Support](core/unittestsupport.html)
 * [Integration Test Support](core/integtestsupport.html)
 * [BDD/Integ Test Support](core/specsupport-and-integtestsupport.html)
-* [Fixture Scripts](./more-advanced-topics/Fixture-Scripts.html)
 <!--
-* [Fixtures](./more-advanced-topics/03-Fixtures-and-SwitchUser.html) (out of date)
+* [Fixture Scripts](./more-advanced-topics/Fixture-Scripts.html)
 -->
+* [Fixtures](./more-advanced-topics/03-Fixtures-and-SwitchUser.html) (out of date)
 * [IsisConfigurationForJdoIntegTests](components/objectstores/jdo/IsisConfigurationForJdoIntegTests.html)
 
 

Added: isis/site/trunk/content/tutorials/apacheconeu-2014.md
URL: http://svn.apache.org/viewvc/isis/site/trunk/content/tutorials/apacheconeu-2014.md?rev=1640424&view=auto
==============================================================================
--- isis/site/trunk/content/tutorials/apacheconeu-2014.md (added)
+++ isis/site/trunk/content/tutorials/apacheconeu-2014.md Tue Nov 18 20:55:12 2014
@@ -0,0 +1,1867 @@
+Apache Isis: Stop scaffolding, start coding
+================
+
+## Run the archetype
+
+    mvn archetype:generate  \
+        -D archetypeGroupId=org.apache.isis.archetype \
+        -D archetypeArtifactId=simpleapp-archetype \
+        -D archetypeVersion=1.8.0-SNAPSHOT \
+        -D groupId=com.mycompany \
+        -D artifactId=myapp \
+        -D version=1.0-SNAPSHOT \
+        -D archetypeRepository=http://repository-estatio.forge.cloudbees.com/snapshot/ \
+        -B
+    
+## Build and run
+
+    cd myapp
+    mvn clean install
+
+then
+
+    mvn antrun:run -P self-host
+
+or alternatively
+
+    mvn jetty:run    
+    
+
+####DEMO
+
+* install fixtures
+* list all
+* create new
+* list all    
+
+        
+##Dev environment
+
+#### Configure
+
+* IDE:
+  * [IntelliJ](http://isis.apache.org/intro/getting-started/ide/intellij.html)
+  * [Eclipse](http://isis.apache.org/intro/getting-started/ide/eclipse.html)
+* [IDE Editor templates](http://isis.apache.org/intro/resources/editor-templates.html)
+
+
+#### Run
+
+* Run the app from within the IDE
+* Run with different deploymentTypes:
+  - `--type SERVER_PROTOTYPE`
+  - `--type SERVER`
+
+  
+## Explore codebase
+
+* `myapp` : parent module
+* `myapp-dom`
+   - `dom.simple.SimpleObject`
+   - `dom.simple.SimpleObjects`
+* `myapp-fixture`
+   - `fixture.simple.SimpleObjectsFixture`
+* `myapp-integtests`
+* `myapp-webapp`
+
+
+## Testing
+
+* `myapp-dom` unit tests
+   - run 
+   - inspect, eg
+        - `SimpleObjectTest`
+* `myapp-integtests` integration tests
+   - run
+   - inspect, eg: 
+       - `integration.tests.smoke.SimpleObjectsTest`
+       - `integration.specs.simple.SimpleObjectSpec_listAllAndCreate.feature`
+   -  generated report, eg
+        - `myapp/integtests/target/cucumber-html-report/index.html`
+    - change test in IDE, re-run (in Maven)   
+
+    
+## Prototyping
+
+Exclude the `integtests` module.
+
+In the parent `pom.xml`:
+
+    <modules>
+        <module>dom</module>
+        <module>fixture</module>
+        <module>integtests</module>
+        <module>webapp</module>
+    </modules>
+
+change to:
+
+    <modules>
+        <module>dom</module>
+        <module>fixture</module>
+        <!--
+        <module>integtests</module>
+        -->
+        <module>webapp</module>
+    </modules>
+
+
+# Build a domain app
+
+The remainder of the tutorial provides guidance on building a domain application.  We'd rather you build your own app, but if you're not feeling inspired, you could have a go at building our petclinic app.  Here's the design:
+
+![](http://yuml.me/a070d071)
+
+<pre>
+[Visit|-checkIn:DateTime;-checkout:DateTime;-diagnosis:String|+checkin();+checkout();+addNote()]->[Pet|-name:String;-species:PetSpecies]
+[Owner|-firstName:String;-lastName:String]<0..1-0..*>[Pet]
+</pre>
+
+
+## Domain class
+
+* rename the `SimpleObject` class
+* rename the `name` property
+* specify a [title](http://isis.apache.org/how-tos/how-to-01-040-How-to-specify-a-title-for-a-domain-entity.html)
+* specify an [icon](http://isis.apache.org/how-tos/how-to-01-070-How-to-specify-the-icon-for-a-domain-entity.html)
+
+
+## Domain service
+
+* rename the `SimpleObjects` class
+* review `create` action (acting as a factory)
+  - as per our [docs](http://isis.apache.org/how-tos/how-to-01-160-How-to-create-or-delete-objects-within-your-code.html)
+* review `listAll` action (acting as a repository)
+  - as per our [docs](http://isis.apache.org/how-tos/how-to-09-040-How-to-write-a-custom-repository.html)
+  - note the annotations on the corresponding domain class (`SimpleObject` above, probably renamed by now)
+
+  
+## Fixture scripts
+
+* rename the `SimpleObjectsTearDownFixture` class
+  - and update
+* create for domain classes
+  - inject in the corresponding domain service
+
+ 
+## Actions
+
+* update the domain property (`SimpleObject#name` above, renamed by now)
+* use [@ActionSemantics](http://isis.apache.org/reference/recognized-annotations/ActionSemantics.html) annotation to indicate the semantics of the action (safe/query-only, idempotent or non-idempotent)
+* annotate safe action as [@Bookmarkable](http://isis.apache.org/reference/recognized-annotations/Bookmarkable.html) 
+  * confirm is available from bookmark panel (top-left of Wicket UI)
+  
+  
+## REST API
+
+* add Chrome extensions
+  * install Postman
+  * install JSON-View
+* browse to Wicket viewer, install fixtures
+* browse to the http://localhost:8080/restful API
+* invoke the service to list all objects
+
+
+## Specify Action semantics
+
+* note the HTTP methods exposed in the REST API
+
+
+## Value properties
+
+* add some [value properties](http://isis.apache.org/how-tos/how-to-01-030-How-to-add-a-property-to-a-domain-entity.html); also:
+  - [optional vs mandatory](http://isis.apache.org/components/objectstores/jdo/mapping-mandatory-and-optional-properties.html)
+  - [joda date/time](http://isis.apache.org/components/objectstores/jdo/mapping-joda-dates.html)
+  - [bigdecimals](http://isis.apache.org/components/objectstores/jdo/mapping-bigdecimals.html)
+  - [blob/clobs](http://isis.apache.org/components/objectstores/jdo/mapping-blobs.html)
+* update the corresponding domain service
+  - if not optional
+* update the title, if need be
+* [order the properties](http://isis.apache.org/how-tos/how-to-01-080-How-to-specify-the-order-in-which-properties-or-collections-are-displayed.html) using the [@MemberOrder](http://isis.apache.org/reference/recognized-annotations/MemberOrder.html) annotation and [@MemberGroupLayout](http://isis.apache.org/reference/recognized-annotations/MemberGroupLayout.html) annotation
+  * see also this [static layouts](http://isis.apache.org/components/viewers/wicket/static-layouts.html) documentation
+
+
+## Reference properties
+
+* add some [reference properties](http://isis.apache.org/how-tos/how-to-01-030-How-to-add-a-property-to-a-domain-entity.html)
+* update the corresponding domain service
+* use different techniques to obtain references (shown in drop-down list box)
+  * use [@Bounded](http://isis.apache.org/reference/recognized-annotations/Bounded.html) annotation
+  * use the [@AutoComplete](http://isis.apache.org/reference/recognized-annotations/AutoComplete.html) annotation
+  * use a `choicesXxx()` supporting method on [property](http://isis.apache.org/how-tos/how-to-03-010-How-to-specify-a-set-of-choices-for-a-property.html) or [action param](http://isis.apache.org/how-tos/how-to-03-020-How-to-specify-a-set-of-choices-for-an-action-parameter.html)
+  * use an `autoCompleteXxx()` supporting method on [property](http://isis.apache.org/how-tos/how-to-03-015-How-to-specify-an-autocomplete-for-a-property.html) or [action param](http://isis.apache.org/how-tos/how-to-03-025-How-to-specify-an-autocomplete-for-an-action-parameter.html)
+
+  
+## Usability: Defaults
+
+* Add [defaults](http://isis.apache.org/how-tos/how-to-03-050-How-to-specify-default-values-for-an-action-parameter.html) for action parameters
+ 
+
+## Collections  
+
+* Ensure that all domain classes implement `Comparable`
+  * use the [ObjectContracts](http://isis.apache.org/reference/Utility.html) utility class to help implement `Comparable` (also `equals()`, `hashCode()`, `toString()`)
+* Add a [one-to-many-collection](http://isis.apache.org/components/objectstores/jdo/managed-1-to-m-relationships.html) to one of the entities
+  * Use `SortedSet` as the class
+* TODO: @Render (http://isis.apache.org/reference/recognized-annotations/Render.html)
+* optional: Use the [@SortedBy](http://isis.apache.org/reference/recognized-annotations/SortedBy.html) annotation to specify a different comparator than the natural ordering
+
+## Actions (ctd)
+
+* Add domain actions to add/remove from the collection
+
+
+## Layout
+
+* Use the [@MemberOrder](http://isis.apache.org/reference/recognized-annotations/MemberOrder.html) annotation to associate an action with a property or with a collection
+  * set the `name` attribute
+* Delete the `@MemberOrder` annotations and use the associated [.layout.json](http://isis.apache.org/components/viewers/wicket/dynamic-layouts.html) file to specify layout hints instead
+
+
+## Business rules: See it!
+
+* Use the [@Hidden](http://isis.apache.org/reference/recognized-annotations/Hidden.html) annotation to make properties/collections/actions invisible
+  * the [@Programmatic](http://isis.apache.org/reference/recognized-annotations/Programmatic.html) annotation can also be used and in many cases is to be preferred; the difference is that the latter means the member is not part of the Isis metamodel.
+* Use the `hideXxx()` supporting method on [properties](http://isis.apache.org/how-tos/how-to-02-010-How-to-hide-a-property.html), [collections](http://isis.apache.org/how-tos/how-to-02-020-How-to-hide-a-collection.html) and [actions](http://isis.apache.org/how-tos/how-to-02-030-How-to-hide-an-action.html) to make a property/collection/action invisible according to some imperative rule
+
+  
+## Business rules: Use it!
+
+* Use the [@Disabled](http://isis.apache.org/reference/recognized-annotations/Disabled.html) annotation to make properties read-only/actions non-invokable ('greyed out')
+* Use the `disabledXxx()` supporting method on [properties](http://isis.apache.org/how-tos/how-to-02-050-How-to-prevent-a-property-from-being-modified.html) and [actions](http://isis.apache.org/how-tos/how-to-02-070-How-to-prevent-an-action-from-being-invoked.html) to make a property/action disabled according to some imperative rule
+
+
+## Business rules: Do it!
+
+* Use the [@Regex](http://isis.apache.org/reference/recognized-annotations/RegEx.html) annotation on string properties or action parameters to validate strings
+* Use the [@MinLength]()
+
+
+(or more generally the [@MustSatisfy]() annotation) to specify 
+
+Use the `validateXxx()` supporting method on [properties](
+
+
+- disable
+- hide
+- vaidate
+
+
+Bookmarkable  
+  
+  
+dashboards (home page)
+
+
+
+
+
+
+contributed properties/collections
+
+
+contributed actions
+
+
+creating objects:
+- domainobjectcontainer
+
+
+clockservice
+
+
+.layout.json
+
+
+view models
+
+
+
+Customising the REST API
+
+
+
+
+wicket extensions
+- excel download
+- fullcalendar
+- gmap3
+
+
+add-ons
+- security
+- command
+- auditing
+- publishing
+
+
+event bus
+
+
+RESTful API
+
+
+
+
+
+composite fixture scripts (a la Estatio)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Stuff to do...
+
+* rename domain class
+
+* rename domain service
+
+* fixture scripts
+
+
+Tests
+
+* delete the BDD specs
+
+
+
+## Dele
+    
+    
+
+##Rename domain class, domain service, fixtures
+
+####SimpleObject
+
+rename to `ConferenceSession`
+
+####SimpleObjects
+
+rename to `ConferenceSessions`
+
+####SimpleObjectsFixture
+
+rename to `ConferenceSessionsFixture`
+
+
+####myapp-webapp/src/main/webapp/WEB-INF/isis.properties
+
+<pre>
+isis.services = \
+                10:dom.simple.SimpleObjects,\
+                ...
+
+isis.fixtures=fixture.simple.SimpleObjectsFixture
+</pre>
+
+to
+
+<pre>
+isis.services = \
+                10:dom.simple.ConferenceSessions,\
+                ...
+
+isis.fixtures=fixture.simple.ConferenceSessionsFixture
+</pre>
+
+####ConferenceSessionsFixture
+
+<pre>
+isisJdoSupport.executeUpdate("delete from \"SimpleObject\"");
+</pre>
+
+to:
+
+<pre>
+isisJdoSupport.executeUpdate("delete from \"ConferenceSession\"");
+</pre>
+
+#### icon
+
+* add icon, `icons/OverheadProjector.png` -> `myapp/dom/src/main/resources/images/ConferenceSession.png`
+
+
+
+####ConferenceSessions
+
+* getId()
+
+*a "nice to have", but has an effect on the URLs exposed in the REST API*
+
+<pre>
+public String getId() {
+    return "conferenceSessions";
+}
+</pre>
+
+* `iconName()`
+
+<pre>
+public String iconName() {
+    return "ConferenceSession";
+}
+</pre>
+
+####ConferenceSession
+
+*a "nice to have", but has an effect on the URLs exposed in the REST API*
+<pre>
+@ObjectType("SESSION")
+</pre>
+
+
+####DEMO
+
+* menu service changed to "Conference Sessions"
+* tooltip on icon/title changed to "Conference Session"
+* icon changed
+
+
+#####Checkpoint:
+`git reset --hard checkpoint-030`
+
+
+
+
+
+
+##Refactor domain class
+
+####ConferenceSession
+
+* rename `name` property to `sessionTitle`
+
+* update `compareTo()`
+<pre>
+public int compareTo(ConferenceSession other) {
+    return ObjectContracts.compare(this, other, "sessionTitle");
+}
+</pre>
+
+* review title
+
+<pre>
+@Title
+public String getSessionTitle() {
+    return sessionTitle;
+}
+</pre>
+
+#### ConferenceSessionsFixture
+
+* Better fixture data
+
+<pre>
+private void installObjects() {
+    create("RRRADDD! Apache Isis");
+    create("Best practices for AngularJS");
+    create("Refactor your specs!");
+    create("Why Kotlin?");
+    create("Introduction to the Play Framework");
+    create("Object Oriented Design in the Wild");
+}
+</pre>
+
+
+####DEMO
+
+* table shows sessionTitle column
+* object form shows sessionTitle property
+* object page labelled with title
+* updated data
+
+also
+
+* discuss injected services into fixtures, entities, services etc.
+  
+#####Checkpoint:
+`git reset --hard checkpoint-040`
+
+
+
+
+
+##Add date scalar property
+
+####ConferenceSession
+
+<pre>
+// //////////////////////////////////////
+// date (property)
+// //////////////////////////////////////
+
+private LocalDate date;
+
+@javax.jdo.annotations.Persistent
+@javax.jdo.annotations.Column(allowsNull = "true")
+@MemberOrder(sequence = "1")
+public LocalDate getDate() {
+    return date;
+}
+
+public void setDate(final LocalDate date) {
+    this.date = date;
+}
+</pre>
+
+
+####ConferenceSessionsFixture    
+
+<pre>
+private ConferenceSession create(final String name) {
+    ...
+    session.setDate(clockService.now().plusDays((int)(Math.random()*5)));
+    ...
+}
+</pre>
+
+and
+
+<pre>
+private ClockService clockService;
+
+public final void injectClockService(final ClockService clockService) {
+    this.clockService = clockService;
+}
+</pre>
+>
+
+
+####DEMO
+
+* now has a date property in title and in form
+* can edit, obviously
+* is optional
+ 
+#####Checkpoint:
+`git reset --hard checkpoint-050`
+
+
+
+
+## Add enum scalar property
+
+* type property
+
+#####ConferenceSession
+
+<pre>
+// //////////////////////////////////////
+// Type (property)
+// //////////////////////////////////////
+
+public enum Type {
+    KEYNOTE, ALL_DAY_TUTORIAL, SESSION, OTHER
+}
+
+private Type Type;
+@javax.jdo.annotations.Column(allowsNull="false")
+@MemberOrder(sequence = "1")
+public Type getType() {
+    return Type;
+}
+public void setType(final Type Type) {
+    this.Type = Type;
+}
+</pre>
+
+    
+#####ConferenceSessions
+
+* update the `create(...)` method
+
+<pre>
+public ConferenceSession create(
+        final @Named("Name") String name,
+        final @Named("Type") ConferenceSession.Type type) {
+    final ConferenceSession obj = newTransientInstance(ConferenceSession.class);
+    obj.setSessionTitle(name);
+    obj.setType(type);
+    persistIfNotAlready(obj);
+    return obj;
+}
+</pre>
+
+    
+#####ConferenceSessionsFixture
+
+* update the fixture data
+
+<pre>
+private void installObjects() {
+    create("RRRADDD! Apache Isis", ConferenceSession.Type.SESSION);
+    create("Best practices for AngularJS", ConferenceSession.Type.SESSION);
+    create("Refactor your specs!", ConferenceSession.Type.SESSION);
+    create("Why Kotlin?", ConferenceSession.Type.OTHER);
+    create("Introduction to the Play Framework", ConferenceSession.Type.SESSION);
+    create("Object Oriented Design in the Wild", ConferenceSession.Type.ALL_DAY_TUTORIAL);
+}
+
+private ConferenceSession create(final String name, final Type type) {
+    return simpleObjects.create(name, type);
+}
+</pre>
+
+  
+####DEMO
+
+* list all
+   * table with new property
+* show object form
+* edit object details
+    * both are mandatory
+* create new object
+
+
+#####Checkpoint:
+`git reset --hard checkpoint-060`
+
+
+
+##UI hints
+
+##### `ConferenceSession
+    
+* sessionTitle, then type, then date
+
+<pre>
+@MemberOrder(sequence="1")
+public String getSessionTitle() {
+</pre>
+
+* then type
+ 
+<pre>
+@MemberOrder(sequence = "2")
+public Type getType() {
+</pre>
+    
+* then date (hide in table)
+
+<pre>
+@Hidden(where=Where.ALL_TABLES)
+@MemberOrder(name="Scheduling", sequence = "3")
+public LocalDate getDate() {
+</pre>
+
+
+####DEMO
+
+* table shows only title, type properties (in that order)
+* order form shows
+   * title, type properties grouped together in the default 'General' group
+   * date property separately in a new 'Scheduling' group
+
+#####Checkpoint:
+`git reset --hard checkpoint-070`
+
+
+
+Add another type (and domain service)
+-------------------------------------
+
+####Speaker
+
+    package dom.simple;
+    
+    import javax.jdo.annotations.IdentityType;
+    import javax.jdo.annotations.VersionStrategy;
+    
+    import org.apache.isis.applib.DomainObjectContainer;
+    import org.apache.isis.applib.annotation.Bookmarkable;
+    import org.apache.isis.applib.annotation.MemberOrder;
+    import org.apache.isis.applib.annotation.ObjectType;
+    import org.apache.isis.applib.annotation.Title;
+    import org.apache.isis.applib.util.ObjectContracts;
+    
+    @javax.jdo.annotations.PersistenceCapable(identityType=IdentityType.DATASTORE)
+    @javax.jdo.annotations.DatastoreIdentity(
+            strategy=javax.jdo.annotations.IdGeneratorStrategy.IDENTITY,
+             column="id")
+    @javax.jdo.annotations.Version(
+            strategy=VersionStrategy.VERSION_NUMBER, 
+            column="version")
+    @ObjectType("SPEAKER")
+    @Bookmarkable
+    public class Speaker implements Comparable<Speaker> {
+    
+        
+        // //////////////////////////////////////
+        // givenName (property)
+        // //////////////////////////////////////
+    
+        private String givenName;
+    
+        @MemberOrder(sequence = "1")
+        @javax.jdo.annotations.Column(allowsNull="false")
+        @Title(sequence="2", prepend=", ")
+        public String getGivenName() {
+            return givenName;
+        }
+    
+        public void setGivenName(final String name) {
+            this.givenName = name;
+        }
+        
+        
+        // //////////////////////////////////////
+        // familyName (property)
+        // //////////////////////////////////////
+    
+        private String familyName;
+    
+        @MemberOrder(sequence = "2")
+        @javax.jdo.annotations.Column(allowsNull="false")
+        @Title(sequence="1")
+        public String getFamilyName() {
+            return familyName;
+        }
+    
+        public void setFamilyName(final String familyName) {
+            this.familyName = familyName;
+        }
+    
+    
+        // //////////////////////////////////////
+        // compareTo
+        // //////////////////////////////////////
+    
+        @Override
+        public int compareTo(Speaker other) {
+            return ObjectContracts.compare(this, other, "givenName, familyName");
+        }
+    
+        
+        // //////////////////////////////////////
+        // Injected
+        // //////////////////////////////////////
+    
+        @SuppressWarnings("unused")
+        private DomainObjectContainer container;
+        public void injectDomainObjectContainer(final DomainObjectContainer container) {
+            this.container = container;
+        }
+        
+    }
+
+
+####Speakers
+
+    package dom.simple;
+    
+    import java.util.List;
+    
+    import org.apache.isis.applib.AbstractFactoryAndRepository;
+    import org.apache.isis.applib.annotation.ActionSemantics;
+    import org.apache.isis.applib.annotation.ActionSemantics.Of;
+    import org.apache.isis.applib.annotation.Bookmarkable;
+    import org.apache.isis.applib.annotation.MemberOrder;
+    import org.apache.isis.applib.annotation.Named;
+    
+    public class Speakers extends AbstractFactoryAndRepository {
+    
+        @Override
+        public String getId() {
+            return "speakers";
+        }
+    
+        public String iconName() {
+            return "Speaker";
+        }
+    
+        @Bookmarkable
+        @ActionSemantics(Of.SAFE)
+        @MemberOrder(sequence = "1")
+        public List<Speaker> listAll() {
+            return allInstances(Speaker.class);
+        }
+    
+        @MemberOrder(sequence = "2")
+        public Speaker create(
+                final @Named("Given name") String givenName,
+                final @Named("Family name") String familyName) {
+            Speaker speaker = getContainer().newTransientInstance(Speaker.class);
+            speaker.setGivenName(givenName);
+            speaker.setFamilyName(familyName);
+            getContainer().persistIfNotAlready(speaker);
+            return speaker;
+        }
+    }
+
+
+#####ConferenceSessionsFixture
+
+* install()
+
+        isisJdoSupport.executeUpdate("delete from \"Speaker\"");
+
+* installObjects()
+
+        createSpeaker("Dan", "Haywood");
+        createSpeaker("Misko", "Hevery");
+        createSpeaker("Cyrille", "Martraire");
+        createSpeaker("Svetlana", "Haywood");
+        createSpeaker("James", "Ward");
+        createSpeaker("Jessica", "Kerr");
+
+* createSpeaker() method:
+
+        private Speaker createSpeaker(final String givenName, final String familyName) {
+            return speakers.create(givenName, familyName);
+        }
+
+
+* and inject new domain service:
+
+        private Speakers speakers;
+        public final void injectSpeakers(final Speakers speakers) {
+            this.speakers = speakers;
+        }
+
+   
+#####isis.properties
+
+* number indicates the ordering in the menu
+
+        isis.services = \
+                        10:dom.simple.ConferenceSessions,\
+                        20:dom.simple.Speakers,\
+
+#### icon
+
+* add icon, `icons/Customer.gif` -> `myapp/dom/src/main/resources/images/Speaker.gif`
+
+
+
+####DEMO
+
+* new domain service
+* list, create
+
+#####Checkpoint:
+`git reset --hard checkpoint-080`
+
+
+##Add reference property
+
+####ConferenceSession
+
+* 'speaker' property (of type 'Speaker')
+
+        // //////////////////////////////////////
+        // speaker (property)
+        // //////////////////////////////////////
+    
+        private Speaker speaker;
+    
+        @javax.jdo.annotations.Column(allowsNull="true")
+        @MemberOrder(sequence = "3")
+        public Speaker getSpeaker() {
+            return speaker;
+        }
+    
+        public void setSpeaker(final Speaker speaker) {
+            this.speaker = speaker;
+        }
+
+####DEMO
+
+* displays field, no way to associate :-(
+
+
+#####Checkpoint:
+`git reset --hard checkpoint-090`
+
+
+##Add autoComplete (and repository queries)
+
+####ConferenceSession
+
+* autoComplete method:
+
+        public List<Speaker> autoCompleteSpeaker(String search) {
+            return speakers.findByGivenOrFamilyName(search);
+        }
+
+* inject service:
+
+        private Speakers speakers;
+    
+        public final void injectSpeakers(final Speakers speakers) {
+            this.speakers = speakers;
+        }
+
+    
+#####Speakers    
+
+* finder (hidden because only intended to be called programmatically)
+    
+        @Hidden
+        public List<Speaker> findByGivenOrFamilyName(String search) {
+            return getContainer().allMatches(
+                    new QueryDefault<Speaker>(Speaker.class, 
+                            "findByGivenOrFamilyName", 
+                            "givenOrFamilyName", ".*"+search+".*"));
+        }
+
+
+#####Speaker
+
+* Isis configured to use JDO, so add the annotation
+
+        @javax.jdo.annotations.Queries({
+            @javax.jdo.annotations.Query(
+                    name="findByGivenOrFamilyName", language="JDOQL",
+                    value="SELECT "
+                            + "FROM dom.simple.Speaker "
+                            + "WHERE givenName.matches(:givenOrFamilyName) "
+                            + "   || familyName.matches(:givenOrFamilyName)")
+        })
+
+####DEMO
+
+* can now add speaker
+    * using either given or family name
+* Speakers finder is not visible
+
+#####Checkpoint:
+`git reset --hard checkpoint-100`
+
+
+
+   
+##Update title (also, prototype actions)
+
+####ConferenceSession
+
+* title imperatively, rather than declaratively
+
+        public String title() {
+            final TitleBuffer buf = new TitleBuffer();
+            if(getSessionTitle().length()>20) {
+                buf.append(getSessionTitle().substring(0, 20)).append("...");
+            } else {
+                buf.append(getSessionTitle());
+            }
+            if(getSpeaker() != null) {
+                buf.append("(").append(container.titleOf(getSpeaker())).append(")");
+            }
+            return buf.toString();
+        }
+
+
+####DEMO:
+
+* metamodel validation exception
+
+
+
+####ConferenceSession
+
+* remove `@Title` from `#sessionTitle`
+
+####ConferenceSessions
+
+* Add prototyping action to domain service
+
+        @Bookmarkable
+        @Prototype
+        @ActionSemantics(Of.SAFE)
+        public ConferenceSession firstOne() {
+            return listAll().get(0);
+        }
+              
+
+
+####DEMO:
+* now runs ok
+* prototype action styled differently
+* add speaker to session
+* title changes
+* (optional) run in SERVER mode
+
+
+#####Checkpoint:
+`git reset --hard checkpoint-110`
+
+
+
+
+## Another new entity, Tag
+
+*the motivation for this is that we want to add a new collection of tags from ConferenceSession*
+
+
+####Tag
+
+* new entity (note the `compareTo`, `toString`, using Isis helper methods)
+
+        package dom.simple;
+        
+        import javax.jdo.annotations.IdentityType;
+        import javax.jdo.annotations.VersionStrategy;
+        
+        import org.apache.isis.applib.DomainObjectContainer;
+        import org.apache.isis.applib.annotation.Bookmarkable;
+        import org.apache.isis.applib.annotation.MemberOrder;
+        import org.apache.isis.applib.annotation.ObjectType;
+        import org.apache.isis.applib.annotation.Title;
+        import org.apache.isis.applib.util.ObjectContracts;
+        
+        @javax.jdo.annotations.PersistenceCapable(identityType=IdentityType.DATASTORE)
+        @javax.jdo.annotations.DatastoreIdentity(
+                strategy=javax.jdo.annotations.IdGeneratorStrategy.IDENTITY,
+                 column="id")
+        @javax.jdo.annotations.Version(
+                strategy=VersionStrategy.VERSION_NUMBER, 
+                column="version")
+        @ObjectType("TAG")
+        @Bookmarkable
+        public class Tag implements Comparable<Tag> {
+        
+            
+            // //////////////////////////////////////
+            // name (property)
+            // //////////////////////////////////////
+        
+            private String name;
+        
+            @javax.jdo.annotations.Column(allowsNull = "true")
+            @Title
+            @MemberOrder(sequence = "1")
+            public String getName() {
+                return name;
+            }
+
+            public void setName(final String name) {
+                this.name = name;
+            }
+        
+        
+        
+            // //////////////////////////////////////
+            // compareTo
+            // //////////////////////////////////////
+        
+            @Override
+            public int compareTo(Tag other) {
+                return ObjectContracts.compare(this, other, "name");
+            }
+        
+            
+            // //////////////////////////////////////
+            // toString
+            // //////////////////////////////////////
+        
+            @Override
+            public String toString() {
+                return ObjectContracts.toString(this, "name");
+            }
+        
+            
+            
+            // //////////////////////////////////////
+            // Injected
+            // //////////////////////////////////////
+        
+            @SuppressWarnings("unused")
+            private DomainObjectContainer container;
+            public void injectDomainObjectContainer(final DomainObjectContainer container) {
+                this.container = container;
+            }
+        }
+
+
+####Tags
+
+* new domain service; all actions are hidden
+
+        package dom.simple;
+        
+        import java.util.List;
+        
+        import org.apache.isis.applib.AbstractFactoryAndRepository;
+        import org.apache.isis.applib.annotation.Hidden;
+        
+        public class Tags extends AbstractFactoryAndRepository {
+        
+            @Override
+            public String getId() {
+                return "tags";
+            }
+            public String iconName() {
+                return "Tag";
+            }
+        
+            @Hidden
+            public List<Tag> listAll() {
+                return allInstances(Tag.class);
+            }
+            @Hidden
+            public Tag create(final String name) {
+                final Tag obj = newTransientInstance(Tag.class);
+                obj.setName(name);
+                persistIfNotAlready(obj);
+                return obj;
+            }
+        }
+
+
+####ConferenceSessionsFixture
+
+* installObjects()
+ 
+        createTag("UX");
+        createTag("Mobile");
+        createTag("Java");
+        createTag("Agile");
+        createTag(".NET");
+        createTag("Test");
+        createTag("Mastery");
+        createTag("Web");
+        createTag("Architecture");
+        createTag("Dev Ops");
+        createTag("Cloud");
+        createTag("Languages");
+        createTag("Tools");
+        createTag("Team");
+        createTag("Database");
+        createTag("Javascript");
+        createTag("Keynote");
+
+
+* createTag() method
+
+        private Tag createTag(final String name) {
+            return tags.create(name);
+        }
+
+* inject new service
+    
+        private Tags tags;
+        public final void injectTags(final Tags tags) {
+            this.tags = tags;
+        }
+
+
+####isis.properties
+
+* no number required, since not visible in UI
+
+        isis.services = \
+                        10:dom.simple.ConferenceSessions,\
+                        20:dom.simple.Speakers,\
+                        dom.simple.Tags,\
+
+
+#### icon
+
+* add icon, `icons/RedFlag.png` -> `myapp/dom/src/main/resources/images/Tag.png`
+
+####DEMO 
+
+* (no visible change, since Tags actions are all hidden)                
+       
+#####Checkpoint:
+`git reset --hard checkpoint-120`
+
+
+
+
+         
+## Add collection
+              
+#### ConferenceSession
+              
+* tags collection
+
+        // //////////////////////////////////////
+        // tags (collection)
+        // //////////////////////////////////////
+    
+        @javax.jdo.annotations.Join
+        @javax.jdo.annotations.Element(dependent = "false")
+        private SortedSet<Tag> tags = new TreeSet<Tag>();
+    
+        @Render(Render.Type.EAGERLY)
+        @Disabled
+        @MemberOrder(sequence = "1")
+        public SortedSet<Tag> getTags() {
+            return tags;
+        }
+    
+        public void setTags(final SortedSet<Tag> tags) {
+            this.tags = tags;
+        }
+
+
+####DEMO
+* view the DDL
+* open object
+* can't add tags :-(
+
+#####Checkpoint:
+`git reset --hard checkpoint-130`
+
+
+
+##Add actions
+
+####ConferenceSession
+
+* addTag action
+
+        // //////////////////////////////////////
+        // addTag (action)
+        // //////////////////////////////////////
+        
+        @MemberOrder(sequence = "1")
+        public ConferenceSession addTag(final Tag tag) {
+            getTags().add(tag);
+            return this;
+        }
+
+* removeTag action
+ 
+        // //////////////////////////////////////
+        // removeTag (action)
+        // //////////////////////////////////////
+        
+        @MemberOrder(sequence = "2")
+        public ConferenceSession removeTag(final Tag tag) {
+            getTags().remove(tag);
+            return this;
+        }
+
+
+####ConferenceSessionsFixture
+
+* create(...) method, add three (random) tags for each session
+
+        for (Tag tag : random(tags.listAll(), 3)) {
+            session.addTag(tag);
+        }
+
+* supporting random(...) method
+
+        private List<Tag> random(List<Tag> tags, int num) {
+            List<Tag> availableTags = Lists.newArrayList(tags);
+            List<Tag> selectedTags = Lists.newArrayList();
+            while(selectedTags.size()<num) {
+                int selected = (int)(availableTags.size() * Math.random());
+                try {
+                    selectedTags.add(availableTags.remove(selected));
+                } catch(Exception ex) {}
+            }
+            return selectedTags;
+        }
+
+
+####DEMO:
+* view
+* need to get a reference to the tag again...
+    
+    
+
+#####Checkpoint:
+`git reset --hard checkpoint-140`
+
+
+
+##Add choices
+
+*an alternative to using autoComplete, useful if the list of choices is relatively short*
+
+####ConferenceSession
+
+* inject Tags repo
+
+        private Tags tagRepo;
+        
+        public final void injectTags(final Tags tags) {
+            this.tagRepo = tags;
+        }
+
+* added the "choices" supporting method for addTag action (all tags not yet added)
+ 
+        public Collection<Tag> choices0AddTag() {
+            List<Tag> tags = Lists.newArrayList(tagRepo.listAll());
+            tags.removeAll(getTags());
+            return tags;
+        }
+
+* added the "choices" supporting method for removeTag action (only those tags previously added)
+
+        public Collection<Tag> choices0RemoveTag() {
+            return getTags();
+        }
+    
+####DEMO
+
+* can now add/remove tags, yay!
+
+#####Checkpoint:
+`git reset --hard checkpoint-150`
+
+    
+    
+##More UI Hints (dynamic, this time)
+
+####DEMO:
+* download layout
+* edit column spans
+* copy `ConferenceSession.layout.json` (below) to `myapp-dom/src/main/java/dom/simple`
+* refresh layout
+ 
+        {
+          "columns": [
+            {
+              "span": 6,
+              "memberGroups": {
+                "General": {
+                  "members": {
+                    "sessionTitle": {},
+                    "type": {},
+                    "speaker": {}
+                  }
+                },
+                "Scheduling": {
+                  "members": {
+                    "date": {}
+                  }
+                }
+              }
+            },
+            {
+              "span": 0,
+              "memberGroups": {}
+            },
+            {
+              "span": 0,
+              "memberGroups": {}
+            },
+            {
+              "span": 6,
+              "collections": {
+                "tags": {
+                  "actions": {
+                    "addTag": {},
+                    "removeTag": {}
+                  }
+                }
+              }
+            }
+          ],
+          "actions": {
+            "downloadLayout": {},
+            "refreshLayout": {}
+          }
+        }
+
+#####Checkpoint:
+`git reset --hard checkpoint-160`
+
+
+
+
+##Declarative and imperative business rules
+
+####ConferenceSession
+
+* add RegEx pattern and a maximum length to the 'sessionTitle' property (latter using JDO annotation)
+
+        @javax.jdo.annotations.Column(allowsNull="false",length=40)
+        @RegEx(validation="[^%]+")
+        public String getSessionTitle() {
+
+
+* can't add more than 4 tags
+
+        public String disableAddTag(Tag tag) {
+            return getTags().size() >= 4? "Cannot add more than 4 tags": null;
+        }
+
+####DEMO
+
+* cannot save session whose sessionTitle property has > 40 chars
+* cannot add more than 4 tags to a session
+* 
+
+#####Checkpoint:
+`git reset --hard checkpoint-170`
+
+    
+
+
+##Other business rules (hiding, validation)
+
+####ConferenceSession
+
+* hideAddTag 
+
+        public boolean hideAddTag() {
+            return getTags().size() >= 4;
+        }
+
+####DEMO
+* action becomes hidden
+
+
+####ConferenceSession
+
+* offer tags that are already in the collection, but then prevent using validation
+
+        public Collection<Tag> choices0AddTag() {
+            List<Tag> tags = Lists.newArrayList(tagRepo.listAll());
+            //tags.removeAll(getTags());
+            return tags;
+        }
+    
+        public String validateAddTag(Tag tag) {
+            return getTags().contains(tag)? "Already added that tag": null;
+        }
+
+####DEMO
+* can attempt to add a tag, but prevented
+
+
+####ConferenceSession
+
+* backing out the above changes
+
+        public Collection<Tag> choices0AddTag() {
+            List<Tag> tags = Lists.newArrayList(tagRepo.listAll());
+            tags.removeAll(getTags());
+            return tags;
+        }
+
+        //    public String validateAddTag(Tag tag) {
+        //        return getTags().contains(tag)? "Already added that tag": null;
+        //    }
+        
+        //    public boolean hideAddTag() {
+        //        return getTags().size() >= 4;
+        //    }
+
+        public String disableAddTag(Tag tag) {
+            return getTags().size() >= 4? "Cannot add more than 4 tags": null;
+        }
+
+
+
+##Contributed Actions etc
+
+####ConferenceSessions
+
+* add action, will be contributed to Tag, as both action and a collection 
+
+        // //////////////////////////////////////
+        // Contributions
+        // //////////////////////////////////////
+
+        // @NotContributed
+        @NotInServiceMenu
+        public List<ConferenceSession> findByTag(Tag tag) {
+            return allMatches(new QueryDefault<ConferenceSession>(
+                    ConferenceSession.class, "findByTag", "tag", tag));
+        }
+
+####ConferenceSession
+
+* declare JDO query
+ 
+        @javax.jdo.annotations.Queries({
+            @javax.jdo.annotations.Query(
+                    name="findByTag", language="JDOQL",
+                    value="SELECT "
+                            + "FROM dom.simple.ConferenceSession "
+                            + "WHERE tags.contains(:tag)")
+        })
+
+#### DEMO
+
+* view Tag; shows the sessions that are associated with it
+
+#####Checkpoint:
+`git reset --hard checkpoint-180`
+
+
+    
+
+##Bookmarks
+    
+####DEMO
+* bookmarked objects
+   * eg `ConferenceSession`
+* bookmarked actions
+   * eg `ConferenceSessions#listAll`
+    
+
+
+
+##CSS
+
+####myapp-webapp/src/main/webapp/css/application.css
+
+* example custom CSS already defined for "x-highlight" and "x-caution" CSS classes 
+
+#### ConferenceSession
+
+* addTag, add annotation
+
+        @CssClass("x-highlight")
+        public ConferenceSession addTag(final Tag tag) {
+
+* removeTag, add annotation
+    
+        @CssClass("x-caution")
+        public ConferenceSession removeTag(final Tag tag) {
+
+
+#### DEMO
+
+* view session object
+
+#####Checkpoint:
+`git reset --hard checkpoint-190`
+
+
+
+##Widgets, also @HomePage
+
+*show Conference sessions on a calendar view.  This uses a "third-party" component, up on github*
+
+#### myapp/pom.xml
+
+* parent module, scope=import for dependency management
+
+        <dependencyManagement>
+            <dependencies>
+                ...
+                
+                <dependency>
+                    <groupId>com.danhaywood.isis.wicket</groupId>
+                    <artifactId>danhaywood-isis-wicket-fullcalendar2</artifactId>
+                    <version>1.3.0</version>
+                    <type>pom</type>
+                    <scope>import</scope>
+                </dependency>
+        
+            </dependencies>
+        </dependencyManagement>
+    
+    
+####myapp-dom/pom.xml
+
+* dom project depends on modules 'applib' (minimal coupling, defines an interface and value type)
+ 
+    	<dependencies>
+            ...
+    
+            <dependency>
+                <groupId>com.danhaywood.isis.wicket</groupId>
+                <artifactId>danhaywood-isis-wicket-fullcalendar2-applib</artifactId>
+            </dependency>
+            
+    	</dependencies>
+
+
+####myapp-webapp/pom.xml
+
+* webapp project references the actual widget UI implementation
+
+        <dependencies>
+            ...
+    
+            <dependency>
+               <groupId>com.danhaywood.isis.wicket</groupId>
+                <artifactId>danhaywood-isis-wicket-fullcalendar2-ui</artifactId>
+            </dependency>
+     
+        </dependencies>
+  
+
+#####Checkpoint:
+`git reset --hard checkpoint-200`
+
+
+
+  
+####ConferenceSession
+
+* declare it implements the `CalendarEventable` interface
+ 
+        public class ConferenceSession implements Comparable<ConferenceSession>
+            , com.danhaywood.isis.wicket.fullcalendar2.applib.CalendarEventable {
+        
+
+* and implement the required methods:
+            
+            // //////////////////////////////////////
+            // CalendarEventable impl
+            // //////////////////////////////////////
+        
+            @Programmatic
+            @Override
+            public String getCalendarName() {
+                return "date";
+            }
+        
+            @Programmatic
+            @Override
+            public CalendarEvent toCalendarEvent() {
+                return new CalendarEvent(
+                        getDate().toDateTimeAtStartOfDay(), 
+                        "date", 
+                        container.titleOf(this));
+            }
+
+
+##ConferenceSessions
+
+* indicate that the listAll action should be called for the home page
+    
+        @HomePage
+        @Bookmarkable
+        @ActionSemantics(Of.SAFE)
+        @MemberOrder(sequence = "1")
+        public List<ConferenceSession> listAll() {
+            return allInstances(ConferenceSession.class);
+        }
+
+
+
+####DEMO
+* all sessions displayed automatically on home page
+* switch to calendar view (top right, new icon should appear)
+
+
+#####Checkpoint:
+`git reset --hard checkpoint-210`
+
+
+
+    
+##View models
+
+####SpeakerViewModel
+
+* shows summary info of speaker, their sessions, the tags of those sessions
+
+        package dom.simple;
+        
+        import java.util.List;
+        import java.util.Set;
+        
+        import org.apache.isis.applib.DomainObjectContainer;
+        import org.apache.isis.applib.ViewModel;
+        import org.apache.isis.applib.annotation.MemberOrder;
+        import org.apache.isis.applib.annotation.Render;
+        import org.apache.isis.applib.annotation.Render.Type;
+        import org.apache.isis.applib.annotation.Title;
+        import org.apache.isis.applib.services.bookmark.Bookmark;
+        import org.apache.isis.applib.services.bookmark.BookmarkService;
+        
+        import com.google.common.base.Function;
+        import com.google.common.base.Predicate;
+        import com.google.common.collect.Iterables;
+        import com.google.common.collect.Lists;
+        import com.google.common.io.BaseEncoding;
+        
+        public class SpeakerViewModel implements ViewModel {
+        
+            
+            // //////////////////////////////////////
+            // ViewModel impl
+            // //////////////////////////////////////
+        
+            @Override
+            public String viewModelMemento() {
+                Bookmark bookmark = bookmarkService.bookmarkFor(getSpeaker());
+                return encode(bookmark);
+            }
+        
+            @Override
+            public void viewModelInit(String memento) {
+                Bookmark bookmark = decode(memento);
+                setSpeaker((Speaker) bookmarkService.lookup(bookmark));
+            }
+        
+            static String encode(Bookmark bookmark) {
+                return BaseEncoding.base32().encode(bookmark.toString().getBytes());
+            }
+        
+            private static Bookmark decode(String memento) {
+                return new Bookmark(new String(BaseEncoding.base32().decode(memento)));
+            }
+            
+            
+            // //////////////////////////////////////
+            // speaker (property)
+            // //////////////////////////////////////
+        
+            private Speaker speaker;
+        
+            @javax.jdo.annotations.Column(allowsNull = "true")
+            @Title
+            @MemberOrder(sequence = "1")
+            public Speaker getSpeaker() {
+                return speaker;
+            }
+        
+            public void setSpeaker(final Speaker speaker) {
+                this.speaker = speaker;
+            }
+        
+        
+        
+            // //////////////////////////////////////
+            // numberOfSessions (property)
+            // //////////////////////////////////////
+        
+        
+            @MemberOrder(sequence = "2")
+            public int getNumberOfSessions() {
+                return getPresenting().size();
+            }
+        
+        
+            // //////////////////////////////////////
+            // presenting (collection)
+            // //////////////////////////////////////
+        
+            @Render(Type.EAGERLY)
+            @MemberOrder(sequence = "1")
+            public List<ConferenceSession> getPresenting() {
+                return container.allMatches(ConferenceSession.class, new Predicate<ConferenceSession>() {
+                    @Override
+                    public boolean apply(ConferenceSession input) {
+                        return input.getSpeaker() == getSpeaker();
+                    }
+                });
+            }
+        
+            // //////////////////////////////////////
+            // tags (collection)
+            // //////////////////////////////////////
+            
+            @Render(Type.EAGERLY)
+            @MemberOrder(sequence = "2")
+            public List<Tag> getTags() {
+                return Lists.newArrayList(
+                        Iterables.concat(
+                            Iterables.transform(
+                                getPresenting(), 
+                                new Function<ConferenceSession, Set<Tag>>(){
+                                    @Override
+                                    public Set<Tag> apply(ConferenceSession input) {
+                                        return input.getTags();
+                                    }
+                                })
+                        ));
+            }
+            
+        
+            
+            // //////////////////////////////////////
+            // Injected
+            // //////////////////////////////////////
+        
+            private DomainObjectContainer container;
+            public final void injectDomainObjectContainer(final DomainObjectContainer container) {
+                this.container = container;
+            }
+        
+            private BookmarkService bookmarkService;
+            public final void injectBookmarkService(final BookmarkService bookmarkService) {
+                this.bookmarkService = bookmarkService;
+            }  
+        }
+        
+        
+####Speakers
+
+* new action to return all speakers as home page (should really be in its own application service...)
+        
+        // //////////////////////////////////////
+        // view models
+        // //////////////////////////////////////
+    
+        @ActionSemantics(Of.SAFE)
+        @HomePage
+        public List<SpeakerViewModel> all() {
+            return Lists.newArrayList(Iterables.transform(listAll(), 
+                new Function<Speaker, SpeakerViewModel>() {
+                    @Override
+                    public SpeakerViewModel apply(Speaker input) {
+                        Bookmark bookmark = bookmarkService.bookmarkFor(input);
+                        return getContainer().newViewModelInstance(
+                                SpeakerViewModel.class, 
+                                SpeakerViewModel.encode(bookmark));
+                    }
+                })
+            );
+        }
+
+* and inject in the framework-provided `BookmarkService`    
+
+        // //////////////////////////////////////
+        // Injected
+        // //////////////////////////////////////
+        
+        private BookmarkService bookmarkService;
+    
+        public final void injectBookmarkService(final BookmarkService bookmarkService) {
+            this.bookmarkService = bookmarkService;
+        }
+
+#### ConferenceSessions
+
+* remove @HomePage annotation from the earlier `listAll()` action
+
+####ConferenceSessionsFixture
+
+* create(...) set up random speaker with each session
+
+        session.setSpeaker(randomSpeaker());
+
+* randomSpeaker() supporting method
+
+        private Speaker randomSpeaker() {
+            List<Speaker> all = speakers.listAll();
+            while(true) {
+                try {
+                    int selected = (int)(all.size() * Math.random());
+                    return all.get(selected);
+                } catch(Exception ex){}
+            }
+        }
+
+
+#### Demo
+* hit the home page
+* select a speaker view model
+
+#####Checkpoint:
+`git reset --hard checkpoint-220`
+
+
+
+## REST API
+
+####DEMO
+
+* [http://localhost:8080/restful](http://localhost:8080/restful)