This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tapestry-5-site.git
commit 1619244f887d6dbb33bd0c2a8631477336dc40dd Author: Volker Lamp <[email protected]> AuthorDate: Wed Dec 18 23:02:12 2024 +0100 Added Tutorial. Moved Getting Started from User Guide to main page. --- antora.yml | 3 +- modules/ROOT/images/newapp_Index.png | Bin 0 -> 155165 bytes modules/ROOT/nav.adoc | 13 +- modules/ROOT/pages/getting-started.adoc | 231 ++++++++++ .../pages/supported-environments-and-versions.adoc | 257 +++++++++++ modules/ROOT/pages/userguide.adoc | 3 + modules/tutorial/images/add-archetype-catalog.png | Bin 0 -> 121925 bytes modules/tutorial/images/address-v3.png | Bin 0 -> 26641 bytes modules/tutorial/images/address-v5.png | Bin 0 -> 30842 bytes modules/tutorial/images/address-v6.png | Bin 0 -> 38225 bytes modules/tutorial/images/address-v7.png | Bin 0 -> 22925 bytes modules/tutorial/images/address-v8.png | Bin 0 -> 21592 bytes modules/tutorial/images/app-error-1.png | Bin 0 -> 118491 bytes modules/tutorial/images/app-error-2.png | Bin 0 -> 155279 bytes modules/tutorial/images/app-live-reload.png | Bin 0 -> 95958 bytes modules/tutorial/images/console-startup.png | Bin 0 -> 56249 bytes modules/tutorial/images/create-address-initial.png | Bin 0 -> 24144 bytes .../tutorial/images/create-address-reordered.png | Bin 0 -> 24577 bytes modules/tutorial/images/gameover.png | Bin 0 -> 6290 bytes modules/tutorial/images/guess-1.png | Bin 0 -> 13282 bytes modules/tutorial/images/guess-no-target-prop.png | Bin 0 -> 55396 bytes modules/tutorial/images/guess-target-zero.png | Bin 0 -> 5508 bytes modules/tutorial/images/guess-target.png | Bin 0 -> 5534 bytes modules/tutorial/images/guess-template-missing.png | Bin 0 -> 52865 bytes modules/tutorial/images/hilo-1.png | Bin 0 -> 7951 bytes .../images/hilo-index-missing-action-error.png | Bin 0 -> 71644 bytes modules/tutorial/images/hmac-warning.png | Bin 0 -> 14513 bytes modules/tutorial/images/index-grid-v1.png | Bin 0 -> 38846 bytes modules/tutorial/images/run-configuration-jre.png | Bin 0 -> 66005 bytes modules/tutorial/images/run-configuration.png | Bin 0 -> 81765 bytes modules/tutorial/images/select-a-wizard.png | Bin 0 -> 45920 bytes modules/tutorial/images/select-archetype.png | Bin 0 -> 56858 bytes .../images/specify-archetype-parameters.png | Bin 0 -> 48923 bytes modules/tutorial/images/startpage.png | Bin 0 -> 73313 bytes .../tutorial/images/templates_and_parameters.png | Bin 0 -> 89564 bytes modules/tutorial/nav.adoc | 8 + .../pages/creating-the-skeleton-application.adoc | 115 +++++ .../pages/dependencies-tools-and-plugins.adoc | 35 ++ modules/tutorial/pages/exploring-the-project.adoc | 449 ++++++++++++++++++ .../implementing-the-hi-lo-guessing-game.adoc | 510 +++++++++++++++++++++ modules/tutorial/pages/index.adoc | 57 +++ .../using-beaneditform-to-create-user-forms.adoc | 415 +++++++++++++++++ .../pages/using-tapestry-with-hibernate.adoc | 262 +++++++++++ modules/tutorial/partials/diagrams/hilo-flow.puml | 10 + 44 files changed, 2366 insertions(+), 2 deletions(-) diff --git a/antora.yml b/antora.yml index 0d09fed..250aa7f 100644 --- a/antora.yml +++ b/antora.yml @@ -1,9 +1,10 @@ name: main -title: Tapestry +title: Home version: ~ start_page: ROOT:about.adoc nav: - modules/ROOT/nav.adoc asciidoc: attributes: + experimental: true # must be true to support User Interface Macros tapestry-version: 5.8.7 diff --git a/modules/ROOT/images/newapp_Index.png b/modules/ROOT/images/newapp_Index.png new file mode 100644 index 0000000..6091a61 Binary files /dev/null and b/modules/ROOT/images/newapp_Index.png differ diff --git a/modules/ROOT/nav.adoc b/modules/ROOT/nav.adoc index 72102f1..90ae27d 100644 --- a/modules/ROOT/nav.adoc +++ b/modules/ROOT/nav.adoc @@ -1,5 +1,16 @@ * xref:about.adoc[] * xref:download.adoc[] -* xref:documentation.adoc[] +* Documentation +** xref:getting-started.adoc[] +** Tutorial +*** xref:tutorial:index.adoc[] +*** xref:tutorial:dependencies-tools-and-plugins.adoc[] +*** xref:tutorial:creating-the-skeleton-application.adoc[] +*** xref:tutorial:exploring-the-project.adoc[] +*** xref:tutorial:implementing-the-hi-lo-guessing-game.adoc[] +*** xref:tutorial:using-beaneditform-to-create-user-forms.adoc[] +*** xref:tutorial:using-tapestry-with-hibernate.adoc[] +** xref:userguide.adoc[] * xref:community.adoc[] * xref:developers.adoc[] + diff --git a/modules/ROOT/pages/getting-started.adoc b/modules/ROOT/pages/getting-started.adoc new file mode 100644 index 0000000..80faf46 --- /dev/null +++ b/modules/ROOT/pages/getting-started.adoc @@ -0,0 +1,231 @@ += Getting Started + +Getting started with Tapestry is easy, and you have lots of ways to begin: watch a video, browse the source code of a working demo app, create a skeleton app using Maven, or step through the tutorial. + +== Watch a short video +For a fast-paced introduction, watch Mark W. Shead's http://blog.markshead.com/900/tapestry-5-10-minute-demo/[10 Minute Demo]. +This video shows how to set up a simple Tapestry application, complete with form validation, Hibernate-based persistence, and Ajax. +The video provides a preview of the development speed and productivity that experienced Tapestry users enjoy. + +== Play with a working demo app +You can also play with Tapestry via our live demonstration applications. +To start, have a look at the https://tapestry-app.apache.org/hotels/[Hotel Booking Demo]. +The http://github.com/bobharner/tapestry5-hotel-booking-5.4/[source code] is provided so you can download and play with it. +Also check out the https://tapestry-jumpstart.org/jumpstart/[Jumpstart demonstration site]. + +== Create your first Tapestry project +The easiest way to start a new app is to use https://maven.apache.org/[Apache Maven] to create your initial project; +Maven can use an archetype (a kind of project template) to create a bare-bones Tapestry application for you. + +Once you have Maven installed, execute the following command: + +---- +mvn archetype:generate -Dfilter=org.apache.tapestry:quickstart +---- + +(Alternatively, if you want to get an archetype for a not-yet-released version of Tapestry – most users don't – you can use the staging URI, https://repository.apache.org/content/repositories/staging ). +// TODO explain how to use the stagingn URI + +Maven will prompt you for the archetype to create ("Tapestry 5 Quickstart Project") and the exact version number (e.g., "5.8.7"). +It also asks you for a group id, an artifact id, and a version number. You can see this in the following transcript: + +---- +$ mvn archetype:generate -DarchetypeCatalog=http://tapestry.apache.org +[INFO] Scanning for projects... +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] Building Maven Stub Project (No POM) 1 +[INFO] ------------------------------------------------------------------------ +[INFO] +[INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom >>> +[INFO] +[INFO] <<< maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom <<< +[INFO] +[INFO] --- maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom --- +[INFO] Generating project in Interactive mode +[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0) +Choose archetype: +1: http://tapestry.apache.org -> org.apache.tapestry:quickstart (Tapestry 5 Quickstart Project) +2: http://tapestry.apache.org -> org.apache.tapestry:tapestry-archetype (Tapestry 5.4.5 Archetype) +Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 1 +Choose org.apache.tapestry:quickstart version: +1: 5.0.19 +2: 5.1.0.5 +3: 5.2.6 +4: 5.3.7 +5: 5.4.5 +6: 5.5.0 +7: 5.6.4 +8: 5.7.3 +9: 5.8.7 +Choose a number: 9: 9 +Define value for property 'groupId': : com.example +Define value for property 'artifactId': : newapp +Define value for property 'version': 1.0-SNAPSHOT: : +Define value for property 'package': com.example: : com.example.newapp +Confirm properties configuration: +groupId: com.example +artifactId: newapp +version: 1.0-SNAPSHOT +package: com.example.newapp + Y: : Y +[INFO] ---------------------------------------------------------------------------- +[INFO] Using following parameters for creating project from Archetype: quickstart:5.8.6 +[INFO] ---------------------------------------------------------------------------- +[INFO] Parameter: groupId, Value: com.example +[INFO] Parameter: artifactId, Value: newapp +[INFO] Parameter: version, Value: 1.0-SNAPSHOT +[INFO] Parameter: package, Value: com.example.newapp +[INFO] Parameter: packageInPathFormat, Value: com/example/newapp +[INFO] Parameter: package, Value: com.example.newapp +[INFO] Parameter: version, Value: 1.0-SNAPSHOT +[INFO] Parameter: groupId, Value: com.example +[INFO] Parameter: artifactId, Value: newapp + +[INFO] project created from Archetype in dir: /home/joeuser/junk/junk2/newapp +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 40.020s +[INFO] Finished at: Sun Apr 09 16:55:01 EDT 2020 +[INFO] Final Memory: 16M/303M +[INFO] ------------------------------------------------------------------------ +---- + +Maven will (after performing a number of one-time downloads) create a skeleton project ready to run. +Because we specified an artifactId of "newapp", the project is created in the `newapp` directory. +(Note: if you get "Unable to get resource" warnings at this stage, you may be behind a firewall which blocks outbound HTTP requests to Maven repositories.) + +To run the skeleton application, change to the `newapp` directory and execute the `mvn jetty:run` command to start the Jetty app server: + +---- +$ cd newapp +$ mvn jetty:run +[INFO] Scanning for projects... +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] Building newapp Tapestry 5 Application 1.0-SNAPSHOT +[INFO] ------------------------------------------------------------------------ + +... + +Application 'app' (version 1.0-SNAPSHOT-DEV) startup time: 329 ms to build IoC Registry, 919 ms overall. + ______ __ ____ +/_ __/__ ____ ___ ___ / /_______ __ / __/ + / / / _ `/ _ \/ -_|_-</ __/ __/ // / /__ \ +/_/ \_,_/ .__/\__/___/\__/_/ \_, / /____/ + /_/ /___/ 5.8.7 (development mode) + +[INFO] Started [email protected]:8080 +[INFO] Started Jetty Server +---- + +After some more one-time downloads you can open your browser to `http://localhost:8080/newapp` to see the application running: + +image::newapp_Index.png[Newapp Index page] + +The application consists of three pages sharing a common look and feel. +The initial page, `Index`, allows you to perform some basic operations. + +You can also load the newly-created project it into any IDE and start coding. +See the next section on where to find the different components of the application. + +== Exploring the generated project +The archetype creates the following files: + +---- +newapp/ +├── build.gradle +├── gradle +│ └── wrapper +│ ├── gradle-wrapper.jar +│ └── gradle-wrapper.properties +├── gradlew +├── gradlew.bat +├── pom.xml +└── src + ├── main + │ ├── java + │ │ └── com + │ │ └── example + │ │ └── newapp + │ │ ├── components + │ │ │ └── Layout.java + │ │ ├── pages + │ │ │ ├── About.java + │ │ │ ├── Contact.java + │ │ │ ├── Error404.java + │ │ │ ├── Index.java + │ │ │ └── Login.java + │ │ └── services + │ │ ├── AppModule.java + │ │ ├── DevelopmentModule.java + │ │ └── QaModule.java + │ ├── resources + │ │ ├── com + │ │ │ └── example + │ │ │ └── newapp + │ │ │ ├── components + │ │ │ │ └── Layout.tml + │ │ │ ├── logback.xml + │ │ │ └── pages + │ │ │ ├── About.tml + │ │ │ ├── Contact.tml + │ │ │ ├── Error404.tml + │ │ │ ├── Index.properties + │ │ │ ├── Index.tml + │ │ │ └── Login.tml + │ │ └── log4j.properties + │ └── webapp + │ ├── WEB-INF + │ │ ├── app.properties + │ │ └── web.xml + │ ├── favicon.ico + │ ├── images + │ │ └── tapestry.png + │ └── mybootstrap + │ ├── css + │ │ ├── bootstrap-responsive.css + │ │ └── bootstrap.css + │ ├── img + │ │ ├── glyphicons-halflings-white.png + │ │ └── glyphicons-halflings.png + │ └── js + │ └── bootstrap.js + ├── site + │ ├── apt + │ │ └── index.apt + │ └── site.xml + └── test + ├── conf + │ ├── testng.xml + │ └── webdefault.xml + ├── java + │ └── PLACEHOLDER + └── resources + └── PLACEHOLDER +30 directories, 39 files +---- + +A Tapestry application is composed of pages, each page consisting of one template file and one Java class. + +Tapestry page templates have the `.tml` extension and are found within `src/main/*resources*/` under the app's `pages` package (`src/main/resources/com/example/newapp/*pages*`, in this example). +Templates are essentially HTML with some special markup to reference properties in the corresponding Java class and to reference ready-made or custom components. + +Similarly, Tapestry page classes are found in within the `src/main/*java*` under the app's `pages` package (`src/main/java/com/example/newapp/*pages*`, in this example) and their name matches their template name (`Index.tml` -> `Index.java`). + +In the skeleton project, most of the HTML is not found on the pages themselves but in a Layout component which acts as a global template for the whole site. +Java classes for components live in `src/main/*java*/com/example/newapp/*components*` and component templates go in `src/main/*resources*/com/example/newapp/*components*`. + +The archetype includes a few optional extras: + +* The bundled version of the Bootstrap CSS library has a per-project override. +You can see the files in `src/webapp/context/mybootstrap`, and the overrides to enable that in `AppModule.java`. +* By default, Tapestry uses http://prototypejs.org/[Prototype] as its client-side library, but the archetype overrides this to https://jquery.org/[jQuery], which is preferred for new projects. +* The archetype adds a simple filter that shows the timing of each request. +* The archetype sets up not just for builds with Maven, but also via http://gradle.org/[Gradle]. + +== What's next? +To deepen your understanding, step through the xref:tutorial:index.adoc[Tapestry Tutorial], which goes into much more detail about setting up your project as well as loading it into Eclipse... then continues on to teach you more about Tapestry. + +Be sure to read about the core xref:principles.adoc[Tapestry Principles], and browse the xref:userguide::index.adoc[User Guide]. diff --git a/modules/ROOT/pages/supported-environments-and-versions.adoc b/modules/ROOT/pages/supported-environments-and-versions.adoc new file mode 100644 index 0000000..b34b6f4 --- /dev/null +++ b/modules/ROOT/pages/supported-environments-and-versions.adoc @@ -0,0 +1,257 @@ += Supported Environments and Versions + +Tapestry is compatible with a wide range of app servers, Java versions, and open source libraries. Not all combinations are known to work, however. + +Note: blanks in the support matrix tables below do NOT indicate incompatibility. They are just documentation gaps. + +If you know of any other known compatibilities or incompatibilities, please add a comment on the http://mail-archives.apache.org/mod_mbox/tapestry-users/[Tapestry Users mailing list]. + +== Java & Servlet API Version + +[%autowidth] +|=== +|Tapestry | 5.8.4+ | 5.8.1 - 5.8-3 |5.7 |5.6 |5.5 |5.4 |5.3.8 |5.3.0 - 5.3.7 |5.0 - 5.2 + +|Java JRE +|8-21 +|8-17 +|8-14 +|8-14 +|8-12 +|7,8 +|6-8 footnote:[For using Tapestry 5.3.8 with Java 8 see xref:userguide::release-notes-5.3.adoc#_release_notes_5_3_8[Release Notes 5.3.8].] +|6,7 +|5,6 + +|Servlet API +|3.0+ +|3.0+ +|3.0+ +|3.0+ +|3.0+ +|2.5+ +|2.5+ +|2.5+ +|2.4+ +|=== + +== App Servers +[%autowidth] +|=== +|Tapestry |5.5+ |5.4 |5.3.8 |5.3.0 - 5.3.7 |5.2 |5.1 | 5.0.10 |5.0.8|5.0 + +|Apache Tomcat +|6+ +| +| +|6+ +| +| +| +| +| + +|Jetty +|9 +|7-9 +|6-9 +|6-8 +|6-8 +|6-8 +|6-8 +|6-8 +|6-8 + +|Glassfish +| +| +| +| +| +|2.1 +| +| +| + +|JBoss EAP +| +|4.2.3 +| +| +| +| +| +| +|4+ +|=== + +== Libraries + +These are the library versions known to work (and, in some cases, bundled with Tapestry). Unless otherwise noted, adjacent versions will often work fine as well. + +[%autowidth] +|=== +|Tapestry |5.7.3+ |5.5+ |5.4.1 |5.4 |5.3.8 |5.3.7 | 5.3.3-5.3.6 |5.3.2|5.3 - 5.3.1 |5.2.1 - 5.2.0 |5.1 |5.0.16 |5.0.10 |5.0.8 |5.0 + +|Hibernate +|5.4.32.Final +|5.1.0.Final +| +|? - 4.3.6 +| +| +| +| +| +|3.5.4 - 3.6.0 +| +|3.3.0+ +| +| +| + +|Spring +| +| +| +| +| +| +| +|3.1.0 +| +| +| +| +| +| +| + +|jQuery.js +| +| +|1.12.1 +| +| +| +| +| +| +| +| +| +| +| +| + +|Prototype.js +| +| +| +|1.7.1 +| +|1.7 +|1.7 +|1.7 +|1.7 +|1.6.1 +|1.6.0.3 +|1.6.0.3 +|1.6.0.2 +|1.6 +|1.6 + +|Scriptaculous +| +| +| +| +|1.9 +|1.9 +|1.9 +|1.9 +|1.9 +|1.8.2 +|1.8.2 +| +| +| +|1.8.0 + +|Bootstrap CSS +| +|4.3.1, 3.3.6 +|3.3.6 +|3.0.2 +| +| +| +| +| +| +| +| +| +| +| + +|Moment.js +| +| +|2.12.0 +| +| +| +| +| +| +| +| +| +| +| +| + +|Less4J +| +| +| +|1.2.1 - 1.9 +| +| +| +| +| +| +| +| +| +| +| + +|Underscore.js +| +| +| +| +| +| +|1.3.3 +|1.1.7 +|1.1.7 +| +| +| +| +| +| +|=== + +[IMPORTANT] +==== +*Java 9+ Dependency Deprecations.* +With the introduction of the http://openjdk.java.net/projects/jigsaw/[Java module system] in version 9, various Java EE dependencies were declared deprecated, and removed entirely in version 11. +This might lead to `java.lang.NoClassDefFoundError` exceptions for `javax`-package classes, like missing `javax.xml.bind.JAXBException` for `tapstry-hibernate`. +Until all related libraries and frameworks add the now missing dependencies explicitly, you might have to re-add them yourself, if no other dependency is pulling them into your project. +See https://stackoverflow.com/questions/43574426/how-to-resolve-java-lang-noclassdeffounderror-javax-xml-bind-jaxbexception and https://crunchify.com/java-11-and-javax-xml-bind-jaxbcontext/ for more information about the deprecation and removal of the Java EE dependencies, and how to remedy. +==== diff --git a/modules/ROOT/pages/userguide.adoc b/modules/ROOT/pages/userguide.adoc new file mode 100644 index 0000000..d31daf8 --- /dev/null +++ b/modules/ROOT/pages/userguide.adoc @@ -0,0 +1,3 @@ += User Guide + +The versioned User Guide is located in a dedicated section of this website available through the navigation or this xref:userguide::index.adoc[direct link to the User Guide]. diff --git a/modules/tutorial/images/add-archetype-catalog.png b/modules/tutorial/images/add-archetype-catalog.png new file mode 100644 index 0000000..33af39c Binary files /dev/null and b/modules/tutorial/images/add-archetype-catalog.png differ diff --git a/modules/tutorial/images/address-v3.png b/modules/tutorial/images/address-v3.png new file mode 100644 index 0000000..eddff6d Binary files /dev/null and b/modules/tutorial/images/address-v3.png differ diff --git a/modules/tutorial/images/address-v5.png b/modules/tutorial/images/address-v5.png new file mode 100644 index 0000000..b604da1 Binary files /dev/null and b/modules/tutorial/images/address-v5.png differ diff --git a/modules/tutorial/images/address-v6.png b/modules/tutorial/images/address-v6.png new file mode 100644 index 0000000..ebfe511 Binary files /dev/null and b/modules/tutorial/images/address-v6.png differ diff --git a/modules/tutorial/images/address-v7.png b/modules/tutorial/images/address-v7.png new file mode 100644 index 0000000..a43a73c Binary files /dev/null and b/modules/tutorial/images/address-v7.png differ diff --git a/modules/tutorial/images/address-v8.png b/modules/tutorial/images/address-v8.png new file mode 100644 index 0000000..47afaa4 Binary files /dev/null and b/modules/tutorial/images/address-v8.png differ diff --git a/modules/tutorial/images/app-error-1.png b/modules/tutorial/images/app-error-1.png new file mode 100644 index 0000000..d305aad Binary files /dev/null and b/modules/tutorial/images/app-error-1.png differ diff --git a/modules/tutorial/images/app-error-2.png b/modules/tutorial/images/app-error-2.png new file mode 100644 index 0000000..e228db6 Binary files /dev/null and b/modules/tutorial/images/app-error-2.png differ diff --git a/modules/tutorial/images/app-live-reload.png b/modules/tutorial/images/app-live-reload.png new file mode 100644 index 0000000..9708a04 Binary files /dev/null and b/modules/tutorial/images/app-live-reload.png differ diff --git a/modules/tutorial/images/console-startup.png b/modules/tutorial/images/console-startup.png new file mode 100644 index 0000000..45e6133 Binary files /dev/null and b/modules/tutorial/images/console-startup.png differ diff --git a/modules/tutorial/images/create-address-initial.png b/modules/tutorial/images/create-address-initial.png new file mode 100644 index 0000000..da84158 Binary files /dev/null and b/modules/tutorial/images/create-address-initial.png differ diff --git a/modules/tutorial/images/create-address-reordered.png b/modules/tutorial/images/create-address-reordered.png new file mode 100644 index 0000000..de0f3c8 Binary files /dev/null and b/modules/tutorial/images/create-address-reordered.png differ diff --git a/modules/tutorial/images/gameover.png b/modules/tutorial/images/gameover.png new file mode 100644 index 0000000..1b6dd4d Binary files /dev/null and b/modules/tutorial/images/gameover.png differ diff --git a/modules/tutorial/images/guess-1.png b/modules/tutorial/images/guess-1.png new file mode 100644 index 0000000..cb3bfc8 Binary files /dev/null and b/modules/tutorial/images/guess-1.png differ diff --git a/modules/tutorial/images/guess-no-target-prop.png b/modules/tutorial/images/guess-no-target-prop.png new file mode 100644 index 0000000..ab555bf Binary files /dev/null and b/modules/tutorial/images/guess-no-target-prop.png differ diff --git a/modules/tutorial/images/guess-target-zero.png b/modules/tutorial/images/guess-target-zero.png new file mode 100644 index 0000000..cfd5b70 Binary files /dev/null and b/modules/tutorial/images/guess-target-zero.png differ diff --git a/modules/tutorial/images/guess-target.png b/modules/tutorial/images/guess-target.png new file mode 100644 index 0000000..ef5c2cc Binary files /dev/null and b/modules/tutorial/images/guess-target.png differ diff --git a/modules/tutorial/images/guess-template-missing.png b/modules/tutorial/images/guess-template-missing.png new file mode 100644 index 0000000..7e7bfea Binary files /dev/null and b/modules/tutorial/images/guess-template-missing.png differ diff --git a/modules/tutorial/images/hilo-1.png b/modules/tutorial/images/hilo-1.png new file mode 100644 index 0000000..073cf81 Binary files /dev/null and b/modules/tutorial/images/hilo-1.png differ diff --git a/modules/tutorial/images/hilo-index-missing-action-error.png b/modules/tutorial/images/hilo-index-missing-action-error.png new file mode 100644 index 0000000..cf09164 Binary files /dev/null and b/modules/tutorial/images/hilo-index-missing-action-error.png differ diff --git a/modules/tutorial/images/hmac-warning.png b/modules/tutorial/images/hmac-warning.png new file mode 100644 index 0000000..458a512 Binary files /dev/null and b/modules/tutorial/images/hmac-warning.png differ diff --git a/modules/tutorial/images/index-grid-v1.png b/modules/tutorial/images/index-grid-v1.png new file mode 100644 index 0000000..97a0c7f Binary files /dev/null and b/modules/tutorial/images/index-grid-v1.png differ diff --git a/modules/tutorial/images/run-configuration-jre.png b/modules/tutorial/images/run-configuration-jre.png new file mode 100644 index 0000000..97e314c Binary files /dev/null and b/modules/tutorial/images/run-configuration-jre.png differ diff --git a/modules/tutorial/images/run-configuration.png b/modules/tutorial/images/run-configuration.png new file mode 100644 index 0000000..2f2c899 Binary files /dev/null and b/modules/tutorial/images/run-configuration.png differ diff --git a/modules/tutorial/images/select-a-wizard.png b/modules/tutorial/images/select-a-wizard.png new file mode 100644 index 0000000..079bd81 Binary files /dev/null and b/modules/tutorial/images/select-a-wizard.png differ diff --git a/modules/tutorial/images/select-archetype.png b/modules/tutorial/images/select-archetype.png new file mode 100644 index 0000000..749fd72 Binary files /dev/null and b/modules/tutorial/images/select-archetype.png differ diff --git a/modules/tutorial/images/specify-archetype-parameters.png b/modules/tutorial/images/specify-archetype-parameters.png new file mode 100644 index 0000000..92d9f78 Binary files /dev/null and b/modules/tutorial/images/specify-archetype-parameters.png differ diff --git a/modules/tutorial/images/startpage.png b/modules/tutorial/images/startpage.png new file mode 100644 index 0000000..49164b6 Binary files /dev/null and b/modules/tutorial/images/startpage.png differ diff --git a/modules/tutorial/images/templates_and_parameters.png b/modules/tutorial/images/templates_and_parameters.png new file mode 100644 index 0000000..8fc6edf Binary files /dev/null and b/modules/tutorial/images/templates_and_parameters.png differ diff --git a/modules/tutorial/nav.adoc b/modules/tutorial/nav.adoc new file mode 100644 index 0000000..895fa66 --- /dev/null +++ b/modules/tutorial/nav.adoc @@ -0,0 +1,8 @@ +* xref:index.adoc[] +* xref:dependencies-tools-and-plugins.adoc[] +* xref:creating-the-skeleton-application.adoc[] +* xref:exploring-the-project.adoc[] +* xref:implementing-the-hi-lo-guessing-game.adoc[] +* xref:using-beaneditform-to-create-user-forms.adoc[] +* xref:using-tapestry-with-hibernate.adoc[] + diff --git a/modules/tutorial/pages/creating-the-skeleton-application.adoc b/modules/tutorial/pages/creating-the-skeleton-application.adoc new file mode 100644 index 0000000..3d902c0 --- /dev/null +++ b/modules/tutorial/pages/creating-the-skeleton-application.adoc @@ -0,0 +1,115 @@ += Creating the Skeleton Application + +First, let's create an empty application. Tapestry provides a Maven *archetype* (a project template) to make this easy. + +For the tutorial, we're using a fresh install of Eclipse and an empty workspace at `/users/joeuser/workspace`. +You may need to adjust a few things for other operating systems or local paths. + +== Using the Quickstart Archetype +From Eclipse, we'll use a Maven archetype to create a skeleton Tapestry project. + +=== Maven Behind a Firewall +If you are behind a firewall/proxy, before performing any Maven downloads, you may need to configure your proxy settings in your Maven `settings.xml` file (typically in the `.m2` subdirectory of your home directory, `~/.m2` or `C:\users\joeuser\.m2`). +Here is an example (but check with your network administrator for the names and numbers you should use here). + +.setting.xml +[source,xml] +---- +<settings> + <proxies> + <proxy> + <active>true</active> + <protocol>http</protocol> + <host>myProxyServer.com</host> + <port>3128</port> + <username>joeuser</username> + <password>myPassword</password> + <nonProxyHosts></nonProxyHosts> + </proxy> + </proxies> + <localRepository>C:/Users/joeuser/.m2/repository</localRepository> <1> +</settings> +---- +<1> Of course, adjust the localRepository element to match the correct path for your computer. + +=== Create Project +Okay, let's get started creating our new project. + +[NOTE] +==== +The instructions below use Eclipse's New Project wizard to create the project from a Maven archetype. +If you'd rather use the `mvn` command line, see the xref:ROOT:getting-started.adoc#_create_your_first_tapestry_project[Getting Started] instructions, then skip to xref:creating-the-skeleton-application.adoc[] page. +==== + +In Eclipse, go to menu:File[New > Project... > Maven > Maven Project] + +image::select-a-wizard.png[] + +Then click btn:[Next], btn:[Next] (again), and then on the "Select an Archetype" page click the btn:[Configure] button on the "Catalog" line. +The "Archetype" preferences dialog should appear. +Click the btn:[Add Remote Catalog...] button, as shown below: + +image::add-archetype-catalog.png[] + +As shown above, enter `http://tapestry.apache.org` in the "Catalog File" field, and "Apache Tapestry" in the Description field. + +Click btn:[OK], then btn:[OK] again. + +On the "Select an Archetype" dialog (shown below), select the newly-added Apache Tapestry catalog, then select the "quickstart" artifact from the list and click btn:[Next]. + +image::select-archetype.png[] + +NOTE: Screenshots in this tutorial may show different (either newer or older) versions of Tapestry than you may see. + +Fill in the Group Id, Artifact Id, Version and Package as follows: + +image::specify-archetype-parameters.png[] + +then click btn:[Finish]. + +[NOTE] +==== +The first time you use Maven, project creation may take a while as Maven downloads a large number of JAR dependencies for Maven, Jetty and Tapestry. +These downloaded files are cached locally and will not need to be downloaded again, but you do have to be patient on first use. +==== + +After Maven finishes, you'll see a new directory, `tutorial1`, in your Package Explorer view in Eclipse. + +=== Running the Application using Jetty +One of the first things you can do is use Maven to run Jetty directly. + +Right-click on the `tutorial1` project in your Package Explorer view and select menu:Run As[Maven Build... >], enter a Goal of `jetty:run`. +This creates a "Run Configuration" named "tutorial1" that we'll use throughout this tutorial to start the app: + +image::run-configuration.png[] + +Tapestry runs best with a couple of additional options; click the "JRE" tab and enter the following VM Arguments: + +`-Xmx600m` + +`-Dtapestry.execution-mode=development` + +image::run-configuration-jre.png[] + +Finally, click btn:[Run]. + +Again, the first time, there's a dizzying number of downloads, but before you know it, the Jetty servlet container is up and running. + +image::console-startup.png[] + +_Note the red square icon above. Later on you'll use that icon to stop Jetty before restarting the app._ + +You can now open a web browser to http://localhost:8080/tutorial1/ to see the running application: + +image::startpage.png[] + +NOTE: Your screen may look very different depending on the version of Tapestry you are using! + +The date and time in the middle of the page shows that this is a live application. + +This is a complete little web app. It doesn't do much, but it demonstrate how to create a number of pages sharing a common layout, and demonstrates some simple navigation and link handling. +You can see that it has several different pages that share a common layout. +(_Layout_ is a loose term meaning common look and feel and navigation across many or all of the pages of an application. +Often an application will include a Layout component to provide that commonness.) + +Next: xref:exploring-the-project.adoc[] diff --git a/modules/tutorial/pages/dependencies-tools-and-plugins.adoc b/modules/tutorial/pages/dependencies-tools-and-plugins.adoc new file mode 100644 index 0000000..c769503 --- /dev/null +++ b/modules/tutorial/pages/dependencies-tools-and-plugins.adoc @@ -0,0 +1,35 @@ += Dependencies, Tools and Plugins +As much as we would like to dive right into the code, we must first set up your development environment. Likely you have some of these, or reasonable alternatives, already on your development machine. + +== JDK 1.8 or Newer +This tutorial uses the latest released version of Tapestry, which requires Java Development Kit (JDK) version 1.8 or newer. +(But see xref:ROOT:supported-environments-and-versions.adoc[] if you want to use an older version of JDK or Tapestry.) + +== Eclipse IDE +For this tutorial we'll assume you're using Eclipse as your Integrated Development Environment (IDE). +Eclipse is a popular IDE, but feel free to adapt these instructions to IntelliJ, NetBeans, or any other. + +Eclipse comes in various flavors, and includes a reasonable XML editor built-in. +It can be downloaded from the eclipse.org web site. +We recommend the latest version of Eclipse IDE for Java Developers (but anything from version 3.7 onward should work fine). + +== Apache Maven 3 +Maven is a software build tool with the ability to automatically download project dependencies (such as the Tapestry JAR files, and the JAR files that Tapestry itself depends on) from one of several central repositories. + +Maven is not essential for using Tapestry, but is especially helpful when performing the initial set-up of a Tapestry application. +Feel free to substitute Gradle or Ivy if you prefer. + +Eclipse comes with a Maven plugin, M2Eclipse (also known as m2e) with an embedded version of Maven. +We'll use that here for simplicity's sake. +Alternatively, you could install Maven from http://maven.apache.org/download.html and use it from the command line (`mvn`). + +== Jetty +For simplicity, this tutorial uses Jetty, a lightweight open source web server and servlet container available from the Eclipse Foundation. Of course, you could use pretty much any other Java servlet container (Tomcat, Glassfish, JBoss, etc), but the instructions that follow assume Jetty. + +We will use Maven to download and run Jetty automatically, so you will NOT have to download it for this tutorial. (Alternatively, you could download and install the RunJettyRun Eclipse plugin from the Eclipse Marketplace.) + +== Tapestry +Tapestry is available as a set of JAR files, but you will not have to download them yourself. +As with Jetty, Maven will take care of downloading Tapestry and its dependencies. + +Next: xref:creating-the-skeleton-application.adoc[] diff --git a/modules/tutorial/pages/exploring-the-project.adoc b/modules/tutorial/pages/exploring-the-project.adoc new file mode 100644 index 0000000..1ce01dc --- /dev/null +++ b/modules/tutorial/pages/exploring-the-project.adoc @@ -0,0 +1,449 @@ += Exploring the Project + +The layout of the project follows the sensible standards promoted by Maven: + +* Java source files under `src/main/java` +* Web application files under `src/main/webapp` (including `src/main/webapp/WEB-INF`) +* Java test sources under `src/test/java` +* Non-code resources (including Tapestry page and component templates) under `src/main/resources` and `src/test/resources` + +Let's look at what Maven has created from the archetype, starting with the web.xml configuration file: + +.src/main/webapp/WEB-INF/web.xml +[source,xml] +---- +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE web-app + PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" + "http://java.sun.com/dtd/web-app_2_3.dtd"> +<web-app> + <display-name>tutorial1 Tapestry 5 Application</display-name> + <context-param> + <!-- The only significant configuration for Tapestry 5, this informs Tapestry +of where to look for pages, components and mixins. --> + <param-name>tapestry.app-package</param-name> + <param-value>com.example.tutorial1</param-value> + </context-param> + <!-- + Specify some additional Modules for two different execution + modes: development and qa. + Remember that the default execution mode is production + --> + <context-param> + <param-name>tapestry.development-modules</param-name> + <param-value> + com.example.tutorial1.services.DevelopmentModule + </param-value> + </context-param> + <context-param> + <param-name>tapestry.qa-modules</param-name> + <param-value> + com.example.tutorial1.services.QaModule + </param-value> + </context-param> + <filter> + <filter-name>app</filter-name> + <filter-class>org.apache.tapestry5.TapestryFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>app</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> +</web-app> +---- + +This is short and sweet: you can see that the package name you provided earlier shows up as the `tapestry.app-package` context parameter; the TapestryFilter instance will use this information to locate the Java classes for pages and components. + +Tapestry operates as a _servlet filter_ rather than as a traditional servlet. +In this way, Tapestry has a chance to intercept all incoming requests, to determine which ones apply to Tapestry pages (or other resources). +The net effect is that you don't have to maintain any additional configuration for Tapestry to operate, regardless of how many pages or components you add to your application. + +Much of the rest of web.xml is configuration to match Tapestry execution modes against module classes. +An execution mode defines how the application is being run: the default execution mode is "production", but the `web.xml` defines two additional modes: "development" and "qa" (for "Quality Assurance"). +The module classes indicated will be loaded for those execution modes, and can change the configuration of the application is various ways. +We'll come back to execution modes and module classes later in the tutorial. + +Tapestry pages minimally consist of an ordinary Java class plus a component template file. + +In the root of your web application, a page named "Index" will be used for any request that specifies no additional path after the context name. + +== Index Java Class +Tapestry has very specific rules for where page classes go. +Tapestry adds a sub-package, `pages`, to the root application package (`com.example.tutorial1`); the Java classes for pages goes there. +Thus the full Java class name is `com.example.tutorial1.pages.Index`. + +.src/main/java/com/example/tutorial/pages/Index.java +[source,java] +---- +package com.example.tutorial1.pages; + +import org.apache.tapestry5.Block; +import org.apache.tapestry5.EventContext; +import org.apache.tapestry5.SymbolConstants; +import org.apache.tapestry5.annotations.InjectPage; +import org.apache.tapestry5.annotations.Log; +import org.apache.tapestry5.annotations.Property; +import org.apache.tapestry5.ioc.annotations.Inject; +import org.apache.tapestry5.ioc.annotations.Symbol; +import org.apache.tapestry5.services.HttpError; +import org.apache.tapestry5.services.ajax.AjaxResponseRenderer; +import org.slf4j.Logger; + +import java.util.Date; + +/** + * Start page of application tutorial1. + */ +public class Index +{ + @Inject + private Logger logger; + + @Inject + private AjaxResponseRenderer ajaxResponseRenderer; + + @Property + @Inject + @Symbol(SymbolConstants.TAPESTRY_VERSION) + private String tapestryVersion; + + @InjectPage + private About about; + + @Inject + private Block block; + + // Handle call with an unwanted context + Object onActivate(EventContext eventContext) + { + return eventContext.getCount() > 0 ? new HttpError(404, "Resource not found") : null; + } + + Object onActionFromLearnMore() + { + about.setLearn("LearnMore"); + + return about; + } + + @Log + void onComplete() + { + logger.info("Complete call on Index page"); + } + + @Log + void onAjax() + { + logger.info("Ajax call on Index page"); + + ajaxResponseRenderer.addRender("middlezone", block); + } + + public Date getCurrentTime() + { + return new Date(); + } +} +---- + +There's a bit going on in this listing, as the Index page attempts to demonstrate a bunch of different ideas in Tapestry. +Even so, the class is essentially pretty simple: +Tapestry pages and components have no base classes to extend, no interfaces to implement, and are just a very pure POJO (Plain Old Java Object) ... with some special naming conventions and annotations for fields and methods. + +You do have to meet the Tapestry framework partway: + +* You need to put the Java class in the expected package, here `com.example.tutorial1.pages`. +* The class must be public. +* You need to make sure there's a public, no-arguments constructor (here, the Java compiler has silently provided one for us). +* All non-static fields must be *private*. + +As we saw when running the application, the page displays the current date and time, as well as a couple of extra links. +The `currentTime` property is where that value comes from; shortly we'll see how that value is referenced in the template, so it can be extracted from the page and output. + +Tapestry always matches a page class to a template; neither is functional without the other. +In fact, components within a page are treated the same way (except that components do not always have templates). + +You will often hear about the http://en.wikipedia.org/wiki/Model_view_controller[Model-View-Controller pattern (MVC)]. +In Tapestry, the page class acts as both the Model (the source of data) and the controller (the logic that responds to user interaction). +The template is the View in MVC. +As a model, the page exposes JavaBeans properties that can be referenced in the template. + +Let's look at how the component template builds on the Java class to provide the full user interface. + +== Component Template +Tapestry pages are the combination of a https://en.wikipedia.org/wiki/Plain_old_Java_object[POJO] Java class with a Tapestry component template. +The template has the same name as the Java class, but has the extension `.tml`. +Since the Java class here is `com.example.tutorial.pages.Index`, the template file will be located at `src/main/resources/com/example/tutorial/pages/Index.tml`. +Ultimately, both the Java class and the component template file will be stored in the same folder within the deployed WAR file. + +Tapestry component templates are well-formed XML documents. +This means that you can use any available XML editor. +Templates may even have a DOCTYPE or an XML schema to validate the structure of the template page. + +[NOTE] +==== +Tapestry parses component templates using a non-validating parser; it only checks for well-formedness: proper syntax, balanced elements, attribute values are quoted, and so forth. +It is reasonable for your build process to perform some kind of template validation, but Tapestry accepts the template as-is, as long as it parses cleanly. +==== + +For the most part, a Tapestry component template looks like ordinary XHTML: + +.src/main/resources/com/example/tutorial1/pages/Index.tml +[source,xml] +---- +<html t:type="layout" title="tutorial1 Index" + xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd" + xmlns:p="tapestry:parameter"> + + <div class="hero-unit"> + <p> + <img src="${asset:context:images/tapestry.png}" + alt="${message:greeting}" title="${message:greeting}"/> + </p> + <h3>${message:greeting}</h3> + <p>The current time is: <strong>${currentTime}</strong></p> + <p> + This is a template for a simple marketing or informational website. It includes a large callout called + the hero unit and three supporting pieces of content. Use it as a starting point to create something + more unique. + </p> + <p><t:actionlink t:id="learnmore" class="btn btn-primary btn-large">Learn more »</t:actionlink></p> + </div> + + <div class="row"> + <div class="span4"> + <h2>Normal link</h2> + <p>Clink the bottom link and the page refresh with event <code>complete</code></p> + <p><t:eventlink event="complete" class="btn btn-default">Complete»</t:eventlink></p> + </div> + <t:zone t:id="middlezone" class="span4"> + + </t:zone> + <div class="span4"> + <h2>Ajax link</h2> + <p>Click the bottom link to update just the middle column with Ajax call with event <code>ajax</code></p> + <p><t:eventlink event="ajax" zone="middlezone" class="btn btn-default">Ajax»</t:eventlink></p> + </div> + </div> + + <t:block t:id="block"> + <h2>Ajax updated</h2> + <p>I'v been updated through AJAX call</p> + <p>The current time is: <strong>${currentTime}</strong></p> + </t:block> + +</html> +---- + +[IMPORTANT] +==== +You do have to name your component template file, Index.tml, with the exact same case as the component class name, Index. +If you get the case wrong, it may work on some operating systems (such as Mac OS X, Windows) and not on others (Linux, and most others). +This can be really vexing, as it is common to develop on Windows and deploy on Linux or Solaris, so be careful about case in this one area. +==== + +The goal in Tapestry is for component templates, such as `Index.tml`, to look as much as possible like ordinary, static HTML files. +(By static, we mean unchanging, as opposed to a dynamically generated Tapestry page.) + +In fact, the expectation is that in many cases, the templates will start as static HTML files, created by a web developer, and then be instrumented to act as live Tapestry pages. + +Tapestry hides non-standard elements and attributes inside XML namespaces. By convention, the prefix `t:` is used for the primary namespace, but that is not a requirement, any prefix you want to use is fine. + +This short template demonstrates quite a few features of Tapestry. + +[NOTE] +==== +Part of the concept of the quickstart archetype is to demonstrate a bunch of different features, approaches, and common patterns used in Tapestry. +So yes, we're hitting you with a lot all at once. +==== + +=== Namespaces + +First of all, there are two XML namespaces commonly defined: + +.src/main/resources/com/example/tutorial1/pages/Index.tml (partial) +[source,xml] +---- +<html t:type="layout" title="tutorial1 Index" + xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd" <1> + xmlns:p="tapestry:parameter"> <2> +---- +<1> :t namespace: used to identify Tapestry-specific elements and attributes. Although there is an XSD (that is, a XML schema definition), it is incomplete (for reasons explained shortly) +<2> :p namespace: A way of marking a chunk of the template as a parameter passed into another component. We'll expand on that shortly. + +A Tapestry component template consists mostly of standard XHTML that will pass down to the client web browser unchanged. +The dynamic aspects of the template are represented by _components_ and _expansions_. + +=== Expansions in Templates + +Let's start with expansions. +Expansions are an easy way of including some dynamic output when rendering the page. +By default, an expansion refers to a JavaBeans property of the page: + +[source.xml] +---- +<p>The current time is: ${currentTime}</p> +---- + +The value inside the curly braces is a _property expression_. +Tapestry uses its own property expression language that is expressive, fast, and type-safe. + +TIP: Tapestry does NOT use reflection to implement property expressions. + +More advanced property expressions can traverse multiple properties (for example, `user.address.city`), or even invoke public methods. +Here the expansion simply reads the `currentTime` property of the page. + +Tapestry follows the rules defined by Sun's JavaBeans specification: a property name of currentTime maps to two methods: `getCurrentTime()` and `setCurrentTime()`. +If you omit one or the other of these methods, the property is either read only (as here), or write only. +(Keep in mind that as far as JavaBeans properties go, it's the methods that count; the names of the instance variables, or even whether they exist, is immaterial.) + +Tapestry does go one step further: it ignores case when matching properties inside the expansion to properties of the page. +In the template we could say `$\{currenttime}` or `$\{CurrentTime}` or any variation, and Tapestry will still invoke the `getCurrentTime()` method. + +Note that in Tapestry it is not necessary to configure what object holds the `currentTime` property. +A template and a page are always used in combination with each other; expressions are always rooted in the page instance, in this case, an instance of the `Index` class. + +The `Index.tml` template includes a second expansion: + +[source,xml] +---- +<p>${message:greeting}</p> +---- + +Here `greeting` is not a property of the page; its actually a localized message key. +Every Tapestry page and component is allowed to have its own message catalog. +(There's also a global message catalog, which we'll describe later.) + +.src/main/resources/com/example/tutorial/pages/Index.properties +[source] +---- +greeting=Welcome to Tapestry 5! We hope that this project template will get you going in style. +---- + +Message catalogs are useful for storing repeating strings outside of code or templates, though their primary purpose is related to localization of the application (which will be described in more detail in a later chapter). +Messages that may be used across multiple pages can be stored in the application's global message catalog, `src/main/webapp/WEB-INF/app.properties`, instead. + +This `message:` prefix is not some special case; there are actually quite a few of these binding prefixes built into Tapestry, each having a specific purpose. +In fact, omitting a binding prefix in an expansion is exactly the same as using the `prop:` binding prefix, which means to treat the binding as a property expression. + +Expansions are useful for extracting a piece of information and rendering it out to the client as a string, but the real heavy lifting of Tapestry occurs inside components. + +== Components Inside Templates +Components can be represented inside a component template in two ways: + +1. As an ordinary element, but with a t:type attribute to define the type of component. +2. As an element in the Tapestry namespace, in which case the element name determines the type. + +Here we've used an `<html>` element to represent the application's `Layout` component. + +[source,xml] +---- +<html t:type="layout" ...> + ... +</html> +---- + +But for the `EventLink` component, we've used an element in the Tapestry namespace: + +[source,xml] +---- +<t:eventlink page="Index">refresh page</t:eventlink> +---- + +Which form you select is a matter of choice. In the vast majority of cases, they are exactly equivalent. + +As elsewhere, case is ignored. +Here the types ("layout" and "eventlink") were in all lower case; the actual class names are `Layout` and `EventLink`. +Further, Tapestry "blends" the core library components in with the components defined by this application; thus type "layout" is mapped to application component class `com.example.tutorial.components.Layout`, but "eventlink" is mapped to Tapestry's built-in `org.apache.tapestry5.corelib.components.EventLink` class. + +Tapestry components are configured using parameters. +For each component, there is a set of parameters, each with a specific type and purpose. +Some parameters are required, others are optional. +Attributes of the element are used to bind parameters to specific literal values, or to page properties. +Tapestry is flexible here as well; you can always place an attribute in the Tapestry namespace (using the `t:` prefix), but in most cases, this is unnecessary. + +[source,xml] +---- +<html t:type="layout" title="tutorial1 Index" + p:sidebarTitle="Framework Version" ... +---- + +This binds two parameters, `title` and `sidebarTitle`, of the `Layout` component to the literal strings "tutorial1 Index" and "Framework Version", respectively. + +The Layout component will actually provide the bulk of the HTML ultimately sent to the browser; we'll look at its template in a later chapter. +The point is, the page's template is integrated into the Layout component's template. +The following diagram shows how parameters passed to the Layout component end up rendered in the final page: + +image:templates_and_parameters.png[] + +The interesting point here (and this is an advanced concept in Tapestry, one we'll return to later) is that we can pass a chunk of the `Index.tml` template to the `Layout` component as the `sidebar` parameter. +That's what the `tapestry:parameter` namespace (the `p:` prefix) is for; the element name is matched against a parameter of the component and the entire block of the template is passed into the `Layout` component ... which decides where, inside its template, that block gets rendered. + +[source,xml] +---- +<t:eventlink event="complete" class="btn btn-default">Complete»</t:eventlink> +---- + +This time, it's the `page` parameter of the `PageLink` component that is bound, to the literal value "Index" (which is the name of this page). +This gets rendered as a URL that re-renders the page, which is how the current time gets updated. +You can also create links to other pages in the application and, as we'll see in later chapters, attach additional information to the URL beyond just the page name. + +== A Magic Trick + +Now it's time for a magic trick. +Edit `Index.java` and change the `getCurrentTime()` method to: + +.Index.java (partial) +[source,java] +---- +public String getCurrentTime() +{ + return "A great day to learn Tapestry"; +} +---- + +Make sure you save changes; then click the refresh link in the web browser: + +image:app-live-reload.png[] + +This is one of Tapestry's early wow factor features: changes to your component classes are picked up immediately (a feature we call Live Class Reloading). +No restart. +No re-deploy. +Make the changes and see them now. +Nothing should slow you down or get in the way of you getting your job done. + +[TIP] +==== +If Live Class Reloading isn't working for you, check xref:userguide::class-reloading.adoc#_troubleshooting_live_class_reloading[Troubleshooting Live Class Reloading]. +==== + +But... what if you make a mistake? +What if you got the name in the template wrong. +Give it a try; in the template, change $\{currentTime} to, say, $\{currenTime}, and see what you get: + +image:app-error-1.png[] + +This is Tapestry's exception report page. +It's quite detailed. +It clearly identifies what Tapestry was doing, and relates the problem to a specific line in the template, which is shown in context. +Tapestry always expands out the entire stack of exceptions, because it is so common for exceptions to be thrown, caught, and re-thrown inside other exceptions. +In fact, if we scroll down just a little bit, we see more detail about this exception, plus a little bit of help: + +image:app-error-2.png[] + +This is part of Tapestry's way: it not only spells out exactly what it was doing and what went wrong, but it even helps you find a solution; here it tells you the names of properties you could have used. + +[NOTE] +==== +This level of detail reflects that the application has been configured to run in _development mode_ instead of _production mode_. +In production mode, the exception report would simply be the top level exception message. +However, most production applications go further and customize how Tapestry handles and reports exceptions. +==== + +Tapestry displays the stack trace of the deepest exception, along with lots of details about the run-time environment: details about the current request, the `HttpSession` (if one exists), and even a detailed list of all JVM system properties. +Scroll down to see all this information. + +Next: xref:implementing-the-hi-lo-guessing-game.adoc[] + + diff --git a/modules/tutorial/pages/implementing-the-hi-lo-guessing-game.adoc b/modules/tutorial/pages/implementing-the-hi-lo-guessing-game.adoc new file mode 100644 index 0000000..512d4be --- /dev/null +++ b/modules/tutorial/pages/implementing-the-hi-lo-guessing-game.adoc @@ -0,0 +1,510 @@ += Implementing the Hi-Lo Guessing Game + +Let's start building a basic Hi-Lo Guessing game. + +In the game, the computer selects a number between 1 and 10. You try and guess the number, clicking links. At the end, the computer tells you how many guesses you required to identify the target number. Even a simple example like this will demonstrate several important concepts in Tapestry: + +* Breaking an application into individual pages +* Transferring information from one page to another +* Responding to user interactions +* Storing client information in the server-side session + +We'll build this little application in small pieces, using the kind of iterative development that Tapestry makes so easy. + +.Flow Diagram +[plantuml,hilo-flow,svg,role=flow] +---- +include::partial$diagrams/hilo-flow.puml[] +---- + +Our page flow is very simple, consisting of three pages: Index (the starting page), Guess and GameOver. +The Index page introduces the application and includes a link to start guessing. +The Guess page presents the user with ten links, plus feedback such as "too low" or "too high". +The GameOver page tells the user how many guesses they took before finding the target number. + +== Index Page + +Let's get to work on the Index page and template. Make Index.tml look like this: + +.src/main/resources/com/example/tutorial1/pages/Index.tml +[source,xml] +---- +<html t:type="layout" title="Hi/Lo Guess" + xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"> + + <p> + I'm thinking of a number between one and ten ... + </p> + <p> + <a href="#">start guessing</a> + </p> + +</html> +---- + +And edit the corresponding Java class, Index.java, removing its body (but you can leave the imports in place for now): + +.src/main/java/com/example/tutorial1/pages/Index.java +[source,java] +---- +package com.example.tutorial1.pages; + +public class Index +{ +} +---- + +Running the application gives us our start: + +image:hilo-1.png[] + +However, clicking the link doesn't do anything yet, as its just a placeholder <a> tag, not an actual Tapestry component. +Let's think about what should happen when the user clicks that link: + +* A random target number between 1 and 10 should be selected. +* The number of guesses taken should be reset to 0. +* The user should be sent to the Guess page to make a guess. + +Our first step is to find out when the user clicks that "start guessing" link. +In a typical web application framework, we might start thinking about URLs and handlers and maybe some sort of XML configuration file. +But this is Tapestry, so we're going to work with components and methods on our classes. + +First, the component. +We want to perform an action (selecting the number) before continuing on to the Guess page. +The ActionLink component is just what we need; it creates a link with a URL that will trigger an action event in our code ... but that's getting ahead of ourselves. +First up, convert the `<a>` tag to an `ActionLink` component: + +.Index.tml (partial) +[source.xml] +---- +<p> + <t:actionlink t:id="start">start guessing</t:actionlink> +</p> +---- + +If you refresh the browser and hover your mouse over the "start guessing" link, you'll see that its URL is now `/tutorial1/index.start`, which identifies the name of the page ("index") and the id of the component ("start"). + +If you click the link now, you'll get an error: + +image:hilo-index-missing-action-error.png[] + +Tapestry is telling us that we need to provide some kind of event handler for that event. +What does that look like? + +An event handler is a method of the Java class with a special name. +The name is on__Eventname__From__Component-id__ ... here we want a method named `onActionFromStart()`. +How do we know that "action" is the right event name? +Because that's what `ActionLink` does, that's why its named `ActionLink`. + +Once again, Tapestry gives us options; if you don't like naming conventions, there's an `@OnEvent` annotation you can place on the method instead, which restores the freedom to name the method as you like. +Details about this approach are in the Tapestry Users' Guide. +We'll be sticking with the naming convention approach for the tutorial. + +When handling a component event request (the kind of request triggered by the `ActionLink` component's URL), Tapestry will find the component and trigger a component event on it. This is the callback our server-side code needs to figure out what the user is doing on the client side. +Let's start with an empty event handler: + +.Index.java +[source,java] +---- +package com.example.tutorial1.pages; + +public class Index +{ + void onActionFromStart() + { + + } +} +---- + +In the browser, we can re-try the failed component event request by hitting the refresh button ... or we can restart the application. +In either case, we get the default behavior, which is simply to re-render the page. + +Note that the event handler method does not have to be public; it can be protected, private, or package private (as in this example). +By convention, such methods are package private, if for no other reason than it is the minimal amount of characters to type. + +Hmm... right now you have to trust us that the method got invoked. +That's no good ... what's a quick way to tell for sure? +One way would be have the method throw an exception, but that's a bit ugly. + +How about this: add the javadoc:org.apache.tapestry5.annotations.Log[label=@Log] annotation to the method: + +.Index.java (partial) +[source,java] +---- +import org.apache.tapestry5.annotations.Log; + +... + + @Log + void onActionFromStart() + { + + } +... +---- + +When you next click the link you should see the following in the Eclipse console: + +---- +[DEBUG] pages.Index [ENTER] onActionFromStart() +[DEBUG] pages.Index [ EXIT] onActionFromStart +[INFO] AppModule.TimingFilter Request time: 3 ms +[INFO] AppModule.TimingFilter Request time: 5 ms +---- + +The `@Log` annotation directs Tapestry to log method entry and exit. +You'll get to see any parameters passed into the method, and any return value from the method ... as well as any exception thrown from within the method. +It's a powerful debugging tool. +This is an example of Tapestry's meta-programming power, something we'll use quite a bit of in the tutorial. + +Why do we see two requests for one click? +Tapestry uses an approach based on the Post/Redirect/Get pattern. +In fact, Tapestry generally performs a redirect after each component event. +So the first request was to process the action, and the second request was to re-render the Index page. +You can see this in the browser, because the URL is still "/tutorial1" (the URL for rendering the Index page). +We'll return to this in a bit. + +We're ready for the next step, which involves tying together the Index and Guess pages. +Index will select a target number for the user to Guess, then "pass the baton" to the Guess page. + +== Guess Page + +Let's start by thinking about the Guess page. +It needs a variable to store the target value in, and it needs a method that the Index page can invoke, to set up that target value. + +.Guess.java +[source,java] +---- +package com.example.tutorial1.pages; + +public class Guess +{ + private int target; + + void setup(int target) + { + this.target = target; + } +} +---- + +Create that Guess.java file in the same folder as Index.java. Next, we can modify Index to invoke the setup() method of our new Guess page class: + +.Index.java (revised) +[source,java] +---- +package com.example.tutorial1.pages; + +import java.util.Random; + +import org.apache.tapestry5.annotations.InjectPage; +import org.apache.tapestry5.annotations.Log; + +public class Index +{ + private final Random random = new Random(System.nanoTime()); + + @InjectPage + private Guess guess; + + @Log + Object onActionFromStart() + { + int target = random.nextInt(10) + 1; + + guess.setup(target); + return guess; + } +} +---- + +The new event handler method now chooses the target number, and tells the Guess page about it. +Because Tapestry is a managed environment, we don't just create an instance of Guess ... it is Tapestry's responsibility to manage the life cycle of the Guess page. +Instead, we ask Tapestry for the Guess page, using the `@InjectPage` annotation. + +IMPORTANT: All fields in a Tapestry page or component class must be non-public. + +Once we have that Guess page instance, we can invoke methods on it normally. + +Returning a page instance from an event handler method directs Tapestry to send a client-side redirect to the returned page, rather than sending a redirect for the active page. +Thus once the user clicks the "start guessing" link, they'll see the Guess page. + +IMPORTANT: When creating your own applications, make sure that the objects stored in final variables are thread safe. It seems counter-intuitive, but final variables are shared across many threads. Ordinary instance variables are not. Fortunately, the implementation of Random is, in fact, thread safe. + +So ... let's click the link and see what we get: + +image:guess-template-missing.png[] + +Ah! We didn't create a Guess page template. Tapestry was really expecting us to create one, so we better do so. + +.src/main/resources/com/example/tutorial/pages/Guess.tml +[source,java] +---- +<html t:type="layout" title="Guess The Number" + xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"> + + <p> + The secret number is: ${target}. + </p> + +</html> +---- + +Hit the browser's back button, then click the "start guessing" link again. We're getting closer: + +image:guess-no-target-prop.png[] + +If you scroll down, you'll see the line of the Guess.tml template that has the error. We have a field named target, but it is private and there's no corresponding property, so Tapestry was unable to access it. + +We just need to write the missing JavaBeans accessor methods `getTarget()` (and `setTarget()` for good measure). Or we could let Tapestry write those methods instead: + +[source,java] +---- +@Property +private int target; +---- + +The `@Property` annotation very simply directs Tapestry to write the getter and setter method for you. +You only need to do this if you are going to reference the field from the template. + +We are getting very close but there's one last big oddity to handle. +Once you refresh the page you'll see that target is 0! + +image:guess-target-zero.png[] + +What gives? We know it was set to at least 1 ... where did the value go? + +As noted above, Tapestry sends a redirect to the client after handling the event request. +That means that the rendering of the page happens in an entirely new request. +Meanwhile, at the end of each request, Tapestry wipes out the value in each instance variable. +So that means that target was a non-zero number during the component event request ... but by the time the new page render request comes up from the web browser to render the Guess page, the value of the target field has reverted back to its default, zero. + +The solution here is to mark which fields have values that should persist from one request to the next (and next, and next ...). That's what the `@Persist` annotation is for: + +[source,java] +---- +@Property +@Persist +private int target; +---- + +This doesn't have anything to do with database persistence (that's coming up in a later chapter). +It means that the value is stored in the `HttpSession` between requests. + +Go back to the Index page and click the link again. Finally, we have a target number: + +image:guess-target.png[] + +That's enough for us to get started. Let's build out the Guess page, and get ready to let the user make guesses. +We'll show the count of guesses, and increment that count when they make them. +We'll worry about high and low and actually selecting the correct value later. + +When building Tapestry pages, you sometimes start with the Java code and build the template to match, and sometime start with the template and build the Java code to match. +Both approaches are valid. +Here, lets start with the markup in the template, then figure out what we need in the Java code to make it work. + +.Guess.tml (revised) +[source,java] +---- +<html t:type="layout" title="Guess The Number" + xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" + xmlns:p="tapestry:parameter"> + + <p> + The secret number is: ${target}. + </p> + + <strong>Guess number ${guessCount}</strong> + + <p>Make a guess from the options below:</p> + + <ul class="list-inline"> + <t:loop source="1..10" value="current"> + <li> + <t:actionlink t:id="makeGuess" context="current">${current} + </t:actionlink> + </li> + </t:loop> + </ul> + +</html> +---- + +So it looks like we need a `guessCount` property that starts at 1. + +We're also seeing one new component, the Loop component. +A Loop component iterates over the values passed to it in its `source` parameter, and renders it body once for each value. +It updates the property bound to its `value` parameter before rendering its body. + +That special property expression, `1..10`, generates a series of numbers from 1 to 10, inclusive. +Usually, when you use the Loop component, you are iterating over a List or Collection of values, such as the results of a database query. + +So, the Loop component is going to set the `current` property to 1, and render its body (the `<li>` tag, and the ActionLink component). +Then its going to set the `current` property to 2 and render its body again ... all the way up to 10. + +And notice what we're doing with the ActionLink component; its no longer enough to know the user clicked on the ActionLink ... we need to know _which iteration_ the user clicked on. +The `context` parameter allows a value to be added to the ActionLink's URL, and we can get it back in the event handler method. + +TIP: The URL for the ActionLink will be `/tutorial1/guess.makeguess/3`. That's the page name, "Guess", the component id, "makeGuess", and the context value, "3". + +.Guess.java (revised) +[source,java] +---- +package com.example.tutorial1.pages; + +import org.apache.tapestry5.annotations.Persist; +import org.apache.tapestry5.annotations.Property; + +public class Guess +{ + @Property + @Persist + private int target, guessCount; + + @Property + private int current; + + void setup(int target) + { + this.target = target; + guessCount = 1; + } + + void onActionFromMakeGuess(int value) + { + guessCount++; + } + +} +---- + +The revised version of Guess includes two new properties: `current` and `guessCount`. +There's also a handler for the action event from the `makeGuess` ActionLink component; currently it just increments the count. + +Notice that the `onActionFromMakeGuess()` method now has a parameter: the context value that was encoded into the URL by the ActionLink. +When then user clicks the link, Tapestry will automatically extract the string from the URL, convert it to an int and pass that int value into the event handler method. +More boilerplate code you don't have to write. + +At this point, the page is partially operational: + +image:guess-1.png[] + +Our next step is to actually check the value provided by the user against the target and provide feedback: either they guessed too high, or too low, or just right. +If they get it just right, we'll switch to the `GameOver` page with a message such as "You guessed the number 5 in 2 guesses". + +Let's start with the Guess page; it now needs a new property to store the message to be displayed to the user, and needs a field for the injected GameOver page: + +.Guess.java (partial) +[source,java] +---- +@Property +@Persist(PersistenceConstants.FLASH) +private String message; + +@InjectPage +private GameOver gameOver; +---- + +First off, we're seeing a variation of the `@Persist` annotation, where a persistence _strategy_ is provided by name. +`FLASH` is a built-in strategy that stores the value in the session, but only for one request ... it's designed specifically for these kind of feedback messages. +If you hit F5 in the browser, to refresh, the page will render but the message will disappear. + +Next, we need some more logic in the `onActionFromMakeGuess()` event handler method: + +.Guess.java (partial) +[source,java] +---- +Object onActionFromMakeGuess(int value) +{ + if (value == target) + { + gameOver.setup(target, guessCount); + return gameOver; + } + + guessCount++; + + message = String.format("Your guess of %d is too %s.", value, + value < target ? "low" : "high"); + + return null; +} +---- + +Again, very straight-forward. +If the value is correct, then we configure the GameOver page and return it, causing a redirect to that page. +Otherwise, we increment the number of guesses, and format the message to display to the user. + +In the template, we just need to add some markup to display the message: + +.Guess.tml (partial) +[source,xml] +---- +<strong>Guess number ${guessCount}</strong> + +<t:if test="message"> + <p> + <strong>${message}</strong> + </p> +</t:if> +---- + +This snippet uses Tapestry's `If` component. +The `If` component evaluates its test parameter and, if the value evaluates to true, renders its body. +The property bound to test doesn't have to be a boolean; Tapestry treats `null` as false, it treats zero as false and non-zero as true, it treats an empty `Collection` as `false` ... and for Strings (such as `message`) it treats a blank string (one that is null, or consists only of white space) as false, and a non-blank string is true. + +## GameOver Page + +GameOver.java + +[source,java] +---- +package com.example.tutorial1.pages; + +import org.apache.tapestry5.annotations.Persist; +import org.apache.tapestry5.annotations.Property; + +public class GameOver +{ + @Property + @Persist + private int target, guessCount; + + void setup(int target, int guessCount) + { + this.target = target; + this.guessCount = guessCount; + } +} +---- + +.GameOver.tml +[source,xml] +---- +<html t:type="layout" title="Game Over" + xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd" + xmlns:p="tapestry:parameter"> + + <p> + You guessed the number + <strong>${target}</strong> + in + <strong>${guessCount}</strong> + guesses. + </p> + +</html> +---- + +The result, when you guess correctly, should be this: + +image:gameover.png[] + +That wraps up the basics of Tapestry; we've demonstrated the basics of linking pages together and passing information from page to page in code as well as incorporating data inside URLs. + +There's still more room to refactor this toy application; for example, making it possible to start a new game from the GameOver page (and doing it in a way that doesn't duplicate code). In addition, later we'll see other ways of sharing information between pages that are less cumbersome than the setup-and-persist approach shown here. + +Next up: let's find out how Tapestry handles HTML forms and user input. + +Next: xref:using-beaneditform-to-create-user-forms.adoc[] diff --git a/modules/tutorial/pages/index.adoc b/modules/tutorial/pages/index.adoc new file mode 100644 index 0000000..67065e3 --- /dev/null +++ b/modules/tutorial/pages/index.adoc @@ -0,0 +1,57 @@ += Introduction + +Welcome to Tapestry! + +This is a tutorial for people who will be creating Tapestry web applications. +It doesn't matter whether you have experience with earlier versions of Tapestry or other web frameworks. +In fact, in some ways, the less you know about web development in general, the better off you may be ... that much less to unlearn! + +You do need to have a reasonable understanding of HTML, a smattering of XML, and a good understanding of basic Java language features, including Annotations. + +== The Challenges of Web Application Development +If you're used to developing web applications using servlets and JSPs, or with Struts, you are simply used to a lot of pain. +These are environments with few safety net; Struts and the Servlet API have no idea how your application is structured, or how the different pieces fit together. +Any URL can be an action and any action can forward to any view (usually a JSP) to provide an HTML response to the web browser. +The pain is the unending series of small, yet important, decisions you have to make as a developer (and communicate to the rest of your team). +What are the naming conventions for actions, for pages, for attributes stored in the `HttpSession` or `HttpServletRequest`? +Where do cross-cutting concerns such as database transactions, caching and security get implemented (and do you have to cut-and-paste Java or XML to make it work?) +How are your packages organized ... where to the user interface classes go, and where do the data and entity objects go? +How do you share code from one part of your application to another? + +On top of all that, the traditional approaches thrust something most unwanted in your face: _multi-threaded coding_. +Remember back to Object Oriented Programming 101 where an object was defined as a bundle of data and operations on that data? +You have to unlearn that lesson as soon as you build a traditional web application, because web applications are multi-threaded. +An application server could be handling dozens or hundreds of requests from individual users, each in their own thread, and each sharing the exact same objects. +Suddenly, you can't store data inside an object (a servlet or a Struts Action) because whatever data you store for one user will be instantly overwritten by some other user. + +Worse, your objects each have only one operation: `doGet()` or `doPost()`. + +Meanwhile, most of your day-to-day work involves deciding how to package up some data already inside a particular Java object and squeeze that data into a URL's query parameters, so that you can write more code to convert it back if the user clicks that particular link. +And don't forget editing a bunch of XML files to keep the servlet container, or the Struts framework, aware of these decisions. + +Just for laughs, remember that you have to rebuild, redeploy and restart your application after virtually any change. +Is any of this familiar? +Then perhaps you'd appreciate something a little less familiar: Tapestry. + +== The Tapestry Way +Tapestry uses a very different model: a structured, organized world of pages, and components within pages. +Everything has a very specific name (that you provide). Once you know the name of a page, you know the location of the Java class for that page, the location of the template for that page, and the total structure of the page. +Tapestry knows all this as well, and can make things *just work*. + +As we'll see in the following pages, Tapestry lets you code in terms of your objects. +You'll barely see any Tapestry classes, outside of a few Java annotations. +If you have information to store, store it as fields of your classes, not inside the `HttpServletRequest` or `HttpSession`. +If you need some code to execute, it's just a simple annotation or method naming convention to get Tapestry to invoke that method, at the right time, with the right data. + +Tapestry also shields you from most of the multi-threaded aspects of web application development. +Tapestry manages the life cycle of your page and components objects, and the fields of the pages and components, in a thread-safe way. +Your page and component classes always look like simple, standard http://en.wikipedia.org/wiki/Plain_Old_Java_Object[POJOs]. + +Tapestry began in January 2000, and it now reflects over twenty years of experience of the entire Tapestry community. +Tapestry brings to the table all that experience about the best ways to build scalable, maintainable, robust, internationalized, and Ajax-enabled applications. + +== Getting the Tutorial Source +Although you won't need it, the source code for this tutorial is available on https://github.com/hlship/tapestry5-tutorial[GitHub]. + +== Time to Begin +Okay, enough background. Now let's dig in: xref:dependencies-tools-and-plugins.adoc[] diff --git a/modules/tutorial/pages/using-beaneditform-to-create-user-forms.adoc b/modules/tutorial/pages/using-beaneditform-to-create-user-forms.adoc new file mode 100644 index 0000000..87a8ee4 --- /dev/null +++ b/modules/tutorial/pages/using-beaneditform-to-create-user-forms.adoc @@ -0,0 +1,415 @@ += Using BeanEditForm To Create User Forms + +In the previous chapters, we saw how Tapestry can handle simple links, even links that pass information in the URL. +In this chapter, we'll see how Tapestry can do the same, and quite a bit more, for HTML forms. + +Form support in Tapestry is deep and rich, more than can be covered in a single chapter. +However, we can show the basics, including some very common development patterns. +To get started, let's create a simple address book application. + +We'll start with the entity data, a simple object to store the information we'll need. +These classes go in an `entities` sub-package. +Unlike the use of the `pages` sub-package (for page component classes), this is not enforced by Tapestry; it's just a convention (but as we'll see shortly, a handy one). + +Tapestry treats public fields as if they were JavaBeans properties; since the Address object is just "dumb data", there's no need to get carried away writing getters and setters. Instead, we'll define an entity that is all public fields: + +.src/main/java/com/example/tutorial/entities/Address.java + +[source,java] +---- +package com.example.tutorial1.entities; + +import com.example.tutorial1.data.Honorific; + +public class Address +{ + public Honorific honorific; + public String firstName; + public String lastName; + public String street1; + public String street2; + public String city; + public String state; + public String zip; + public String email; + public String phone; +} +---- + +We also need to define the enum type, Honorific: + +.src/main/java/com/example/tutorial/data/Honorific.java +[source,java] +---- +package com.example.tutorial1.data; + +public enum Honorific +{ + MR, MRS, MISS, DR +} +---- + +== Address Pages + +We're probably going to create a few pages related to addresses: pages for creating them, for editing them, for searching and listing them. We'll create a sub-folder, address, to hold them. Let's get started on the first of these pages, "address/Create" (that's the real name, including the slash — we'll see in a minute how that maps to classes and templates). + +First, we'll update the Index.tml template, to create a link to the new page: + +.src/main/resources/com/example/tutorial/pages/Index.tml (partial) + +[source,xml] +---- +<h1>Address Book</h1> + +<ul> + <li><t:pagelink page="address/create">Create new address</t:pagelink></li> +</ul> +---- + +Now we need the address/Create page; lets start with an empty shell, just to test our navigation. + +.src/main/resources/com/example/tutorial/pages/address/CreateAddress.tml +[source,xml,subs="+attributes"] +---- +<html t:type="layout" title="Create New Address" + xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd"> <1> + + <em>coming soon ...</em> + +</html> +---- +<1> Adapt as necessary for earlier versions than Tapestry 5.4, for `tapestry_5_3.xsd` + +Next, the corresponding class: + +.src/main/java/com/example/tutorial/pages/address/CreateAddress.java +[source,java] +---- +package com.example.tutorial1.pages.address; + +public class CreateAddress +{ + +} +---- + +So ... why is the class named `CreateAddress` and not simply `Create`? +Actually, we could have named it `Create"`, and the application would still work, but the longer _class_ name is equally valid. +Tapestry noticed the redundancy in the class name (`com.example.tutorial1.pages.__address__.Create__Address__`) and just stripped out the redundant suffix. + +Tapestry actually creates a bunch of aliases for you pages; any of these aliases are valid and can appear in URLs or in the page parameter of PageLink. +You can see the list in the console: + +---- +[INFO] TapestryModule.ComponentClassResolver Available pages (12): + (blank): com.example.tutorial1.pages.Index + ComponentLibraries: org.apache.tapestry5.corelib.pages.ComponentLibraries + Error404: com.example.tutorial1.pages.Error404 + ExceptionReport: org.apache.tapestry5.corelib.pages.ExceptionReport + GameOver: com.example.tutorial1.pages.GameOver + Guess: com.example.tutorial1.pages.Guess + Index: com.example.tutorial1.pages.Index + PageCatalog: org.apache.tapestry5.corelib.pages.PageCatalog +PropertyDisplayBlocks: org.apache.tapestry5.corelib.pages.PropertyDisplayBlocks + PropertyEditBlocks: org.apache.tapestry5.corelib.pages.PropertyEditBlocks + ServiceStatus: org.apache.tapestry5.corelib.pages.ServiceStatus + T5Dashboard: org.apache.tapestry5.corelib.pages.T5Dashboard + address/Create: com.example.tutorial1.pages.address.CreateAddress +address/CreateAddress: com.example.tutorial1.pages.address.CreateAddress +---- + +Tapestry users the shortest alias when constructing URLs. + +Eventually, your application will probably have more entities: perhaps you'll have a `user/Create` page and a `payment/Create` page and an `account/Create` page. +You _could_ have a bunch of different classes all named Create spread across a number of different packages. +That's legal Java, but it isn't ideal. +You may find yourself accidentally editing the Java code for creating an Account when you really want to be editing the code for creating a Payment. + +Tapestry is encouraging you to use a more descriptive name: `Create__Address__`, not just `Create`, but it isn't making you pay the cost (in terms of longer, uglier URLs). +The URL to access the page will still be `http://localhost:8080/tutorial1/address/create`. + +And remember, regardless of the name that Tapestry assigns to your page, the template file is named like the Java class itself: `CreateAddress.tml`. + +[NOTE] +==== +Index pages work in folders as well. +A class named `com.example.tutorial1.pages.address.AddressIndex` would be given the name `address/Index`. +However, Tapestry has special rules for pages named `Index` and the rendered URL would be `http://localhost:8080/tutorial1/address/`. +In other words, you can place Index pages in any folder and Tapestry will build a short URL for that page ... and you don't have to keep naming the classes `Index` (it's confusing to have many classes with the same name, even across multiple packages); instead, you can name each index page after the package that contains it. +Tapestry users a smart convention to keep it all straight and generate short, to the point URLs. +==== + +== Using the BeanEditForm Component + +Time to start putting together the logic for this form. +Tapestry has a specific component for client-side Forms: the javadoc:org.apache.tapestry5.corelib.components.Form[] component, as well as components for form controls, such as javadoc:org.apache.tapestry5.corelib.components.Checkbox[] and javadoc:org.apache.tapestry5.corelib.components.TextField[]. +We'll cover those in a bit more detail later .. instead, we're again going to let Tapestry do the heavy lifting for us, via the javadoc:org.apache.tapestry5.corelib.components.BeanEditForm[] component. + +Add the following to the `CreateAddress` template (replacing the "coming soon ..." message): + +.CreateAddress.tml (partial) +[source,xml] +---- +<t:beaneditform object="address"/> +---- + +And match that up with a property in the `CreateAddress` class: + +.CreateAddress.java (partial) +[source,java] +---- +@Property +private Address address; +---- + +When you refresh the page, you may see a warning like the following at the top of the page: + +image:hmac-warning.png[] + +If you see that, it means you need to invent an HMAC passphrase for your app. +Just edit your `AppModule.java` class (in your `services` package), adding a couple of lines to the `contributeApplicationDefaults` method like the following: + +.AppModule.java (partial) +---- +// Set the HMAC pass phrase to secure object data serialized to client +configuration.add(SymbolConstants.HMAC_PASSPHRASE, ""); +---- + +but, instead of an empty string, insert a long, *random string of characters* (like a very long and complex password, at least 30 characters) that you keep private. + +After you do that, stop the app and restart it, and click on the Create new address link again, and you'll see something like this: + +image:create-address-initial.png[] + +Tapestry has done quite a bit of work here. +It has created a form that includes a field for each property. +Further, it has seen that the honorific property is an enumerated type, and presented that as a drop-down list. + +In addition, Tapestry has converted the property names (`city`, `email`, `firstName`) to user presentable labels ("City", "Email", "First Name"). +In fact, these are `<label>` elements, so clicking a label with the mouse will move the input cursor into the corresponding field. + +This is an awesome start; it's a presentable interface, quite nice in fact for a few minute's work. +But it's far from perfect; let's get started with some customizations. + +=== Changing Field Order + +The `BeanEditForm` must guess at the right order to present the fields; for public fields, they end up in alphabetical order. +For standard JavaBeans properties, the `BeanEditForm` default is in the order in which the getter methods are defined in the class (it uses line number information, if available). + +A better order for these fields is the order in which they are defined in the `Address` class: + +* honorific +* firstName +* lastName +* street1 +* street2 +* city +* state +* zip +* email +* phone + +We can accomplish this by using the `reorder` parameter of the `BeanEditForm` component, which is a comma separated list of property (or public field) names: + +.CreateAddress.tml (partial) +[source,xml] +---- +<t:beaneditform object="address" + reorder="honorific,firstName,lastName,street1,street2,city,state,zip,email,phone" /> +---- + +image:create-address-reordered.png[] + +=== Customizing labels +Tapestry makes it pretty easy to customize the labels used on the fields. +It's just a matter of creating a _message catalog_ for the page. + +In Tapestry, every page and component may have its own message catalog. +This is a standard Java properties file, and it is named the same as the page or component class, with a `.properties` extension. +A message catalog consists of a series of lines, each line is a message key and a message value separated with an equals sign. + +All it takes is to create a message entry with a particular name: the name of the property suffixed with `-label`. +As elsewhere, Tapestry is forgiving of case. + +.src/main/resources/com/example/tutorial/pages/address/CreateAddress.properties +[source,properties] +---- +street1-label=Street 1 +street2-label=Street 2 +email-label=E-Mail +zip-label=Zip Code +phone-label=Phone Number +---- + +Since this is a _new_ file (and not a change to an existing file), you may have to restart Jetty to force Tapestry to pick up the change. + +image:address-v3.png[] + +We can also customize the options in the drop down list. +All we have to do is add some more entries to the message catalog matching the enum names to the desired labels. +Update `CreateAddress.properties` and add: + +[source,properties] +---- +MR=Mr. +MRS=Mrs. +DR=Dr. +---- + +Notice that we don't have to include an option for MISS, because that is converted to "Miss" anyway. +You might just want to include it for sake of consistency ... the point is, each option label is searched for separately. + +Lastly, the default label on the submit button is "Create/Update" (BeanEditForm doesn't know how it is being used). +Let's change that to "Create Address". + +That button is a component within the `BeanEditForm` component. +It's not a property, so we can't just put a message into the message catalog, the way we can with the fields. +Fortunately, the BeanEditForm component includes a parameter expressly for re-labeling the button. +Simply change the `CreateAddress` component template: + +.CreateAddress.tml (partial) +[source,xml] +---- +<t:beaneditform submitlabel="Create Address" object="address" + reorder="honorific,firstName,lastName,street1,street2,city,state,zip,email,phone"/> +---- + +The default for the submitlabel parameter is "Create/Update", but here we're overriding that default to a specific value. + +The final result shows the reformatting and relabelling: + +image:address-v5.png[] + +Before continuing on to validation, a side note about message catalogs. +Message catalogs are not just for re-labeling fields and options; we'll see in later chapters how message catalogs are used in the context of localization and internationalization. + +Instead of putting the label for the submit button directly inside the template, we're going to provide a reference to the label; the actual label will go in the message catalog. + +In Tapestry, when binding a parameter, the value you provide may include a prefix. +The prefix guides Tapestry in how to interpret the rest of the the parameter value ... is it the name of a property? The id of a component? +A message key? Most parameters have a default prefix, usually `prop:`, that is used when you fail to provide one (this helps to make the templates as terse as possible). + +Here we want to reference a message from the catalog, so we use the `message:` prefix: + +[source,xml] +---- +<t:beaneditform object="address" submitlabel="message:submit-label" + reorder="honorific,firstName,lastName,street1,street2,city,state,zip,email,phone" /> +---- + +And then we define the submit-label key in the message catalog: + +[source,properties] +---- +submit-label=Create Address +---- + +In the end, the exact same HTML is sent to the client, regardless of whether you include the label text directly in the template, or indirectly in the message catalog. +In the long term, the latter approach will work better if you later chose to internationalize your application. + +=== Adding Validation +Before we worry about storing the Address object, we should make sure that the user provides reasonable values. +For example, several of the fields should be required, and phone numbers and email address have specific formats. + +The `BeanEditForm` checks for a Tapestry-specific annotation, javadoc:org.apache.tapestry5.beaneditor.Validate[label=@Validate], on the field, the getter method, or the setter method of each property. + +Edit the `Address` entity, and update the `lastName`, `firstName`, `street1`, `city`, `state` and `zip` fields, adding a `@Validate` annotation to each: + +[source,java] +---- +@Validate("required") +public String firstName; +---- + +What is that string, `required`? +That's how you specify the desired validation. +It is a series of names that identify what type of validation is desired. +A number of validators are built in, such as `required`, `minLength` and `maxLength`. As elsewhere, Tapestry is case insensitive. + +You can apply multiple validations, by separating the validator names with commas. +Some validators can be configured (with an equals sign). +Thus you might say `required,minLength=5` for a field that must be specified, and must be at least five characters long. + +[IMPORTANT] +==== +You can easily get confused when you make a change to an entity class, such as adding the `@Validate` annotation, and _not_ see the result in the browser. +Only component classes, and (most) classes in the Tapestry services layer, are live-reloaded. +Data and entity objects are not reloaded, so this is one area where you need to stop and restart Jetty to see the change. +==== + +Restart the application, and refresh your browser, then hit the Create Address button. + +image:address-v6.png[] + +This is a shot just after hitting the Create Address button; all the fields have been validated and errors displayed. +Each field in error has been highlighted in red and had an error message added. +Further, the label for each of the fields has also been highlighted in red, to even more clearly identify what's in error. +The cursor has also been moved to the first field that's in error. +And _all_ of this is taking place on the client side, without any communication with the application. + +Once all the errors are corrected, and the form does submit, all validations are performed on the server side as well (just in case the client has JavaScript disabled). + +So ... how about some more interesting validation than just "required or not". +Tapestry has built in support for validating based on field length and several variations of field value, including regular expressions. +Zip codes are pretty easy to express as a regular expression. + +[source,java] +---- +@Validate("required,regexp=^\\d{5}(-\\d{4})?$") +public String zip; +---- + +Let's give it a try; restart the application and enter an "abc" for the zip code. + +image:address-v7.png[] + +This is what you'll see after typing "abc" and clicking the Create Address button. + +TIP: Modern browsers will automatically validate a regexp field when the form is submitted, as shown above. Older browsers do not have that automatic support, but will still validate input, using the same decorations as for the required fields in the previous screenshot. + +In any case, that's the right validation behavior, but it's the wrong message. Your users are not going to know or care about regular expressions. + +Fortunately, it's easy to customize validation messages. +All we need to know is the name of the property ("zip") and the name of the validator ("regexp"). +We can then put an entry into the CreateAddress message catalog: + +[source,properties] +---- +zip-regexp-message=Zip Codes are five or nine digits. Example: 02134 or 90125-1655. +---- + +Refresh the page and submit again: + +image:address-v8.png[] + +This trick isn't limited to just the regexp validator, it works equally well with any validator. + +Let's go one step further. +Turns out, we can move the regexp pattern to the message catalog as well. +If you only provide the name of the validator in the `@Validate` annotation, Tapestry will search the containing page's message catalog of the constraint value, as well as the validation message. +The constraint value for the regexp validator is the regular expression to match against. + +[source,java] +---- +@Validate("required,regexp") +public String zip; +---- + +Now, just put the regular expression into the CreateAddress message catalog: + +[source,properties] +---- +zip-regexp=^\\d{5}(-\\d{4})?$ +zip-regexp-message=Zip Codes are five or nine digits. Example: 02134 or 90125-1655. +---- + +After a restart you'll see the ... the same behavior. +But when we start creating more complicated regular expressions, it'll be much, much nicer to put them in the message catalog rather than inside the annotation value. +And inside the message catalog, you can change and tweak the regular expressions without having to restart the application each time. + +We could go a bit further here, adding more regular expression validation for phone numbers and e-mail addresses. +We're also far from done in terms of further customizations of the `BeanEditForm` component. + +By now you are likely curious about what happens after the form submits successfully (without validation errors), so that's what we'll focus on next. + +Next: xref:using-tapestry-with-hibernate.adoc[] diff --git a/modules/tutorial/pages/using-tapestry-with-hibernate.adoc b/modules/tutorial/pages/using-tapestry-with-hibernate.adoc new file mode 100644 index 0000000..f114217 --- /dev/null +++ b/modules/tutorial/pages/using-tapestry-with-hibernate.adoc @@ -0,0 +1,262 @@ += Using Tapestry With Hibernate + +So, you fill in all the fields, submit the form (without validation errors) and voila: you get back the same form, blanked out. +What happened, and where did the data go? + +What happened is that we haven't told Tapestry what to do after the form is successfully submitted (by successful, we mean, with no validation errors). +Tapestry's default behavior is to redisplay the active page, and that occurs in a new request, with a new instance of the Address object (because the address field is not a peristent field). + +Well, since we're creating objects, we might as well store them somewhere ... in a database. +We're going to quickly integrate Tapestry with http://hibernate.org/[Hibernate] as the object/relational mapping layer, and ultimately store our data inside a http://www.hsqldb.org/[HyperSQL] (HSQLDB) database. +HSQLDB is an embedded database engine and requires no installation – it will be pulled down as a dependency by Maven. + +== Re-configuring the Project +We're going to bootstrap this project from a simple Tapestry project to one that uses Hibernate and HSQLDB. + +=== Updating the Dependencies +First, we must update the POM to list a new set of dependencies, that includes Hibernate, the Tapestry/Hibernate integration library, and the HSQLDB JDBC driver: + +.pom.xml (partial) +[source,xml] +---- +<dependencies> + + <dependency> + <groupId>org.apache.tapestry</groupId> + <artifactId>tapestry-hibernate</artifactId> + <version>${tapestry-release-version}</version> + </dependency> + + <dependency> + <groupId>org.hsqldb</groupId> + <artifactId>hsqldb</artifactId> + <version>2.3.2</version> + </dependency> + ... +</dependencies> +---- + +The `tapestry-hibernate` library includes, as transitive dependencies, Hibernate and `tapestry-core`. +This means that you can simply replace `tapestry-core` with `tapestry-hibernate` inside the `<artifactId>` element. + +After changing the POM and saving, Maven should automatically download the JARs for the new dependencies. + +=== Hibernate Configuration +Hibernate needs a master configuration file, `hibernate.cfg.xml`, used to store connection and other data. +Create this in your `src/main/resources` folder: + +.src/main/resources/hibernate.cfg.xml +[source,xml] +---- +<!DOCTYPE hibernate-configuration PUBLIC + "-//Hibernate/Hibernate Configuration DTD 3.0//EN" + "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> +<hibernate-configuration> + <session-factory> + <property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property> + <property name="hibernate.connection.url">jdbc:hsqldb:./target/work/t5_tutorial1;shutdown=true</property> + <property name="hibernate.dialect">org.hibernate.dialect.HSQLDialect</property> + <property name="hibernate.connection.username">sa</property> + <property name="hibernate.connection.password"></property> + <property name="hbm2ddl.auto">update</property> + <property name="hibernate.show_sql">true</property> + <property name="hibernate.format_sql">true</property> + </session-factory> +</hibernate-configuration> +---- + +Most of the configuration is to identify the JDBC driver and connection URL. + +Note the connection URL. +We are instructing HSQLDB to store its database files within our project's target directory. +We are also instructing HSQLDB to flush any data to these files at shutdown. +This means that data will persist across different invocations of this project, but if the target directory is destroyed (e.g., via `mvn clean`), then all the database contents will be lost. + +In addition, we are configuring Hibernate to _update_ the database schema; when Hibernate initializes it will create or even modify tables to match the entities. +Finally, we are configuring Hibernate to output any SQL it executes, which is very useful when initially building an application. + +But what entities? +Normally, the available entities are listed inside `hibernate.cfg.xml`, but that's not necessary with Tapestry; in another example of convention over configuration, Tapestry locates all entity classes inside the entities package (`com.example.tutorial1.entities` in our case) and adds them to the configuration. +Currently, that is just the Address entity. + +== Adding Hibernate Annotations +For an entity class to be used with Hibernate, some Hibernate annotations must be added to the class. + +Below is the updated Address class, with the Hibernate annotations (as well as the Tapestry ones). + +.src/main/java/com/example/tutorial/entities/Address.java +[source,java] +---- +package com.example.tutorial1.entities; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +import org.apache.tapestry5.beaneditor.NonVisual; +import org.apache.tapestry5.beaneditor.Validate; + +import com.example.tutorial1.data.Honorific; + +@Entity +public class Address +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @NonVisual + public Long id; + + public Honorific honorific; + + @Validate("required") + public String firstName; + + @Validate("required") + public String lastName; + + public String street1; + + public String street2; + + @Validate("required") + public String city; + + @Validate("required") + public String state; + + @Validate("required,regexp") + public String zip; + + public String email; + + public String phone; +} +---- + +The Tapestry annotations, `@NonVisual` and `@Validate`, may be placed on the setter or getter method or on the field (as we have done here). +As with the Hibernate annotations, putting the annotation on the field requires that the field name match the corresponding property name. + +* `@NonVisual` – indicates a field, such as a primary key, that should not be made visible to the user. +* `@Validate` – identifies the validations associated with a field. + +At this point you should stop and restart your application. + +== Updating the Database +So we have a database set up, and Hibernate is configured to connect to it. +Let's make use of that to store our Address object in the database. + +What we need is to provide some code to be executed when the form is submitted. +When a Tapestry form is submitted, there is a whole series of events that get fired. +The event we are interested in is the "success" event, which comes late in the process, after all the values have been pulled out of the request and applied to the page properties, and after all server-side validations have occurred. + +The success event is only fired if there are no validation errors. + +Our event handler must do two things: + +* Use the Hibernate Session object to persist the new Address object. +* Commit the transaction to force the data to be written to the database. + +Let's update our `CreateAddress.java` class: + +.src/main/java/com/example/tutorial/pages/address/CreateAddress.java +[source,java] +---- +package com.example.tutorial1.pages.address; + +import com.example.tutorial1.entities.Address; +import com.example.tutorial1.pages.Index; +import org.apache.tapestry5.annotations.InjectPage; +import org.apache.tapestry5.annotations.Property; +import org.apache.tapestry5.hibernate.annotations.CommitAfter; +import org.apache.tapestry5.ioc.annotations.Inject; +import org.hibernate.Session; + +public class CreateAddress +{ + @Property + private Address address; + + @Inject + private Session session; + + @InjectPage + private Index index; + + @CommitAfter + Object onSuccess() + { + session.persist(address); + + return index; + } +} +---- + +The `@Inject` annotation tells Tapestry to inject a service into the annotated field; Tapestry includes a sophisticated Inversion of Control container (similar in many ways to Spring) that is very good at locating available services by type, rather than by a string id. +In any case, the Hibernate Session object is exposed as a Tapestry IoC service, ready to be injected (this is one of the things provided by the tapestry-hibernate module). + +Tapestry automatically starts a transaction as necessary; however that transaction will be _aborted_ at the end of the request by default. +If we make changes to persistent objects, such as adding a new Address object, then it is necessary to commit the transaction. + +The `@CommitAfter` annotation can be applied to any component method; if the method completes normally, the transaction will be committed (and a new transaction started to replace the committed transaction). + +After persisting the new address, we return to the main Index page of the application. + +_Note: In real applications, it is rare to have pages and components directly use the Hibernate Session. It is generally a better approach to define your own Data Access Object layer to perform common update operations and queries._ + +== Showing Addresses +As a little preview of what's next, let's display all the Addresses entered by the user on the Index page of the application. +After you enter a few names, it will look something like: + +image:index-grid-v1.png[] + +== Adding the Grid to the Index page +So, how is this implemented? Primarily, its accomplished by the `Grid` component. + +The `Grid` component is based on the same concepts as the `BeanEditForm` component; it can pull apart a bean into columns. +The columns are sortable, and when there are more entries than will fit on a single page, page navigation is automatically added. + +A minimal `Grid` is very easy to add to the template. +Just add this near the bottom of `Index.tml`: + +.src/main/webapp/Index.tml (partial) +[source,xml] +---- +<t:grid source="addresses" + include="honorific,firstName,lastName,street1,city,state,zip,phone"/> +---- + +Note that the `Grid` component accepts many of the same parameters that we used with the `BeanEditForm`. +Here we use the include parameter to specify the properties to show, and in what order. + +Now all we have to do is supply the addresses property in the Java code. Here's how `Index.java` should look now: + +.src/main/java/com/example/tutorial/pages/Index.java +[source,java] +---- +package com.example.tutorial1.pages; +import java.util.List; +import org.apache.tapestry5.ioc.annotations.Inject; +import org.hibernate.Session; +import com.example.tutorial1.entities.Address; +public class Index +{ + @Inject + private Session session; + public List<Address> getAddresses() + { + return session.createCriteria(Address.class).list(); + } +} +---- + +Here, we're using the Hibernate Session object to find all Address objects in the database. Any sorting that takes place will be done in memory. +This is fine for now (with only a handful of Address objects in the database). Later we'll see how to optimize this for very large result sets. + +== What's Next? +We have lots more to talk about: more components, more customizations, built-in Ajax support, more common design and implementation patterns, and even writing your own components (which is easy!). + +Check out the many Tapestry resources available on the Documentation page, including the xref:ROOT:getting-started.adoc[] and FAQ pages and the Cookbook. +Be sure to peruse the xref:userguide::index.adoc[User Guide], which provides comprehensive details on nearly every Tapestry topic. +Finally, be sure to visit (and bookmark) https://tapestry-jumpstart.org/jumpstart[Tapestry JumpStart], which provides a nearly exhaustive set of tutorials. diff --git a/modules/tutorial/partials/diagrams/hilo-flow.puml b/modules/tutorial/partials/diagrams/hilo-flow.puml new file mode 100644 index 0000000..cedaa9d --- /dev/null +++ b/modules/tutorial/partials/diagrams/hilo-flow.puml @@ -0,0 +1,10 @@ +@startuml +hide empty description + +[*] -right-> Index +Index -right-> Guess +Guess -> Guess +Guess --> GameOver +GameOver -right-> [*] + +@enduml
