diff --git a/README.html b/README.html deleted file mode 100644 index 04f0358b7..000000000 --- a/README.html +++ /dev/null @@ -1,14 +0,0 @@ -README -

What is this?

- -

This is the TicketMonster distribution, a showcase application for JBoss Developer Framework.

- -

What can you find here?

- -

The content of the underlying directories is as follows:

- - - diff --git a/README.md b/README.md index 0283f2f50..3d5e13308 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # What is this? -This is the TicketMonster distribution, a showcase application for [JBoss Developer Framework](http://jboss.org/jdf). +This is the Git repository for all things TicketMonster, a showcase application for [JBoss Developer Framework](http://jboss.org/jdf). ## What can you find here? The content of the underlying directories is as follows: * `demo` - the sources of TicketMonster application - you can build and run it! Follow the [instructions](demo/README.md). Or you can see it at work [here](http://ticketmonster-jdf.rhcloud.com). -* `cordova` - the sources of the TicketMonster Hybrid Mobile (Cordova) application. Follow the [instructions](cordova/README.md) to build and run it. \ No newline at end of file +* `cordova` - the sources of the TicketMonster Hybrid Mobile (Cordova) application. Follow the [instructions](cordova/README.md) to build and run it. +* `dist` - utility scripts for versioning and release +* `tutorial` - the sources of the TicketMonster tutorial, which describe how it works, how it's designed, and outlines a series of steps for *you* to build it. The tutorial is available in built form [here](http://www.jboss.org/ticket-monster/). \ No newline at end of file diff --git a/cordova/README.html b/cordova/README.html deleted file mode 100644 index 26d2530b1..000000000 --- a/cordova/README.html +++ /dev/null @@ -1,57 +0,0 @@ -README -

What is this?

- -

This is the Hybrid Mobile project for TicketMonster.

- -

Importing and running the project

- -

Prerequisites

- - - -

For running on an Android emulator:

- - - -

For running on an iOS simulator:

- - - -

If you need more detailed instruction to setup a iOS Development Environment with Apache Cordova, you can take a look at Setting up your development environment to use Apache Cordova

- -

Import the ticket-monster Code

- -

First we need to import the existing Hybrid Mobile project code to JBDS.

- -
    -
  1. In JBDS, click File then Import.
  2. -
  3. Select Import Cordova Project and click Next.
  4. -
  5. On Root Directory, click on Browse… button and navigate to the $TICKET-MONSTER_HOME/cordova/ directory on your filesystem.
  6. -
  7. After selecting the TicketMonster-Cordova project, you can click on Finish button to start the project import.
  8. -
  9. Make sure that $TICKET-MONSTER_HOME/cordova/www is a linked folder that resolves to ../demo/src/main/webapp.
  10. -
- -

Deploy the application on an Android Emulator

- -
    -
  1. Select your project in JBDS.
  2. -
  3. Click on Run, then Run As and Run on Android Emulator.
  4. -
- -

Deploy the application on an iOS Simulator

- -
    -
  1. Select your project in JBDS.
  2. -
  3. Click on Run, then Run As and Run on iOS Simulator.
  4. -
- diff --git a/demo/README.html b/demo/README.html deleted file mode 100644 index 9af1d6628..000000000 --- a/demo/README.html +++ /dev/null @@ -1,197 +0,0 @@ -README -

TicketMonster - a JBoss example

- -

TicketMonster is an online ticketing demo application that gets you started with JBoss technologies, and helps you learn and evaluate them.

- -

Here are a few instructions for building and running it. You can learn more about the example from the tutorial.

- -

Updating the Performance dates

- -

NOTE: This step is optional. It is necessary only if you want to update the dates of the Performances in the import.sql script in an automated manner. Updating the performance dates ensure that they are always set to some timestamp in the future, and ensures that all performances are visible in the Monitor section of the TicketMonster application.

- -
    -
  1. Run the update_import_sql Perl script. You’ll need the DateTime, DateTime::Format::Strptime and Tie::File Perl modules. These are usually available by default in your Perl installation.

    -
    $ perl update_import_sql.pl src/main/resources/import.sql
    -
  2. -
- -

Generating the administration site

- -

NOTE: This step is optional. The administration site is already present in the source code. If you want to regenerate it from Forge, and apply the changes outlined in the tutorial, you may continue to follow the steps outlined here. Otherwise, you can skip this step and proceed to build TicketMonster.

- -

Before building and running TicketMonster, you must generate the administration site with Forge.

- -
    -
  1. Ensure that you have JBoss Forge installed. The current version of TicketMonster supports version 2.6.0.Final or higher of JBoss Forge. JBoss Developer Studio 8 is recommended, since it contains JBoss Forge 2 with all the necessary plugins for the TicketMonster app.

  2. -
  3. Start the JBoss Forge console in JBoss Developer Studio. This can be done from the Forge Console view. If the view is not already visible, it can be opened through the ‘Window’ menu: Window -> Show View -> Other…. Select the ‘Forge Console’ item in the dialog to open the Forge Console. Click the Start button in the Forge Console tab, to start Forge.

  4. -
  5. From the JBoss Forge prompt, browse to the ‘demo’ directory of the TicketMonster sources and execute the script for generating the administration site

    -
    $ cd ticket-monster/demo
    -$ run admin_layer.fsh
    -
    -

    The git patches need to be applied manually. Both the patches are located in the patches sub-directory. To apply the manual changes, first apply the patch located in file admin_layer_functional.patch. Then perform the same for the file admin_layer_graphics.patch if you want to apply the style changes for the generated administration site. You can do so in JBoss Developer Studio, by opening the context-menu on the project (Right-click on the project) and then apply a git patch via Team -> Apply Patch…. Locate the patch file in the Workspace, select it and click the ‘Next’ button. In the next dialog, select to apply the patch on the ‘ticket-monster’ project in the workspace. Click Finish in the final page of the wizard after satisfying that the patch applies cleanly.

  6. -
  7. Deployment to JBoss EAP 6.3 is optional. The project can be built and deployed to a running instance of JBoss EAP through the following command in JBoss Forge:

    -
    $ build clean package jboss-as:deploy
    -
  8. -
- -

Building TicketMonster

- -

TicketMonster can be built from Maven, by runnning the following Maven command:

-
mvn clean package
-
-

Building TicketMonster with tests

- -

If you want to run the Arquillian tests as part of the build, you can enable one of the two available Arquillian profiles.

- -

For running the tests in an already running application server instance, use the arq-jbossas-remote profile.

-
mvn clean package -Parq-jbossas-remote
-
-

If you want the test runner to start an application server instance, use the arq-jbossas-managed profile. You must set up the JBOSS_HOME property to point to the server location, or update the src/main/test/resources/arquillian.xml file.

-
mvn clean package -Parq-jbossas-managed
-
-

Building TicketMonster with Postgresql (for OpenShift)

- -

If you intend to deploy into OpenShift, you can use the postgresql-openshift profile

-
mvn clean package -Ppostgresql-openshift
-
-

Building TicketMonster with MySQL (for OpenShift)

- -

If you intend to deploy into OpenShift, you can use the mysql-openshift profile

-
mvn clean package -Pmysql-openshift
-
-

Running TicketMonster

- -

You can run TicketMonster into a local JBoss EAP 6.3 instance or on OpenShift.

- -

Running TicketMonster locally

- -

Start JBoss Enterprise Application Platform 6.3

- -
    -
  1. Open a command line and navigate to the root of the JBoss server directory.
  2. -
  3. The following shows the command line to start the server with the web profile:

    -
    For Linux:   JBOSS_HOME/bin/standalone.sh
    -For Windows: JBOSS_HOME\bin\standalone.bat
    -
  4. -
- -

Deploy TicketMonster

- -
    -
  1. Make sure you have started the JBoss Server as described above.
  2. -
  3. Type this command to build and deploy the archive into a running server instance.

    -
    mvn clean package jboss-as:deploy
    -
    -

    (You can use the arq-jbossas-remote profile for running tests as well)

  4. -
  5. This will deploy target/ticket-monster.war to the running instance of the server.

  6. -
  7. Now you can see the application running at http://localhost:8080/ticket-monster

  8. -
- -

Running TicketMonster in OpenShift

- -

Create an OpenShift project

- -
-

The following variables are used in these instructions. Be sure to replace them as follows: -* APP_NAME should be replaced with the name of the application you create on OpenShift. -* YOUR_DOMAIN_NAME should be replaced with the OpenShift domain name. -* APPLICATION_UUID should be replaced with the UUID generated by OpenShift for your application, for example: 52864af85973ca430200006f -* TICKETMONSTER_MAVEN_PROJECT_ROOT is the location of the Maven project sources for the TicketMonster application.

-
- -
    -
  1. Open a shell command prompt and change to a directory of your choice. Enter the following command to create a JBoss EAP 6 application:

    -
    rhc app create -a APP_NAME -t jbosseap-6
    -
  2. -
- -

This command creates an OpenShift application named APP_NAME and will run the application inside the jbosseap-6 container. You should see some output similar to the following:

-
    Application Options
-    -------------------
-    Domain:     YOUR_DOMAIN
-    Cartridges: jbosseap-6 (addtl. costs may apply)
-    Gear Size:  default
-    Scaling:    no
-
-    Creating application 'APP_NAME' ... done
-
-
-    Waiting for your DNS name to be available ... done
-
-    Cloning into 'APP_NAME'...
-    Warning: Permanently added the RSA host key for IP address '54.90.10.115' to the list of known hosts.
-
-    Your application 'APP_NAME' is now available.
-
-      URL:        http://APP_NAME-YOUR_DOMAIN.rhcloud.com/
-      SSH to:     APPLICATION_UUID@APP_NAME-YOURDOMAIN.rhcloud.com
-      Git remote: ssh://APPLICATION_UUID@APP_NAME-YOUR_DOMAIN.rhcloud.com/~/git/APP_NAME.git/
-      Cloned to:  /Users/vineet/openshiftapps/APP_NAME
-
-    Run 'rhc show-app APP_NAME' for more details about your app.
-
-
    -
  1. The create command creates a git repository in the current directory with the same name as the application.
  2. -
- -

You do not need the generated default application, so navigate to the new git repository directory created by the OpenShift command and tell git to remove the source and pom files:

-
    cd APP_NAME
-    git rm -r src pom.xml
-
-
    -
  1. Copy the TicketMonster application sources into this new git repository:

    -
    cp -r TICKETMONSTER_MAVEN_PROJECT_ROOT/src .
    -cp -r TICKETMONSTER_MAVEN_PROJECT_ROOT/pom.xml .
    -
  2. -
- -

Use MySQL as the database

- -
    -
  1. Add the MySQL 5.5 cartridge to the ticketmonster application:

    -
    rhc cartridge add mysql-5.5 -a ticketmonster
    -
  2. -
  3. Configure the OpenShift build process, to use the mysql-openshift profile within the project POM. To do so, create a file named pre_build_jbosseap under the .openshift/action_hooks directory located in the git repository of the OpenShift application, with the following contents:

    -
    export MAVEN_ARGS="clean package -Popenshift,mysql-openshift -DskipTests"
    -
  4. -
  5. Set the executable bit for the action hook:

    -
    chmod +x TICKET_MONSTER_OPENSHIFT_GIT_REPO/.openshift/build_hooks/pre_build_jbosseap
    -
  6. -
- -

On Windows, you will need to run the following command to set the executable bit to the pre_build_jbosseap file:

-
    git update-index --chmod=+x .openshift/build_hooks/pre_build_jbosseap
-
-

Use PostgreSQL as the database

- -
    -
  1. Add the PostgreSQL 9.2 cartridge to the ticketmonster application:

    -
    rhc cartridge add postgresql-9.2 -a ticketmonster
    -
  2. -
  3. Configure the OpenShift build process, to use the postgresql-openshift profile within the project POM. To do so, create a file named pre_build_jbosseap under the .openshift/action_hooks directory located in the git repository of the OpenShift application, with the following contents:

    -
    export MAVEN_ARGS="clean package -Popenshift,postgresql-openshift -DskipTests"
    -
  4. -
  5. Set the executable bit for the action hook:

    -
    chmod +x TICKET_MONSTER_OPENSHIFT_GIT_REPO/.openshift/build_hooks/pre_build_jbosseap
    -
  6. -
- -

On Windows, you will need to run the following command to set the executable bit to the pre_build_jbosseap file:

-
    git update-index --chmod=+x .openshift/build_hooks/pre_build_jbosseap
-
-

Deploying to OpenShift

- -
    -
  1. You can now deploy the changes to your OpenShift application using git as follows:

    -
    git add -A
    -git commit -m "TicketMonster on OpenShift"
    -git push
    -
  2. -
- -

The final push command triggers the OpenShift infrastructure to build and deploy the changes.

- -

Note that the openshift profile in pom.xml is activated by OpenShift, and causes the WAR build by OpenShift to be copied to the deployments/ directory, and deployed without a context path.

- -

Now you can see the application running at http://APP_NAME-YOUR_DOMAIN.rhcloud.com/.

- diff --git a/demo/pom.xml b/demo/pom.xml index 2aab5b2c0..e66e3d22f 100644 --- a/demo/pom.xml +++ b/demo/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.jboss.examples ticket-monster - 2.7.0.ER2 + 2.7.0-SNAPSHOT war ticket-monster A starter HTML5 + REST webapp project for use on JBoss EAP. diff --git a/dist/README.dist.md b/dist/README.dist.md new file mode 100644 index 000000000..0283f2f50 --- /dev/null +++ b/dist/README.dist.md @@ -0,0 +1,10 @@ +# What is this? + +This is the TicketMonster distribution, a showcase application for [JBoss Developer Framework](http://jboss.org/jdf). + +## What can you find here? + +The content of the underlying directories is as follows: + +* `demo` - the sources of TicketMonster application - you can build and run it! Follow the [instructions](demo/README.md). Or you can see it at work [here](http://ticketmonster-jdf.rhcloud.com). +* `cordova` - the sources of the TicketMonster Hybrid Mobile (Cordova) application. Follow the [instructions](cordova/README.md) to build and run it. \ No newline at end of file diff --git a/dist/github-flavored-markdown.rb b/dist/github-flavored-markdown.rb new file mode 100755 index 000000000..fc5404f22 --- /dev/null +++ b/dist/github-flavored-markdown.rb @@ -0,0 +1,153 @@ +#!/usr/bin/env ruby +# +# JBoss, Home of Professional Open Source +# Copyright 2013, Red Hat, Inc. and/or its affiliates, and individual +# contributors by the @authors tag. See the copyright.txt in the +# distribution for a full listing of individual contributors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +require 'rubygems' +require 'redcarpet' +require 'nokogiri' +require 'fileutils' +require 'pygments.rb' +require 'rexml/document' + +# create a custom renderer that allows highlighting of code blocks +class HTMLWithPygmentsAndPants < Redcarpet::Render::HTML + include Redcarpet::Render::SmartyPants + def block_code(code, language) + Pygments.highlight(code, :lexer => language, :options => {:encoding => 'utf-8'}) + end + + #method copied from: https://gist.github.com/suan/5692767 + def header(title, level) + @headers ||= [] + + title_elements = REXML::Document.new(title) + flattened_title = title_elements.inject('') do |flattened, element| + flattened += if element.respond_to?(:text) + element.text + else + element.to_s + end + end + permalink = flattened_title.downcase.gsub(/[^a-z0-9\s]/, '').gsub(/\W+/, "-") + + # for extra credit: implement this as its own method + if @headers.include?(permalink) + permalink += "_1" + # my brain hurts + loop do + break if !@headers.include?(permalink) + # generate titles like foo-bar_1, foo-bar_2 + permalink.gsub!(/\_(\d+)$/, "_#{$1.to_i + 1}") + end + end + @headers << permalink + %(\n#{title}\n) + end +end + + +def find(p, tag) + if p.text + r = p.text[/^(#{tag}: )(.+)$/, 2] + if r + p['id'] = 'metadata' + return r + end + end +end + +def find_split(p, tag) + s = find(p, tag) + if s + return s.split(',').sort + end +end + +def metadata(source_path, html) + # TODO canonicalise path + toc_file='dist/target/toc.html' + # Markdown doesn't have an metadata syntax, so all we can do is pray ;-) + # Look for a paragraph that contains tags, which we define by convention + page_content = Nokogiri::HTML(html) + technologies = [] + level = "" + prerequisites = [] + summary = "" + page_content.css('p').each do |p| + t = find_split(p, 'Technologies') + if t + technologies = t + end + l = find(p, 'Level') + if l + level = l + end + pr = find_split(p, 'Prerequisites') + if pr + prerequisites = pr + end + s = find(p, 'Summary') + if s + summary = s + end + + end + dir = source_path[/([^\/]+)\/([^\/]+).md$/, 1] + filename = source_path[/([^\/]+)\/([^\/]+).md$/, 2] + if dir + output = "#{dir}#{' '.concat(technologies.map{|u| u} * ', ')}#{summary}#{level}#{' '.concat(prerequisites.map{|u| u} * ', ')}\n" + FileUtils.mkdir_p(File.dirname(toc_file)) + File.open(toc_file, 'a').write(output) + end +end + +def markdown(source_path) + renderer = HTMLWithPygmentsAndPants.new(optionize [ + :with_toc_data, + :xhtml + ]) + markdown = Redcarpet::Markdown.new(renderer, optionize([ + :fenced_code_blocks, + :no_intra_emphasis, + :tables, + :autolink, + :strikethrough, + :space_after_headers, + :with_toc_data + ])) + text = source_path.read + toc_file='dist/target/toc.html' + if File.exist?(toc_file) + qs_toc_content=File.open('dist/target/toc.html').read + qs_toc = "#{qs_toc_content}
Quickstart NameDemonstrated TechnologiesDescriptionExperience Level RequiredPrerequisites
" + text.gsub!("\[TOC-quickstart\]", qs_toc) + end + toc = Redcarpet::Markdown.new(Redcarpet::Render::HTML_TOC).render(text) + text.gsub!("\[TOC\]", toc) + rendered = markdown.render(text) + # metadata(source_path.path, rendered) + rendered = rendered.gsub(/README.md/, "README.html").gsub(/CONTRIBUTING.md/, "CONTRIBUTING.html") + 'README' + rendered + '' + end + +def optionize(options) + options.each_with_object({}) { |option, memo| memo[option] = true } +end + +puts markdown(ARGF) + diff --git a/dist/release-utils.sh b/dist/release-utils.sh new file mode 100755 index 000000000..e0db5d70f --- /dev/null +++ b/dist/release-utils.sh @@ -0,0 +1,155 @@ +#!/bin/bash + +REQUIRED_BASH_VERSION=3.0.0 + +if [[ $BASH_VERSION < $REQUIRED_BASH_VERSION ]]; then + echo "You must use Bash version 3 or newer to run this script" + exit +fi + +DIR=$(cd -P -- "$(dirname -- "$0")" && pwd -P) + + +# DEFINE + +VERSION_REGEX='([0-9]*)\.([0-9]*)([a-zA-Z0-9\.]*)' +FILEMGMT="jbossdeveloper@filemgmt.jboss.org:docs_htdocs" +URL_BASE="http://docs.jboss.org" + +# SCRIPT + +usage() +{ +cat << EOF +usage: $0 options + +This script aids in releasing TicketMonster + +OPTIONS: + -u Updates version numbers in all POMs, used with -o and -n + -t Updates timestamps in the import.sql script + -o Old version number to update from + -n New version number to update to + -p Publish docs for the given version + -h Shows this message +EOF +} + +parse_git_branch() { + git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' +} + +update() +{ + cd $DIR/../ + echo "Updating versions from $OLDVERSION TO $NEWVERSION for all Java and XML files under $PWD" + perl -pi -e "s/${OLDVERSION}/${NEWVERSION}/g" `find . -name \*.xml -or -name \*.java` +} + +update_timestamp() +{ + cd $DIR/../ + echo "Updating import.sql script with new dates" + perl $DIR/../demo/update_import_sql.pl $DIR/../demo/src/main/resources/import.sql +} + +publish_docs() +{ + if [[ $VERSION =~ $VERSION_REGEX ]]; then + MAJOR_VERSION=${BASH_REMATCH[1]} + MINOR_VERSION=${BASH_REMATCH[2]} + fi + + if [ "$MAJOR_VERSION" == "UNDEFINED" -o "$MINOR_VERSION" == "UNDEFINED" ] + then + echo "\nUnable to extract major and minor versions\n" + exit + fi + + BRANCH=$(parse_git_branch) + git checkout $VERSION + echo "Generating guide" + cd $DIR/../tutorial + ./generate-guides.sh + cd $DIR + git checkout $BRANCH + RPATH="jbossdeveloper/$MAJOR_VERSION.$MINOR_VERSION" + RFILE="ticket-monster-$VERSION" + SPATH="$DIR/../tutorial/target/guides" + LPATH="$DIR/target/upload" + + mkdir -p $LPATH/$RPATH + cp $SPATH/pdf/ticket-monster.pdf $LPATH/$RPATH/ticket-monster-$VERSION.pdf + cp $SPATH/epub/ticket-monster.epub $LPATH/$RPATH/ticket-monster-$VERSION.epub + + echo "Uploading guides to $URL_BASE/$RPATH" + rsync -Pvr --protocol=28 $LPATH/* $FILEMGMT + +} + +markdown_to_html() +{ + cd $DIR/../ + + # Loop through the directories and process them + subdirs=( cordova demo ) + for subdir in ${subdirs[@]} + do + readmes=`find $subdir -maxdepth 1 -iname readme.md` + for readme in $readmes + do + echo "Processing $readme" + output_filename=${readme//.md/.html} + output_filename=${output_filename//.MD/.html} + $DIR/github-flavored-markdown.rb $readme > $output_filename + done + done + # Now process the root readme + cd $DIR/../ + readme=README.md + echo "Processing $readme" + output_filename=${readme//.md/.html} + output_filename=${output_filename//.MD/.html} + $DIR/github-flavored-markdown.rb $readme > $output_filename +} + +OLDVERSION="1.0.0-SNAPSHOT" +NEWVERSION="1.0.0-SNAPSHOT" +VERSION="1.0.0-SNAPSHOT" +CMD="usage" + +while getopts "mhuo:n:p:t" OPTION + +do + case $OPTION in + u) + CMD="update" + ;; + t) + update_timestamp + ;; + h) + usage + exit + ;; + p) + VERSION=$OPTARG + CMD="publish_docs" + ;; + o) + OLDVERSION=$OPTARG + ;; + n) + NEWVERSION=$OPTARG + ;; + m) + CMD="markdown_to_html" + ;; + [?]) + usage + exit + ;; + esac +done + +$CMD diff --git a/dist/release.sh b/dist/release.sh new file mode 100755 index 000000000..822a5dbad --- /dev/null +++ b/dist/release.sh @@ -0,0 +1,151 @@ +#!/bin/bash + +REQUIRED_BASH_VERSION=3.0.0 + +if [[ $BASH_VERSION < $REQUIRED_BASH_VERSION ]]; then + echo "You must use Bash version 3 or newer to run this script" + exit +fi + +DIR=$(cd -P -- "$(dirname -- "$0")" && pwd -P) + +# DEFINE + +VERSION_REGEX='([0-9]*)\.([0-9]*)([a-zA-Z0-9\.]*)' + +# EAP team email subject +EMAIL_SUBJECT="\${RELEASEVERSION} of Ticket Monster released, please merge with https://github.com/jboss-eap/ticket-monster, tag and add to EAP maven repo build" +# EAP team email To ? +EMAIL_TO="pgier@redhat.com kpiwko@redhat.com lvogel@redhat.com" +EMAIL_FROM="\"JDF Publish Script\" " + + +# SCRIPT + +usage() +{ +cat << EOF +usage: $0 options + +This script performs a release of TicketMonster + +OPTIONS: + -s Snapshot version number to update from + -n New snapshot version number to update to, if undefined, defaults to the version number updated from + -r Release version number +EOF +} + +notify_email() +{ + echo "***** Performing Ticket Monster release notifications" + echo "*** Notifying JBoss EAP team" + subject=`eval echo $EMAIL_SUBJECT` + echo "Email from: " $EMAIL_FROM + echo "Email to: " $EMAIL_TO + echo "Subject: " $subject + # send email using sendmail + printf "Subject: $subject\nSee \$subject :)\n" | /usr/bin/env sendmail -f "$EMAIL_FROM" "$EMAIL_TO" + +} + +release() +{ + git reset --hard + echo "Releasing TicketMonster version $RELEASEVERSION" + default="Y" + read -p "Do you want to update the Performance dates in import.sql [Y/n]? " yn + yn=${yn:-$default} + case $yn in + [Yy] ) + $DIR/release-utils.sh -t -u -o $SNAPSHOTVERSION -n $RELEASEVERSION + ;; + [Nn] ) + $DIR/release-utils.sh -u -o $SNAPSHOTVERSION -n $RELEASEVERSION + ;; + *) echo "Invalid input" + ;; + esac + read -p "Do you want to create a WFK release [Y/n]? " wfk + wfk=${wfk:-$default} + if [[ $wfk = "Y" || $wfk = "y" ]] ; then + echo "Regenerating html from markdown" + mv $DIR/../README.md $DIR/../dist/README.orig.md + cp $DIR/../dist/README.dist.md $DIR/../README.md + $DIR/release-utils.sh -m + git ls-files --others $DIR/.. | grep '\README.html$' | xargs git add + + echo "Omitting files unnecessary for WFK distribution" + git rm --cached -r $DIR/../dist/ + git rm --cached -r $DIR/../tutorial/ + fi + git commit -a -m "Prepare for $RELEASEVERSION release" + git tag -a $RELEASEVERSION -m "Tag $RELEASEVERSION" + git branch $RELEASEVERSION tags/$RELEASEVERSION + $DIR/release-utils.sh -u -o $RELEASEVERSION -n $NEWSNAPSHOTVERSION + if [[ $wfk = "Y" || $wfk = "y" ]] ; then + echo "Adding files again..." + mv $DIR/../dist/README.orig.md $DIR/../README.md + git add $DIR/../dist/ + git add $DIR/../tutorial/ + + echo "Removing READMEs again..." + git ls-files $DIR/.. | grep '\README.html$' | xargs git rm + fi + git commit -a -m "Prepare for development of $NEWSNAPSHOTVERSION" + if [[ $wfk = "N" || $wfk = "n" ]] ; then + $DIR/release-utils.sh -p $RELEASEVERSION + fi + read -p "Do you want to send release notifcations to $EAP_EMAIL_TO[y/N]? " yn + case $yn in + [Yy] ) notify_email;; + * ) ;; + esac + echo "Don't forget to push the tag and the branch" + echo " git push --tags upstream refs/heads/$RELEASEVERSION" +} + +SNAPSHOTVERSION="UNDEFINED" +RELEASEVERSION="UNDEFINED" +NEWSNAPSHOTVERSION="UNDEFINED" +MAJOR_VERSION="UNDEFINED" +MINOR_VERSION="UNDEFINED" + +while getopts “hn:r:s:” OPTION + +do + case $OPTION in + h) + usage + exit + ;; + s) + SNAPSHOTVERSION=$OPTARG + ;; + r) + RELEASEVERSION=$OPTARG + ;; + n) + NEWSNAPSHOTVERSION=$OPTARG + ;; + [?]) + usage + exit + ;; + esac +done + +if [ "$NEWSNAPSHOTVERSION" == "UNDEFINED" ] +then + NEWSNAPSHOTVERSION=$SNAPSHOTVERSION +fi + +if [ "$SNAPSHOTVERSION" == "UNDEFINED" -o "$RELEASEVERSION" == "UNDEFINED" ] +then + echo "\nMust specify -r and -s\n" + usage + exit +fi + +release + diff --git a/tutorial/AdminHTML5.asciidoc b/tutorial/AdminHTML5.asciidoc new file mode 100644 index 000000000..c777e3de8 --- /dev/null +++ b/tutorial/AdminHTML5.asciidoc @@ -0,0 +1,534 @@ += Building the Administration UI using Forge +:Author: Pete Muir +:thumbnail: http://static.jboss.org/ffe/1/www/origin/ticket-monster-splash-2.png + +== What Will You Learn Here? + + +You've just defined the domain model of your application, and all the entities managed directly by the end-users. Now it's time to build an administration GUI for the TicketMonster application using JAX-RS and AngularJS. After reading this guide, you'll understand how to use JBoss Forge to create the JAX-RS resources from the entities and how to create an AngularJS based UI. + +We'll round out the guide by revealing the required, yet short and sweet, configuration. + +The tutorial will show you how to perform all these steps in JBoss Developer Studio, including screenshots that guide you through. + + +== Setting up Forge + + +=== JBoss Developer Studio + + +Forge is available in JBoss Developer Studio 8. You would have already used Forge in the Introductory chapter. + +You can start Forge in JBoss Developer Studio, using the *Ctrl + 4* (Windows/Linux) or *Cmd + 4* (Mac OS X) key stroke combination. This would launch the Forge action menu from where you can choose the desired commands to run in a particular context. + +Or alternatively, to use the Forge Console, navigate to _Window -> Show View -> Other_, locate _Forge Console_ and click _OK_. Then click the _Start_ button in top right corner of the view. + + +== Getting started with Forge + + +Forge is a powerful rapid application development (aimed at Java EE 6) and project comprehension tool. It can operate both on projects it creates, and on existing projects, such as TicketMonster. If you want to learn more about Forge, head over to the link:http://forge.jboss.org[JBoss Forge site]. + +Forge can scaffold an entire app for you from a set of existing resources. For instance, it can generate a HTML5 scaffold with RESTful services, based on existing JPA entities. We shall see how to use this feature to generate the administration section of the TicketMonster application. + +== Generating the CRUD UI + + +.Forge Scripts +************************************************************************************* +Forge supports the execution of scripts. The generation of the CRUD UI is provided +as a Forge script in TicketMonster, so you don't need to type the commands everytime +you want to regenerate the Admin UI. The script will also prompt you to apply all +changes to the generated CRUD UI that listed later in this chapter. This would relieve +us of the need to manually type in the changes. + +To run the script: + + run admin_layer.fsh +************************************************************************************* + + +=== Scaffold the AngularJS UI from the JPA entities + +Scaffolding capabilities are available through the "Scaffold: Setup" and "Scaffold: Generate" commands in the Forge action menu. The first command is used to set up the pre-requisites for a scaffold in a project - usually static files and libraries that can be installed separately and are not modified by subsequent scaffolding operations. The second command is used to generate various source files in a project, based on some input files (in this case JPA entities). + +In the case of the AngularJS scaffold, an entire CRUD app (a HTML5 UI with a RESTful backend using a database) can be generated from JPA entities. + +Forge can detect whether the scaffold was initially setup during scaffold generation and adjust for missing capabilities in the project. Let's therefore go ahead and launch the "Scaffold: Generate" command from the Forge action menu: + + +[[project_scaffold_generate_in_menu]] +.Filter the `Scaffold: Generate` command in the menu +image::gfx/forge_scaffold_generate_action_menu.png[] + +We're now prompted to select which scaffold to generate. Forge supports AngularJS and JSF out of the box. Choose `AngularJS`. The generated scaffold can be placed in any directory under the web root path (which corresponds to the `src/main/webapp` directory of the project). We'll choose to generate the scaffold in the `admin` directory. + +[[project_scaffold_generate]] +.Launch the `Scaffold: Generate` command +image::gfx/forge_scaffold_generate.png[] + +[[project_scaffold_generate_input_webroot]] +.Select the scaffold to generate and the web root path +image::gfx/forge_scaffold_generate_input_webroot.png[] + +Click the `Next` button, and proceed to choose the JPA entities that we would use as the basis for the scaffold. You can either scaffold the entities one-by-one, which allows you to control which UIs are generated, or you can generate a CRUD UI for all the entities. We'll do the latter. We'll also choose to generate REST resources for the entities, since the existing REST resources are not suitable for CRUD operations: + +[[project_scaffold_generate_select_entities]] +.Select the JPA entities to use for generation +image::gfx/forge_scaffold_generate_select_entities.png[] + +Click the `Next` button, to configure the nature of the REST resources generated by the scaffold. Multiple strategies exist in Forge for generating REST resources from JPA entities. We'll choose the option to generate and expose DTOs for the JPA entities, since it is more suitable for the TicketMonster object model. Provide a value of `org.jboss.examples.ticketmonster.rest` as the target package for the generated REST resources, if not already specified. Click `Finish` to generate the scaffold. + +[[project_scaffold_generate_rest_resources]] +.Choose the REST resource generation strategy +image::gfx/forge_scaffold_generate_choose_rest_strategy.png[] + +[NOTE] +============================================================== +The `Root and Nested DTO` resource representation enables Forge to create REST resources for complex object graphs without adding Jackson annotations to avoid cycles in the graph. Without this constrained representation, one would have to add annotations like `@JsonIgnore` (to ignore certain undesirable object properties), or `@JsonIdentity` (to represent cycles in JSON without succumbing to StackOverflowErrors or similar such errors/exceptions). +============================================================== + +The scaffold generation command performs a multitude of activities, depending on the previous state of the project: + +* It copies the css, images and JavaScript libraries used by the scaffold, to the project. It does this if you did not setup the scaffold in a separate step (this is optional; the generate command will do this for you). +* It generates JAX-RS resources for all the JPA entities in the project. The resources would be represented in JSON to enable the AngularJS-based front-end to communicate with the backend services. Each resource representation is structured to contain the representation of the corresponding JPA entity (the root) and any associated entities (that are represneted as nested objects). +* It generates the AngularJS-based front-end that contains HTML based Angular templates along with AngularJS factories, services and controllers. + +We now have a database-driven CRUD UI for all the entities used in TicketMonster! + + +== Test the CRUD UI + + +Let's test our UI on our local JBoss AS instance. As usual, we'll build and deploy using Maven: + +---- +mvn clean package jboss-as:deploy +---- + +== Make some changes to the UI + +Let’s add support for images to the Admin UI. `Events` and `Venues` have `MediaItem`s associated with them, but they're only displayed as URLs. Let's display the corresponding images in the AngularJS views, by adding the required bindings: + +.src/main/webapp/admin/views/Event/detail.html +[source,html] +------------------------------------------------------------------------------------------ + ... +
+ +
+ +
+ ... +------------------------------------------------------------------------------------------ + +.src/main/webapp/admin/views/Venue/detail.html +[source,html] +------------------------------------------------------------------------------------------ + ... +
+ +
+ +
+ ... +------------------------------------------------------------------------------------------ + +Now that the bindings are set, we'll modify the underlying controllers to provide the URL of the MediaItem when the `{{mediaItemSelection.text}}` expression is evaluated: + +.src/main/webapp/admin/scripts/scripts/controllers/editEventController.js +[source,html] +------------------------------------------------------------------------------------------ +... + MediaItemResource.queryAll(function(items) { + $scope.mediaItemSelectionList = $.map(items, function(item) { + ... + var labelObject = { + value : item.id, + text : item.url + }; + ... + }); + }); +... +------------------------------------------------------------------------------------------ + +.src/main/webapp/admin/scripts/scripts/controllers/editVenueController.js +[source,html] +------------------------------------------------------------------------------------------ +... + MediaItemResource.queryAll(function(items) { + $scope.mediaItemSelectionList = $.map(items, function(item) { + ... + var labelObject = { + value : item.id, + text : item.url + }; + ... + }); + }); +... +------------------------------------------------------------------------------------------ + +The admin site will now display the corresponding image if a media item is associated with the venue or event. + +[TIP] +============================================================== +The location of the MediaItem is present in the `text` property of the `mediaItemSelection` object. +The parameter to the `ngSrc` directive is set to this value. This ensures that the browser fetches the image present at this location. +The expression `src={{mediaItemSelection.text}}` should be avoided since the browser would attempt to fetch the URL with the literal text `{{hash}}` before AngularJS replaces the expression with the actual URL. +============================================================== + + +Let's also modify the UI to make it more user-friendly. Shows and Performances are displayed in a non-intuitive manner at the moment. Shows are displayed as their object identities, while performances are displayed as date-time values. This makes it difficult to identify them in the views. Let's modify the UI to display more semantically useful values. + +These values will be computed at the server-side, since these are already available in the `toString()` implementations of these classes. This would be accomplished by adding a read-only property `displayTitle` to the `Show` and `Performance` REST resource representations: + +.src/main/java/org/jboss/examples/ticketmonster/rest/dto/ShowDTO.java +[source,java] +------------------------------------------------------------------------------------------ + ... + private Set performances = new HashSet(); + private NestedVenueDTO venue; + private String displayTitle; + + public ShowDTO() + ... + } + this.venue = new NestedVenueDTO(entity.getVenue()); + this.displayTitle = entity.toString(); + } + } + ... + public String getDisplayTitle() + { + return this.displayTitle; + } +} +------------------------------------------------------------------------------------------ + +.src/main/java/org/jboss/examples/ticketmonster/rest/dto/PerformanceDTO.java +[source,java] +------------------------------------------------------------------------------------------ + ... + private NestedShowDTO show; + private Date date; + private String displayTitle; + + public PerformanceDTO() + ... + this.show = new NestedShowDTO(entity.getShow()); + this.date = entity.getDate(); + this.displayTitle = entity.toString(); + } + } + ... + public String getDisplayTitle() + { + return this.displayTitle; + } +} +------------------------------------------------------------------------------------------ + +And let us do the same for the nested representations: + +.src/main/java/org/jboss/examples/ticketmonster/rest/dto/NestedPerformanceDTO.java +[source,java] +------------------------------------------------------------------------------------------ + ... + private Long id; + private Date date; + private String displayTitle; + + public NestedPerformanceDTO() + ... + this.id = entity.getId(); + this.date = entity.getDate(); + this.displayTitle = entity.toString(); + } + } + ... + public String getDisplayTitle() + { + return this.displayTitle; + } +} +------------------------------------------------------------------------------------------ + +.src/main/java/org/jboss/examples/ticketmonster/rest/dto/NestedShowDTO.java +[source,java] +------------------------------------------------------------------------------------------ + ... + private Long id; + private String displayTitle; + + public NestedShowDTO() + ... + { + this.id = entity.getId(); + this.displayTitle = entity.toString(); + } + } + ... + public String getDisplayTitle() + { + return this.displayTitle; + } +} +------------------------------------------------------------------------------------------ + +We shall now proceed to modify the AngularJS views to use the new properties in the resource representations: + +.src/main/webapp/admin/scripts/controllers/editPerformanceController.js +[source,javascript] +------------------------------------------------------------------------------------------ + ... + var labelObject = { + value : item.id, + text : item.displayTitle + }; + if($scope.performance.show && item.id == $scope.performance.show.id) { + ... +------------------------------------------------------------------------------------------ + +.src/main/webapp/admin/scripts/controllers/editSectionAllocationController.js +[source,javascript] +------------------------------------------------------------------------------------------ + ... + var labelObject = { + value : item.id, + text : item.displayTitle + }; + if($scope.sectionAllocation.performance && item.id == $scope.sectionAllocation.performance.id) { + ... +------------------------------------------------------------------------------------------ + +.src/main/webapp/admin/scripts/controllers/editShowController.js +[source,javascript] +------------------------------------------------------------------------------------------ + ... + var labelObject = { + value : item.id, + text : item.displayTitle + }; + if($scope.show.performances){ + ... +------------------------------------------------------------------------------------------ + +.src/main/webapp/admin/scripts/controllers/editTicketPriceController.js +[source,javascript] +------------------------------------------------------------------------------------------ + ... + var labelObject = { + value : item.id, + text : item.displayTitle + }; + if($scope.ticketPrice.show && item.id == $scope.ticketPrice.show.id) { + ... +------------------------------------------------------------------------------------------ + +.src/main/webapp/admin/scripts/controllers/newPerformanceController.js +[source,javascript] +------------------------------------------------------------------------------------------ + ... + $scope.showSelectionList = $.map(items, function(item) { + return ( { + value : item.id, + text : item.displayTitle + }); + }); + ... +------------------------------------------------------------------------------------------ + +.src/main/webapp/admin/scripts/controllers/newSectionAllocationController.js +[source,javascript] +------------------------------------------------------------------------------------------ + ... + $scope.performanceSelectionList = $.map(items, function(item) { + return ( { + value : item.id, + text : item.displayTitle + }); + }); + ... +------------------------------------------------------------------------------------------ + +.src/main/webapp/admin/scripts/controllers/newShowController.js +[source,javascript] +------------------------------------------------------------------------------------------ + ... + $scope.performancesSelectionList = $.map(items, function(item) { + return ( { + value : item.id, + text : item.displayTitle + }); + }); + ... +------------------------------------------------------------------------------------------ + +.src/main/webapp/admin/scripts/controllers/newTicketPriceController.js +[source,javascript] +------------------------------------------------------------------------------------------ + ... + $scope.showSelectionList = $.map(items, function(item) { + return ( { + value : item.id, + text : item.displayTitle + }); + }); + ... +------------------------------------------------------------------------------------------ + +.src/main/webapp/admin/views/Performance/search.html +[source,html] +------------------------------------------------------------------------------------------ + +
+ + ... + + + {{result.show.displayTitle}} + {{result.date| date:'yyyy-MM-dd HH:mm:ss Z'}} + +------------------------------------------------------------------------------------------ + +.src/main/webapp/admin/views/SectionAllocation/search.html +[source,html] +------------------------------------------------------------------------------------------ + +
+ + ... + + + {{result.occupiedCount}} + {{result.performance.displayTitle}} + {{result.section.name}} + +------------------------------------------------------------------------------------------ + +.src/main/webapp/admin/views/TicketPrice/search.html +[source,html] +------------------------------------------------------------------------------------------ + +
+ + ... + + + {{result.show.displayTitle}} + {{result.section.name}} + {{result.ticketCategory.description}} +------------------------------------------------------------------------------------------ + + +=== Fixing the landing page of the Administration site + +The generated administration site contains a landing page - `app.html` that works well as a standalone site. +However, we need to fix this page to make it work with the rest of the site. + +For brevity, the significant sections of the corrected page are listed below: + +.src/main/webapp/admin/app.html +[source,html] +------------------------------------------------------------------------------------------ + + + + + + Ticket-monster + + + + + + + +
+ + + + +
+ + ... + +
+ + ... + + + +------------------------------------------------------------------------------------------ + +It is sufficient to copy the corrected page from the project sources. Additionally, do not forget to copy the `src/main/webapp/admin/styles/custom-forge.css` file, that we now reference it in the corrected page. + + +== Updating the ShrinkWrap deployment for the test suite + +We've added classes to the project that should be in the ShrinkWrap deployment used in the test suite. Let us update the ShrinkWrap deployment to reflect this. + +.src/test/java/org/jboss/examples/ticketmonster/test/rest/RESTDeployment.java +[source,java] +------------------------------------------------------------------------------------------ +public class RESTDeployment { + + public static WebArchive deployment() { + return TicketMonsterDeployment.deployment() + .addPackage(Booking.class.getPackage()) + .addPackage(BaseEntityService.class.getPackage()) + .addPackage(MultivaluedHashMap.class.getPackage()) + .addPackage(SeatAllocationService.class.getPackage()) + .addPackage(VenueDTO.class.getPackage()); + } + +} +------------------------------------------------------------------------------------------ + +We can test these changes by executing + +---- +mvn clean test -Parq-jbossas-managed +---- + +or (against an already running JBoss EAP 6.2 instance) + +---- +mvn clean test -Parq-jbossas-remote +---- + +as usual. diff --git a/tutorial/BusinessLogic.asciidoc b/tutorial/BusinessLogic.asciidoc new file mode 100644 index 000000000..2071dc03b --- /dev/null +++ b/tutorial/BusinessLogic.asciidoc @@ -0,0 +1,1686 @@ +[[BuildingBusinessServices]] += Building The Business Services With JAX-RS +:Author: Marius Bogoevici +:thumbnail: http://static.jboss.org/ffe/1/www/origin/ticket-monster-splash-2.png + +== What Will You Learn Here? + + +We've just defined the domain model of the application and created its persistence layer. Now we need to define the services that implement the business logic of the application and expose them to the front-end. After reading this, you'll understand how to design the business layer and what choices to make while developing it. Topics covered include: + +* Encapsulating business logic in services and integrating with the persistence tier +* Using CDI for integrating individual services +* Integration testing using Arquillian +* Exposing RESTful services via JAX-RS + +The tutorial will show you how to perform all these steps in JBoss Developer Studio, including screenshots that guide you through. + +== Business Services And Their Relationships + + +TicketMonster's business logic is implemented by a number of classes, with different responsibilities: + +* managing media items +* allocating tickets +* handling information on ticket availability +* remote access through a RESTful interface + +The services are consumed by various other layers of the application: + +* the media management and ticket allocation services encapsulate complex functionality, which in turn is exposed externally by RESTful services that wrap them +* RESTful services are mainly used by the HTML5 view layer +* the ticket availability service is used by the HTML5 and JavaScript based monitor + +[TIP] +.Where to draw the line? +===================================================================================== +A business service is an encapsulated, reusable logical component that groups +together a number of well-defined cohesive business operations. Business services +perform business operations, and may coordinate infrastructure services such as +persistence units, or even other business services as well. The boundaries drawn +between them should take into account whether the newly created services represent +, potentially reusable components. +===================================================================================== + +As you can see, some of the services are intended to be consumed within the business layer of the application, while others provide an external interface as JAX-RS services. We will start by implementing the former, and we'll finish up with the latter. During this process, you will +discover how CDI, EJB and JAX-RS make it easy to define and wire together our services. + +== Preparations + + +=== Adding Jackson Core + + +The first step for setting up our service architecture is to add Jackson Core as a dependency in the project. Adding Jackson Core as a provided dependency will enable you to use the Jackson annotations in the project. This is necessary to obtain a certain degree of control over the content of the JSON responses. We can bring in the same version of Jackson Core as the one used in RESTEasy, by adding `org.jboss.resteasy:resteasy-jackson-provider` and `org.jboss.resteasy:resteasy-jaxrs` as provided-scope dependencies, through the `org.jboss.bom.eap:jboss-javaee-6.0-with-resteasy` BOM. The versions of these dependencies would depend on the version of the JBoss BOMs we use in our project. Using the same version of the JBoss BOM as the one we will deploy to production, will ensure that we use the right dependencies during compilation and build. + +.pom.xml +[source,xml] +--------------------------------------------------------------------------------- + + ... + + + ... + + + org.jboss.bom.eap + jboss-javaee-6.0-with-resteasy + ${version.jboss.bom.eap} + pom + import + + + + + + ... + + + + org.jboss.resteasy + resteasy-jackson-provider + provided + + + org.jboss.resteasy + resteasy-jaxrs + provided + + + ... + +--------------------------------------------------------------------------------- + +[NOTE] +.Why do you need the Jackson annotations? +======================================================================================= +JAX-RS does not specify mediatype-agnostic annotations for certain use cases. You will +encounter atleast one of them in the project. The object graph contains +cyclic/bi-directional relationships among entities like `Venue`, `Section`, `Show`, +`Performance` and `TicketPrice`. JSON representations for these objects will need +tweaking to avoid stack oVerflow errors and the like, at runtime. + +JBoss Enterprise Application 6 uses Jackson to perform serialization and +dserialization of objects, thus requiring use of Jackson annotations to modify this +behavior. `@JsonIgnoreProperties` from Jackson will be used to suppress serialization +and deserialization of one of the fields involved in the cycle. +======================================================================================= + +=== Verifying the versions of the JBoss BOMs + +The next step is to verify if we're using the right version of the JBoss BOMs in the project. +Using the right versions of the BOMs ensures that you work against a known set of tested dependencies. +Verify that the property `version.jboss.bom.eap` contains the value `6.3.1.GA` or higher: + +.pom.xml +[source,xml] +--------------------------------------------------------------------------------- + + ... + + ... + 6.3.1.GA + ... + + ... + +--------------------------------------------------------------------------------- + + +=== Enabling CDI + + +The next step is to enable CDI in the deployment by creating a `beans.xml` file in the `WEB-INF` folder of the web application. + +.src/main/webapp/WEB-INF/beans.xml +[source,xml] +------------------------------------------------------------------------------------------ + + +------------------------------------------------------------------------------------------ + +[NOTE] +.If you used the Maven archetype +===================================================================================== +If you used the Maven archetype to create the project, this file will exist already +in the project - it is added automatically. +===================================================================================== + +You may wonder why the file is empty! Whilst `beans.xml` can specify various deployment-time configuration (e.g. activation of interceptors, +decorators or alternatives), it can also act as a marker file, telling the container to enable CDI for the deployment (which it doesn't do, unless `beans.xml` is present). + +[TIP] +.Contexts and Dependency Injection (CDI) +===================================================================================== +As it's name suggests, CDI is the contexts and dependency injection standard for Java +EE. By enabling CDI in your application, deployed classes become managed components +and their lifecycle and wiring becomes the responsibility of the Java EE server. + +In this way, we can reduce coupling between components, which is a requirement o a +well-designed architecture. Now, we can focus on implementing the responsibilities of +the components and describing their dependencies in a declarative fashion. The +runtime will do the rest for you: instantiating and wiring them together, as well as +disposing of them as needed. +===================================================================================== + +=== Adding utility classes + + +Next, we add some helper classes providing low-level utilities for the application. We won't get in their implementation details here, but you can study their source code for details. + +Copy the following classes from the original example to `src/main/java/org/jboss/examples/ticketmonster/util`: + +* `Base64` +* `CircularBuffer` +* `ForwardingMap` +* `MultivaluedHashMap` +* `Reflections` +* `Resources` + +== Internal Services + + +We begin the service implementation by implementing some helper services. + +=== The Media Manager + + +First, let's add support for managing media items, such as images. The persistence layer simply stores URLs, referencing media items stored by online services. The URL look like link:http://dl.dropbox.com/u/65660684/640px-Roy_Thomson_Hall_Toronto.jpg[]. + +Now, we could use the URLs in our application, and retrieve these media items from the provider. However, we would prefer to cache these media items in order to improve application performance and increase resilience to external failures - this will allow us to run the application +successfully even if the provider is down. The `MediaManager` is a good illustration of a business service; it performs the retrieval and caching of media objects, encapsulating the operation from the rest of the application. + +We begin by creating `MediaManager`: + +.src/main/java/org/jboss/examples/ticketmonster/service/MediaManager.java +[source,java] +------------------------------------------------------------------------------------------ +/** + *

+ * The media manager is responsible for taking a media item, and returning either the URL + * of the cached version (if the application cannot load the item from the URL), or the + * original URL. + *

+ * + *

+ * The media manager also transparently caches the media items on first load. + *

+ * + *

+ * The computed URLs are cached for the duration of a request. This provides a good balance + * between consuming heap space, and computational time. + *

+ * + */ +public class MediaManager { + + /** + * Locate the tmp directory for the machine + */ + private static final File tmpDir; + + static { + String dataDir = System.getenv("OPENSHIFT_DATA_DIR"); + String parentDir = dataDir != null ? dataDir : System.getProperty("java.io.tmpdir"); + tmpDir = new File(parentDir, "org.jboss.examples.ticket-monster"); + if (tmpDir.exists()) { + if (tmpDir.isFile()) + throw new IllegalStateException(tmpDir.getAbsolutePath() + " already exists, and is a file. Remove it."); + } else { + tmpDir.mkdir(); + } + } + + /** + * A request scoped cache of computed URLs of media items. + */ + private final Map cache; + + public MediaManager() { + + this.cache = new HashMap(); + } + + /** + * Load a cached file by name + * + * @param fileName + * @return + */ + public File getCachedFile(String fileName) { + return new File(tmpDir, fileName); + } + + /** + * Obtain the URL of the media item. If the URL h has already been computed in this + * request, it will be looked up in the request scoped cache, otherwise it will be + * computed, and placed in the request scoped cache. + */ + public MediaPath getPath(MediaItem mediaItem) { + if (cache.containsKey(mediaItem)) { + return cache.get(mediaItem); + } else { + MediaPath mediaPath = createPath(mediaItem); + cache.put(mediaItem, mediaPath); + return mediaPath; + } + } + + /** + * Compute the URL to a media item. If the media item is not cacheable, then, as long + * as the resource can be loaded, the original URL is returned. If the resource is not + * available, then a placeholder image replaces it. If the media item is cachable, it + * is first cached in the tmp directory, and then path to load it is returned. + */ + private MediaPath createPath(MediaItem mediaItem) { + if(mediaItem == null) { + return createCachedMedia(Reflections.getResource("not_available.jpg").toExternalForm(), IMAGE); + } else if (!mediaItem.getMediaType().isCacheable()) { + if (checkResourceAvailable(mediaItem)) { + return new MediaPath(mediaItem.getUrl(), false, mediaItem.getMediaType()); + } else { + return createCachedMedia(Reflections.getResource("not_available.jpg").toExternalForm(), IMAGE); + } + } else { + return createCachedMedia(mediaItem); + } + } + + /** + * Check if a media item can be loaded from it's URL, using the JDK URLConnection classes. + */ + private boolean checkResourceAvailable(MediaItem mediaItem) { + URL url = null; + try { + url = new URL(mediaItem.getUrl()); + } catch (MalformedURLException e) { + } + + if (url != null) { + try { + URLConnection connection = url.openConnection(); + if (connection instanceof HttpURLConnection) { + return ((HttpURLConnection) connection).getResponseCode() == HttpURLConnection.HTTP_OK; + } else { + return connection.getContentLength() > 0; + } + } catch (IOException e) { + } + } + return false; + } + + /** + * The cached file name is a base64 encoded version of the URL. This means we don't need to maintain a database of cached + * files. + */ + private String getCachedFileName(String url) { + return Base64.encodeToString(url.getBytes(), false); + } + + /** + * Check to see if the file is already cached. + */ + private boolean alreadyCached(String cachedFileName) { + File cache = getCachedFile(cachedFileName); + if (cache.exists()) { + if (cache.isDirectory()) { + throw new IllegalStateException(cache.getAbsolutePath() + " already exists, and is a directory. Remove it."); + } + return true; + } else { + return false; + } + } + + /** + * To cache a media item we first load it from the net, then write it to disk. + */ + private MediaPath createCachedMedia(String url, MediaType mediaType) { + String cachedFileName = getCachedFileName(url); + if (!alreadyCached(cachedFileName)) { + URL _url = null; + try { + _url = new URL(url); + } catch (MalformedURLException e) { + throw new IllegalStateException("Error reading URL " + url); + } + + try { + InputStream is = null; + OutputStream os = null; + try { + is = new BufferedInputStream(_url.openStream()); + os = new BufferedOutputStream(getCachedOutputStream(cachedFileName)); + while (true) { + int data = is.read(); + if (data == -1) + break; + os.write(data); + } + } finally { + if (is != null) + is.close(); + if (os != null) + os.close(); + } + } catch (IOException e) { + throw new IllegalStateException("Error caching " + mediaType.getDescription(), e); + } + } + return new MediaPath(cachedFileName, true, mediaType); + } + + private MediaPath createCachedMedia(MediaItem mediaItem) { + return createCachedMedia(mediaItem.getUrl(), mediaItem.getMediaType()); + } + + private OutputStream getCachedOutputStream(String fileName) { + try { + return new FileOutputStream(getCachedFile(fileName)); + } catch (FileNotFoundException e) { + throw new IllegalStateException("Error creating cached file", e); + } + } + +} +------------------------------------------------------------------------------------------ + +The service delegates to a number of internal methods that do the heavy lifting, but exposes a simple API, to the external observer it simply converts the `MediaItem` entities into `MediaPath` data structures, that can be used by the application to load the binary data of the media item. The service will retrieve and cache the data locally in the filesystem, if possible (e.g. streamed videos aren't cacheable!). + +.src/main/java/org/jboss/examples/ticketmonster/service/MediaPath.java +[source,java] +------------------------------------------------------------------------------------------ +public class MediaPath { + + private final String url; + private final boolean cached; + private final MediaType mediaType; + + public MediaPath(String url, boolean cached, MediaType mediaType) { + this.url = url; + this.cached = cached; + this.mediaType = mediaType; + } + + public String getUrl() { + return url; + } + + public boolean isCached() { + return cached; + } + + public MediaType getMediaType() { + return mediaType; + } + +} +------------------------------------------------------------------------------------------ + +The service can be injected by type into the components that depend on it. + +We should also control the lifecycle of this service. The `MediaManager` stores request-specific state, so should be scoped to the web request, the CDI `@RequestScoped` is perfect. + +.src/main/java/org/jboss/examples/ticketmonster/service/MediaManager.java +[source,java] +------------------------------------------------------------------------------------------ + ... +@RequestScoped +public class MediaManager { + ... +} +------------------------------------------------------------------------------------------ + +=== The Seat Allocation Service + + +The seat allocation service finds free seats at booking time, in a given section of the venue. It is a good example of how a service can coordinate infrastructure services (using the injected persistence unit to get access to the `SeatAllocation` instance) and domain objects (by invoking the `allocateSeats` method on a concrete allocation instance). + +Isolating this functionality in a service class makes it possible to write simpler, self-explanatory code in the layers above and opens the possibility of replacing this code at a later date with a more advanced implementation (for example one using an in-memory cache). + +.src/main/java/org/jboss/examples/ticketmonster/service/SeatAllocationService.java +[source,java] +------------------------------------------------------------------------------------------ +@SuppressWarnings("serial") +public class SeatAllocationService implements Serializable { + + @Inject + EntityManager entityManager; + + public AllocatedSeats allocateSeats(Section section, Performance performance, int seatCount, boolean contiguous) { + SectionAllocation sectionAllocation = retrieveSectionAllocationExclusively(section, performance); + List seats = sectionAllocation.allocateSeats(seatCount, contiguous); + return new AllocatedSeats(sectionAllocation, seats); + } + + public void deallocateSeats(Section section, Performance performance, List seats) { + SectionAllocation sectionAllocation = retrieveSectionAllocationExclusively(section, performance); + for (Seat seat : seats) { + if (!seat.getSection().equals(section)) { + throw new SeatAllocationException("All seats must be in the same section!"); + } + sectionAllocation.deallocate(seat); + } + } + + private SectionAllocation retrieveSectionAllocationExclusively(Section section, Performance performance) { + SectionAllocation sectionAllocationStatus = null; + try { + sectionAllocationStatus = (SectionAllocation) entityManager.createQuery( + "select s from SectionAllocation s where " + + "s.performance.id = :performanceId and " + + "s.section.id = :sectionId") + .setParameter("performanceId", performance.getId()) + .setParameter("sectionId", section.getId()) + .getSingleResult(); + } catch (NoResultException noSectionEx) { + // Create the SectionAllocation since it doesn't exist + sectionAllocationStatus = new SectionAllocation(performance, section); + entityManager.persist(sectionAllocationStatus); + entityManager.flush(); + } + entityManager.lock(sectionAllocationStatus, LockModeType.PESSIMISTIC_WRITE); + return sectionAllocationStatus; + } +} +------------------------------------------------------------------------------------------ + +Next, we define the `AllocatedSeats` class that we use for storing seat reservations for a booking, before they are made persistent. + +.src/main/java/org/jboss/examples/ticketmonster/service/AllocatedSeats.java +[source,java] +------------------------------------------------------------------------------------------ +public class AllocatedSeats { + + private final SectionAllocation sectionAllocation; + + private final List seats; + + public AllocatedSeats(SectionAllocation sectionAllocation, List seats) { + this.sectionAllocation = sectionAllocation; + this.seats = seats; + } + + public SectionAllocation getSectionAllocation() { + return sectionAllocation; + } + + public List getSeats() { + return seats; + } + + public void markOccupied() { + sectionAllocation.markOccupied(seats); + } +} +------------------------------------------------------------------------------------------ + + +== JAX-RS Services + + +The majority of services in the application are JAX-RS web services. They are critical part of the design, as they next service is used for provide communication with the HTML5 view layer. The JAX-RS services range from simple CRUD to processing bookings and media items. + +To pass data across the wire we use JSON as the data marshalling format, as it is less verbose and easier to process than XML by the JavaScript client-side framework. + +=== Initializing JAX-RS + +We shall ensure that the required dependencies are present in the project POM, to setup JAX-RS in the project: + +.pom.xml +[source,xml] +------------------------------------------------------------------------------------------ + + + ... + + ... + + org.jboss.spec.javax.ws.rs + jboss-jaxrs-api_1.1_spec + provided + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.0_spec + provided + + + ... + +------------------------------------------------------------------------------------------ + +Some of these may already be present in the project POM, and should not be added again. + +To activate JAX-RS we add the class below, which instructs the container to look for JAX-RS +annotated classes and install them as endpoints. This class should exist already in your +project, as it is generated by the archetype, so make sure that it is there and it contains the +code below: + +.src/main/java/org/jboss/examples/ticketmonster/rest/JaxRsActivator.java +[source,java] +------------------------------------------------------------------------------------------ +@ApplicationPath("/rest") +public class JaxRsActivator extends Application { + /* class body intentionally left blank */ +} +------------------------------------------------------------------------------------------ + +All the JAX-RS services are mapped relative to the `/rest` path, as defined by the `@ApplicationPath` annotation. + +=== A Base Service For Read Operations + + +Most JAX-RS services must provide both a (filtered) list of entities or individual entity (e.g. events, venues and bookings). Instead of duplicating the implementation into each individual service we create a base service class and wire the helper objects in. + +.src/main/java/org/jboss/examples/ticketmonster/rest/BaseEntityService.java +[source,java] +----------------------------------------------------------------------------------------- +/** + *

+ * A number of RESTful services implement GET operations on a particular type of entity. For + * observing the DRY principle, the generic operations are implemented in the BaseEntityService + * class, and the other services can inherit from here. + *

+ * + *

+ * Subclasses will declare a base path using the JAX-RS {@link Path} annotation, for example: + *

+ * + *
+ * 
+ * @Path("/widgets")
+ * public class WidgetService extends BaseEntityService {
+ * ...
+ * }
+ * 
+ * 
+ * + *

+ * will support the following methods: + *

+ * + *
+ * 
+ *   GET /widgets
+ *   GET /widgets/:id
+ *   GET /widgets/count
+ * 
+ * 
+ * + *

+ * Subclasses may specify various criteria for filtering entities when retrieving a list of them, by supporting + * custom query parameters. Pagination is supported by default through the query parameters first + * and maxResults. + *

+ * + *

+ * The class is abstract because it is not intended to be used directly, but subclassed by actual JAX-RS + * endpoints. + *

+ * + */ +public abstract class BaseEntityService { + + @Inject + private EntityManager entityManager; + + private Class entityClass; + + public BaseEntityService() {} + + public BaseEntityService(Class entityClass) { + this.entityClass = entityClass; + } + + public EntityManager getEntityManager() { + return entityManager; + } + +} +----------------------------------------------------------------------------------------- + +Now we add a method to retrieve all entities of a given type: + +.src/main/java/org/jboss/examples/ticketmonster/rest/BaseEntityService.java +[source,java] +----------------------------------------------------------------------------------------- +public abstract class BaseEntityService { + + ... + + /** + *

+ * A method for retrieving all entities of a given type. Supports the query parameters + * first + * and maxResults for pagination. + *

+ * + * @param uriInfo application and request context information (see {@see UriInfo} class + * information for more details) + * @return + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public List getAll(@Context UriInfo uriInfo) { + return getAll(uriInfo.getQueryParameters()); + } + + public List getAll(MultivaluedMap queryParameters) { + final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + final CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(entityClass); + Root root = criteriaQuery.from(entityClass); + Predicate[] predicates = extractPredicates(queryParameters, criteriaBuilder, root); + criteriaQuery.select(criteriaQuery.getSelection()).where(predicates); + criteriaQuery.orderBy(criteriaBuilder.asc(root.get("id"))); + TypedQuery query = entityManager.createQuery(criteriaQuery); + if (queryParameters.containsKey("first")) { + Integer firstRecord = Integer.parseInt(queryParameters.getFirst("first"))-1; + query.setFirstResult(firstRecord); + } + if (queryParameters.containsKey("maxResults")) { + Integer maxResults = Integer.parseInt(queryParameters.getFirst("maxResults")); + query.setMaxResults(maxResults); + } + return query.getResultList(); + } + + /** + *

+ * Subclasses may choose to expand the set of supported query parameters (for adding more filtering + * criteria) by overriding this method. + *

+ * @param queryParameters - the HTTP query parameters received by the endpoint + * @param criteriaBuilder - @{link CriteriaBuilder} used by the invoker + * @param root @{link Root} used by the invoker + * @return a list of {@link Predicate}s that will added as query parameters + */ + protected Predicate[] extractPredicates(MultivaluedMap queryParameters, + CriteriaBuilder criteriaBuilder, Root root) { + return new Predicate[]{}; + } + +} +----------------------------------------------------------------------------------------- + +The newly added method `getAll` is annotated with `@GET` which instructs JAX-RS to call it when a `GET` HTTP requests on the JAX-RS' endpoint base URL '/rest/' is performed. But remember, this is not a true JAX-RS endpoint. It is an abstract class and it is not mapped to a path. The classes that extend it are JAX-RS endpoints, and will have to be mapped to a path, and are able to process requests. + +The `@Produces` annotation defines that the response sent back by the server is in JSON format. The JAX-RS implementation will automatically convert the result returned by the method (a list of entities) into JSON format. + +As well as configuring the marshaling strategy, the annotation affects content negotiation and method resolution. If the client requests JSON content specifically, this method will be invoked. + +[NOTE] +===================================================================================== +Even though it is not shown in this example, you may have multiple methods that +handle a specific URL and HTTP method, whilst consuming and producing different types +of content (JSON, HTML, XML or others). +===================================================================================== + +Subclasses can also override the `extractPredicates` method and add own support for additional query parameters to `GET /rest/` which can act as filter criteria. + +The `getAll` method supports retrieving a range of entities, which is especially useful when we need to handle very large sets of data, and use pagination. In those cases, we need to support counting entities as well, so we add a method that retrieves the entity count: + +.src/main/java/org/jboss/examples/ticketmonster/rest/BaseEntityService.java +[source,java] +----------------------------------------------------------------------------------------- +public abstract class BaseEntityService { + + ... + + /** + *

+ * A method for counting all entities of a given type + *

+ * + * @param uriInfo application and request context information (see {@see UriInfo} class information for more details) + * @return + */ + @GET + @Path("/count") + @Produces(MediaType.APPLICATION_JSON) + public Map getCount(@Context UriInfo uriInfo) { + CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Long.class); + Root root = criteriaQuery.from(entityClass); + criteriaQuery.select(criteriaBuilder.count(root)); + Predicate[] predicates = extractPredicates(uriInfo.getQueryParameters(), criteriaBuilder, root); + criteriaQuery.where(predicates); + Map result = new HashMap(); + result.put("count", entityManager.createQuery(criteriaQuery).getSingleResult()); + return result; + } + +} +----------------------------------------------------------------------------------------- + +We use the `@Path` annotation to map the new method to a sub-path of '/rest/. Now all the JAX-RS endpoints that subclass `BaseEntityService` will be able to get entity counts from '/rest//count'. Just like `getAll`, this method also delegates to `extractPredicates`, so any customizations done there by subclasses + +Next, we add a method for retrieving individual entities. + +.src/main/java/org/jboss/examples/ticketmonster/rest/BaseEntityService.java +[source,java] +----------------------------------------------------------------------------------------- + ... +public abstract class BaseEntityService { + + ... + + /** + *

+ * A method for retrieving individual entity instances. + *

+ * @param id entity id + * @return + */ + @GET + @Path("/{id:[0-9][0-9]*}") + @Produces(MediaType.APPLICATION_JSON) + public T getSingleInstance(@PathParam("id") Long id) { + final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + final CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(entityClass); + Root root = criteriaQuery.from(entityClass); + Predicate condition = criteriaBuilder.equal(root.get("id"), id); + criteriaQuery.select(criteriaBuilder.createQuery(entityClass).getSelection()).where(condition); + return entityManager.createQuery(criteriaQuery).getSingleResult(); + } +} +----------------------------------------------------------------------------------------- + +This method is similar to `getAll` and `getCount`, and we use the `@Path` annotation to map it to a sub-path of '/rest/'. The annotation attribute identifies the expected format of the URL (here, the last segment has to be a number) and binds a portion of the URL to a variable (here named `id`). The `@PathParam` annotation allows the value of the variable to be passed as a method argument. Data conversion is performed automatically. + +Now, all the JAX-RS endpoints that subclass `BaseEntityService` will get two operations for free: + +`GET /rest/`:: retrieves all entities of a given type +`GET /rest//`:: retrieves an entity with a given id + +=== Retrieving Venues + + +Adding support for retrieving venues is now extremely simple. We refactor the class we created during the introduction, and make it extend `BaseEntityService`, passing the entity type to the superclass constructor. We remove the old retrieval code, which is not needed anymore. + +.src/main/java/org/jboss/examples/ticketmonster/rest/VenueService.java +[source,java] +------------------------------------------------------------------------------------------ +/** + *

+ * A JAX-RS endpoint for handling {@link Venue}s. Inherits the actual + * methods from {@link BaseEntityService}. + *

+ */ +@Path("/venues") +/** + *

+ * This is a stateless service, so a single shared instance can be used in this case. + *

+ */ +@Stateless +public class VenueService extends BaseEntityService { + + public VenueService() { + super(Venue.class); + } + +} +------------------------------------------------------------------------------------------ + +We add the `@Path` annotation to the class, to indicate that this is a JAX-RS resource which can serve URLs starting with `/rest/venues`. + +We define this service (along with all the other JAX-RS services) as an EJB (see how simple is that in Java EE 6!) to benefit from automatic transaction enrollment. Since the service is fundamentally stateless, we take advantage of the new EJB 3.1 singleton feature. + +Before we proceed, . Retrieving shows from URLs like `/rest/venues` or `/rest/venues/1` almost always results in invalid JSON responses. +The root cause is the presence of a bi-directional relationship in the `Venue` entity. A `Venue` contains a 1:M relationship with `Section` s that also links back to a `Venue`. JSON serialiers like Jackson (the one used in JBoss Enterprise Application Platform) need to be instructed on how to handle such cycles in object graphs, failing which the serializer will traverse between the entities in the cycle, resulting in an infinite loop (and often an `OutOfMemoryError` or a `StackOverflowError`). We'll address this, by instructing Jackson to not serialize the `venue` field in a `Section`, through the `@JsonIgnoreProperties` annotation on the `Section` entity: + +.src/main/java/org/jboss/examples/ticketmonster/model/Section.java +[source,java] +------------------------------------------------------------------------------------------ +... +@JsonIgnoreProperties("venue") +public class Section implements Serializable { + +... + +} +------------------------------------------------------------------------------------------ + +Now, we can retrieve venues from URLs like `/rest/venues` or `rest/venues/1`. + +=== Retrieving Events + + +Just like `VenueService`, the `EventService` implementation we use for TicketMonster is a direct subclass of `BaseEntityService`. Refactor the existing class, remove the old retrieval code and make it extend `BaseEntityService`. + +One additional functionality we will implement is querying events by category. We can use URLs like `/rest/events?category=1` to retrieve all concerts, for example (`1` is the category id of concerts). This is done by overriding the `extractPredicates` method to handle any query parameters (in this case, the `category` parameter). + +.src/main/java/org/jboss/examples/ticketmonster/rest/EventService.java +[source,java] +------------------------------------------------------------------------------------------ +/** + *

+ * A JAX-RS endpoint for handling {@link Event}s. Inherits the actual + * methods from {@link BaseEntityService}, but implements additional search + * criteria. + *

+ */ +@Path("/events") +/** + *

+ * This is a stateless service, we declare it as an EJB for transaction demarcation + *

+ */ +@Stateless +public class EventService extends BaseEntityService { + + public EventService() { + super(Event.class); + } + + /** + *

+ * We override the method from parent in order to add support for additional search + * criteria for events. + *

+ * @param queryParameters - the HTTP query parameters received by the endpoint + * @param criteriaBuilder - @{link CriteriaBuilder} used by the invoker + * @param root @{link Root} used by the invoker + * @return + */ + @Override + protected Predicate[] extractPredicates( + MultivaluedMap queryParameters, + CriteriaBuilder criteriaBuilder, + Root root) { + List predicates = new ArrayList() ; + + if (queryParameters.containsKey("category")) { + String category = queryParameters.getFirst("category"); + predicates.add(criteriaBuilder.equal(root.get("category").get("id"), category)); + } + + return predicates.toArray(new Predicate[]{}); + } +} +------------------------------------------------------------------------------------------ + +=== Retrieving Shows + + +The `ShowService` follows the same pattern and we leave the implementation as an exercise to the reader (knowing that its contents can always be copied over to the appropriate folder). + +Just like the `Venue` entity, a `Show` also contains bi-directional relationships that need to be handled as a special case for the JSON serializer. A `Show` contains a 1:M relationship with `Performance` s that also links back to a `Show`; a `Show` also contains a 1:M relationship with `TicketPrice` s that also links back to a `Show`. We'll address this, by instructing Jackson to not serialize the `show` field in a `Performance`, through the `@JsonIgnoreProperties` annotation on the `Performance` entity: + +.src/main/java/org/jboss/examples/ticketmonster/model/Performance.java +[source,java] +------------------------------------------------------------------------------------------ +... +@JsonIgnoreProperties("show") +public class Performance implements Serializable { + +... + +} +------------------------------------------------------------------------------------------ + +Likewise, we'll also instruct Jackson to not serialize the `Show` in a `TicketPrice`: + +.src/main/java/org/jboss/examples/ticketmonster/model/TicketPrice.java +[source,java] +------------------------------------------------------------------------------------------ +... +@JsonIgnoreProperties("show") +public class TicketPrice implements Serializable { + +... + +} +------------------------------------------------------------------------------------------ + + +=== Creating and deleting bookings + +Of course, we also want to change data with our services - we want to create and delete bookings as well! + +To create a booking, we add a new method, which handles `POST` requests to `/rest/bookings`. This is not a simple CRUD method, as the client does not send a booking, but a booking request. It is the responsibility of the service to process the request, reserve the seats and return the full booking details to the invoker. + +.src/main/java/org/jboss/examples/ticketmonster/rest/BookingService.java +[source,java] +------------------------------------------------------------------------------------------ +/** + *

+ * A JAX-RS endpoint for handling {@link Booking}s. Inherits the GET + * methods from {@link BaseEntityService}, and implements additional REST methods. + *

+ */ +@Path("/bookings") +/** + *

+ * This is a stateless service, we declare it as an EJB for transaction demarcation + *

+ */ +@Stateless +public class BookingService extends BaseEntityService { + + @Inject + SeatAllocationService seatAllocationService; + + @Inject @Created + private Event newBookingEvent; + + public BookingService() { + super(Booking.class); + } + + /** + *

+ * Create a booking. Data is contained in the bookingRequest object + *

+ * @param bookingRequest + * @return + */ + @SuppressWarnings("unchecked") + @POST + /** + *

Data is received in JSON format. For easy handling, it will be unmarshalled in the support + * {@link BookingRequest} class. + */ + @Consumes(MediaType.APPLICATION_JSON) + public Response createBooking(BookingRequest bookingRequest) { + try { + // identify the ticket price categories in this request + Set priceCategoryIds = bookingRequest.getUniquePriceCategoryIds(); + + // load the entities that make up this booking's relationships + Performance performance = getEntityManager().find(Performance.class, bookingRequest.getPerformance()); + + // As we can have a mix of ticket types in a booking, we need to load all of them that are relevant, + // id + Map ticketPricesById = loadTicketPrices(priceCategoryIds); + + // Now, start to create the booking from the posted data + // Set the simple stuff first! + Booking booking = new Booking(); + booking.setContactEmail(bookingRequest.getEmail()); + booking.setPerformance(performance); + booking.setCancellationCode("abc"); + + // Now, we iterate over each ticket that was requested, and organize them by section and category + // we want to allocate ticket requests that belong to the same section contiguously + Map> ticketRequestsPerSection + = new TreeMap>(SectionComparator.instance()); + for (TicketRequest ticketRequest : bookingRequest.getTicketRequests()) { + final TicketPrice ticketPrice = ticketPricesById.get(ticketRequest.getTicketPrice()); + if (!ticketRequestsPerSection.containsKey(ticketPrice.getSection())) { + ticketRequestsPerSection + .put(ticketPrice.getSection(), new HashMap()); + } + ticketRequestsPerSection.get(ticketPrice.getSection()).put( + ticketPricesById.get(ticketRequest.getTicketPrice()).getTicketCategory(), ticketRequest); + } + + // Now, we can allocate the tickets + // Iterate over the sections, finding the candidate seats for allocation + // The process will acquire a write lock for a given section and performance + // Use deterministic ordering of sections to prevent deadlocks + Map seatsPerSection = + new TreeMap(SectionComparator.instance()); + List

failedSections = new ArrayList
(); + for (Section section : ticketRequestsPerSection.keySet()) { + int totalTicketsRequestedPerSection = 0; + // Compute the total number of tickets required (a ticket category doesn't impact the actual seat!) + final Map ticketRequestsByCategories = ticketRequestsPerSection.get(section); + // calculate the total quantity of tickets to be allocated in this section + for (TicketRequest ticketRequest : ticketRequestsByCategories.values()) { + totalTicketsRequestedPerSection += ticketRequest.getQuantity(); + } + // try to allocate seats + + AllocatedSeats allocatedSeats = + seatAllocationService.allocateSeats(section, performance, totalTicketsRequestedPerSection, true); + if (allocatedSeats.getSeats().size() == totalTicketsRequestedPerSection) { + seatsPerSection.put(section, allocatedSeats); + } else { + failedSections.add(section); + } + } + if (failedSections.isEmpty()) { + for (Section section : seatsPerSection.keySet()) { + // allocation was successful, begin generating tickets + // associate each allocated seat with a ticket, assigning a price category to it + final Map ticketRequestsByCategories = ticketRequestsPerSection.get(section); + AllocatedSeats allocatedSeats = seatsPerSection.get(section); + allocatedSeats.markOccupied(); + int seatCounter = 0; + // Now, add a ticket for each requested ticket to the booking + for (TicketCategory ticketCategory : ticketRequestsByCategories.keySet()) { + final TicketRequest ticketRequest = ticketRequestsByCategories.get(ticketCategory); + final TicketPrice ticketPrice = ticketPricesById.get(ticketRequest.getTicketPrice()); + for (int i = 0; i < ticketRequest.getQuantity(); i++) { + Ticket ticket = + new Ticket(allocatedSeats.getSeats().get(seatCounter + i), ticketCategory, ticketPrice.getPrice()); + // getEntityManager().persist(ticket); + booking.getTickets().add(ticket); + } + seatCounter += ticketRequest.getQuantity(); + } + } + // Persist the booking, including cascaded relationships + booking.setPerformance(performance); + booking.setCancellationCode("abc"); + getEntityManager().persist(booking); + newBookingEvent.fire(booking); + return Response.ok().entity(booking).type(MediaType.APPLICATION_JSON_TYPE).build(); + } else { + Map responseEntity = new HashMap(); + responseEntity.put("errors", Collections.singletonList("Cannot allocate the requested number of seats!")); + return Response.status(Response.Status.BAD_REQUEST).entity(responseEntity).build(); + } + } catch (ConstraintViolationException e) { + // If validation of the data failed using Bean Validation, then send an error + Map errors = new HashMap(); + List errorMessages = new ArrayList(); + for (ConstraintViolation constraintViolation : e.getConstraintViolations()) { + errorMessages.add(constraintViolation.getMessage()); + } + errors.put("errors", errorMessages); + // A WebApplicationException can wrap a response + // Throwing the exception causes an automatic rollback + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST).entity(errors).build()); + } catch (Exception e) { + // Finally, handle unexpected exceptions + Map errors = new HashMap(); + errors.put("errors", Collections.singletonList(e.getMessage())); + // A WebApplicationException can wrap a response + // Throwing the exception causes an automatic rollback + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST).entity(errors).build()); + } + } + + /** + * Utility method for loading ticket prices + * @param priceCategoryIds + * @return + */ + private Map loadTicketPrices(Set priceCategoryIds) { + List ticketPrices = (List) getEntityManager() + .createQuery("select p from TicketPrice p where p.id in :ids") + .setParameter("ids", priceCategoryIds).getResultList(); + // Now, map them by id + Map ticketPricesById = new HashMap(); + for (TicketPrice ticketPrice : ticketPrices) { + ticketPricesById.put(ticketPrice.getId(), ticketPrice); + } + return ticketPricesById; + } +} +------------------------------------------------------------------------------------------ + +You should also copy over the `BookingRequest`, `TicketRequest` and `SectionComparator` classes referenced in these methods, from the project sources. + +We won't get into the details of the inner workings of the method - it implements a fairly complex algorithm - but we'd like to draw attention to a few particular items. + +We use the `@POST` annotation to indicate that this method is executed on inbound HTTP POST requests. When implementing a set of RESTful services, it is important that the semantic of HTTP methods are observed in the mappings. Creating new resources (e.g. bookings) is typically associated with HTTP POST invocations. The `@Consumes` annotation indicates that the type of the request content is JSON and identifies the correct unmarshalling strategy, as well as content negotiation. + +The `BookingService` delegates to the `SeatAllocationService` to find seats in the requested section, the required `SeatAllocationService` instance is initialized and supplied by the container as needed. The only thing that our service does is to specify the dependency in form +of an injection point - the field annotated with `@Inject`. + +We would like other parts of the application to be aware of the fact that a new booking has been created, therefore we use the CDI to fire an event. We do so by injecting an `Event` instance into the service (indicating that its payload will be a booking). In order to individually identify this event as referring to event creation, we use a CDI qualifier, which we need to add: + +.src/main/java/org/jboss/examples/ticketmonster/util/qualifier/Created.java +[source, java] +------------------------------------------------------------------------------------------ +/** + * {@link Qualifier} to mark a Booking as new (created). + */ +@Qualifier +@Target({ElementType.FIELD,ElementType.PARAMETER,ElementType.METHOD,ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Created { + +} +------------------------------------------------------------------------------------------ + +[TIP] +.What are qualifiers? +===================================================================================== +CDI uses a type-based resolution mechanism for injection and observers. In order to +distinguish between implementations of an interface, you can use qualifiers, a type +of annotations, to disambiguate. Injection points and event observers can use +qualifiers to narrow down the set of candidates +===================================================================================== + +We also need allow the removal of bookings, so we add a method: + +.src/main/java/org/jboss/examples/ticketmonster/rest/BookingService.java +[source,java] +------------------------------------------------------------------------------------------ +@Singleton +public class BookingService extends BaseEntityService { + ... + + @Inject @Cancelled + private Event cancelledBookingEvent; + ... + /** + *

+ * Delete a booking by id + *

+ * @param id + * @return + */ + @DELETE + @Path("/{id:[0-9][0-9]*}") + public Response deleteBooking(@PathParam("id") Long id) { + Booking booking = getEntityManager().find(Booking.class, id); + if (booking == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + getEntityManager().remove(booking); + cancelledBookingEvent.fire(booking); + return Response.noContent().build(); + } +} +------------------------------------------------------------------------------------------ + +We use the `@DELETE` annotation to indicate that it will be executed as the result of an HTTP DELETE request (again, the use of the DELETE HTTP verb is a matter of convention). + +We need to notify the other components of the cancellation of the booking, so we fire an event, with a different qualifier. + +.src/main/java/org/jboss/examples/ticketmonster/util/qualifier/Cancelled.java +[source, java] +------------------------------------------------------------------------------------------ +/** + * {@link Qualifier} to mark a Booking as cancelled. + */ +@Qualifier +@Target({ElementType.FIELD,ElementType.PARAMETER,ElementType.METHOD,ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Cancelled { + +} +------------------------------------------------------------------------------------------ + +The other services, including the `MediaService` that handles media items follow roughly the same patterns as above, so we leave them as an exercise to the reader. + +== Testing the services + + +We've now finished implementing the services and there is a significant amount of functionality in the application. Before taking any step forward, you need to make sure the services work correctly: we need to test them. + +Testing enterprise services be a complex task as the implementation is based on services provided by a container: dependency injection, access to infrastructure services such as persistence, transactions etc. Unit testing frameworks, whilst offering a valuable infrastructure for running tests, do not provide these capabilities. + +One of the traditional approaches has been the use of mocking frameworks to simulate 'what will happen' in the runtime environment. While certainly providing a solution mocking brings its own set of problems (e.g. the additional effort required to provide a proper simulation or the risk of introducing errors in the test suite by incorrectly implemented mocks. + +[TIP] +.What to test? +===================================================================================== +A common asked question is: how much application functionality should we test? The +truth is, you can never test too much. That being said, resources are always limited +and tradeoffs are part of an engineer's work. Generally speaking, trivial +functionality (setters/getters/toString methods) is a big concern compared to the +actual business code, so you probably want to focus your efforts on the business +code. Testing should include individual parts (unit testing), as well as +aggregates (integration testing). +===================================================================================== + +Fortunately, Arquillian provides the means to testing your application code within the container, with access to all the services and container features. In this section we will show you how to create a few Arquillian tests for your business services. + +[TIP] +.New to Arquillian? +===================================================================================== +The Arquillian project site contains several tutorials to help you get started. +If you're new to Arquillian and Shrinkwrap, we recommend going through the +http://arquillian.org/guides/[beginner-level Arquillian guides], at the very least. +===================================================================================== + + +=== Adding ShrinkWrap Resolvers + +We'll need to use an updated version of the ShrinkWrap Resolvers project, that is not provided by the existing `org.jboss.bom.eap:jboss-javaee-6.0-with-tools` BOM. Fortunately, the JBoss WFK project provides this for us. It provides us with the `shrinkwrap-resolver-depchain` module, which allows us to use ShrinkWrap resolvers in our project through a single dependency. We can bring in the required version of ShrinkWrap Resolvers, by merely using the `org.jboss.bom.wfk:jboss-javaee-6.0-with-tools` BOM instead of the pre-existing tools BOM from EAP: + +.pom.xml +[source,xml] +--------------------------------------------------------------------------------- + + ... + + ... + 2.7.0-redhat-1 + ... + + + + + ... + + + org.jboss.bom.wfk + jboss-javaee-6.0-with-resteasy + ${version.jboss.bom.wfk} + pom + import + + + + ... + + + ... + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-depchain + pom + test + + + ... + + +--------------------------------------------------------------------------------- + +Remember to remove the original Tools BOM with the `org.jboss.bom.eap` groupId. + +=== A Basic Deployment Class + + +In order to create Arquillian tests, we need to define the deployment. The code under test, as well as its dependencies is packaged and deployed in the container. + +Much of the deployment contents is common for all tests, so we create a helper class with a method that creates the base deployment with all the general content. + +.src/test/java/org/jboss/examples/ticketmonster/test/TicketMonsterDeployment.java +[source,java] +------------------------------------------------------------------------------------------ +public class TicketMonsterDeployment { + + public static WebArchive deployment() { + return ShrinkWrap + .create(WebArchive.class, "test.war") + .addPackage(Resources.class.getPackage()) + .addAsResource("META-INF/test-persistence.xml", "META-INF/persistence.xml") + .addAsResource("import.sql") + .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml") + // Deploy our test datasource + .addAsWebInfResource("test-ds.xml"); + } +} +------------------------------------------------------------------------------------------ + +Remember to copy over the `test-persistence.xml` file into the `src/test/resources` directory of your project. + +Arquillian uses Shrinkwrap to define the contents of the deployment. At runtime, when the test executes, Arquillian employs Shrinkwrap to create a WAR file that will be deployed to a running instance of JBoss Enterprise Application Platform. The WAR file would be composed of: + +* all classes from the `org.jboss.examples.ticketmonster.util` package, +* the test `persistence.xml` file that defines a JPA persistence unit against a test datasource, +* the `import.sql` file, +* an empty `beans.xml` file to activate CDI +* and, a test data source definition. + +We use a separate data source for our integration tests, and we recommend the same for real applications. This would allow you to run your tests against a pristine test environment, without having to clean your development, or worse, your production environment! + +=== Writing RESTful service tests + + +For testing our JAX-RS RESTful services, we need to add the corresponding application classes to the deployment. Since we need to do that for each test we create, we abide by the DRY principles and create a utility class. + +.src/test/java/org/jboss/examples/ticketmonster/test/rest/RESTDeployment.java +[source,java] +------------------------------------------------------------------------------------------ +public class RESTDeployment { + + public static WebArchive deployment() { + return TicketMonsterDeployment.deployment() + .addPackage(Booking.class.getPackage()) + .addPackage(BaseEntityService.class.getPackage()) + .addPackage(MultivaluedHashMap.class.getPackage()) + .addPackage(SeatAllocationService.class.getPackage()); + } + +} +------------------------------------------------------------------------------------------ + +Now, we create the first test to validate the proper retrieval of individual events. + +.src/test/java/org/jboss/examples/ticketmonster/test/rest/VenueServiceTest.java +[source,java] +------------------------------------------------------------------------------------------ +@RunWith(Arquillian.class) +public class VenueServiceTest { + + @Deployment + public static WebArchive deployment() { + return RESTDeployment.deployment(); + } + + @Inject + private VenueService venueService; + + @Test + public void testGetVenueById() { + + // Test loading a single venue + Venue venue = venueService.getSingleInstance(1l); + assertNotNull(venue); + assertEquals("Roy Thomson Hall", venue.getName()); + } + +} +------------------------------------------------------------------------------------------ + +In the class above we specify the deployment, and we define the test method. The test supports CDI injection - one of the strengths of Arquillian is the ability to inject the object being tested. + +Now, we test a more complicated use cases, query parameters for pagination. + +.src/test/java/org/jboss/examples/ticketmonster/test/rest/VenueServiceTest.java +[source,java] +------------------------------------------------------------------------------------------ +... +@RunWith(Arquillian.class) +public class VenueServiceTest { + + ... + + @Test + public void testPagination() { + + // Test pagination logic + MultivaluedMap queryParameters = new MultivaluedHashMap(); + + queryParameters.add("first", "2"); + queryParameters.add("maxResults", "1"); + + List venues = venueService.getAll(queryParameters); + assertNotNull(venues); + assertEquals(1, venues.size()); + assertEquals("Sydney Opera House", venues.get(0).getName()); + } + +} +------------------------------------------------------------------------------------------ + +We add another test method (`testPagination`), which tests the retrieval of all venues, passing the +search criteria as parameters. We use a Map to simulate the passing of query parameters. + +Now, we test more advanced use cases such as the creation of a new booking. We do so by adding a new test for bookings + +.src/test/java/org/jboss/examples/ticketmonster/test/rest/BookingServiceTest.java +[source,java] +------------------------------------------------------------------------------------------ +@RunWith(Arquillian.class) +public class BookingServiceTest { + + @Deployment + public static WebArchive deployment() { + return RESTDeployment.deployment(); + } + + @Inject + private BookingService bookingService; + + @Inject + private ShowService showService; + + @Test + @InSequence(1) + public void testCreateBookings() { + BookingRequest br = createBookingRequest(1l, 0, new int[]{4, 1}, new int[]{1,1}, new int[]{3,1}); + bookingService.createBooking(br); + + BookingRequest br2 = createBookingRequest(2l, 1, new int[]{6,1}, new int[]{8,2}, new int[]{10,2}); + bookingService.createBooking(br2); + + BookingRequest br3 = createBookingRequest(3l, 0, new int[]{4,1}, new int[]{2,1}); + bookingService.createBooking(br3); + } + + @Test + @InSequence(10) + public void testGetBookings() { + checkBooking1(); + checkBooking2(); + checkBooking3(); + } + + private void checkBooking1() { + Booking booking = bookingService.getSingleInstance(1l); + assertNotNull(booking); + assertEquals("Roy Thomson Hall", booking.getPerformance().getShow().getVenue().getName()); + assertEquals("Rock concert of the decade", booking.getPerformance().getShow().getEvent().getName()); + assertEquals("bob@acme.com", booking.getContactEmail()); + + // Test the ticket requests created + + assertEquals(3 + 2 + 1, booking.getTickets().size()); + + List requiredTickets = new ArrayList(); + requiredTickets.add("A @ 219.5 (Adult)"); + requiredTickets.add("A @ 219.5 (Adult)"); + requiredTickets.add("D @ 149.5 (Adult)"); + requiredTickets.add("C @ 179.5 (Adult)"); + requiredTickets.add("C @ 179.5 (Adult)"); + requiredTickets.add("C @ 179.5 (Adult)"); + + checkTickets(requiredTickets, booking); + } + + private void checkBooking2() { + Booking booking = bookingService.getSingleInstance(2l); + assertNotNull(booking); + assertEquals("Sydney Opera House", booking.getPerformance().getShow().getVenue().getName()); + assertEquals("Rock concert of the decade", booking.getPerformance().getShow().getEvent().getName()); + assertEquals("bob@acme.com", booking.getContactEmail()); + + assertEquals(3 + 2 + 1, booking.getTickets().size()); + + List requiredTickets = new ArrayList(); + requiredTickets.add("S2 @ 197.75 (Adult)"); + requiredTickets.add("S6 @ 145.0 (Child 0-14yrs)"); + requiredTickets.add("S6 @ 145.0 (Child 0-14yrs)"); + requiredTickets.add("S4 @ 145.0 (Child 0-14yrs)"); + requiredTickets.add("S6 @ 145.0 (Child 0-14yrs)"); + requiredTickets.add("S4 @ 145.0 (Child 0-14yrs)"); + + checkTickets(requiredTickets, booking); + } + + private void checkBooking3() { + Booking booking = bookingService.getSingleInstance(3l); + assertNotNull(booking); + assertEquals("Roy Thomson Hall", booking.getPerformance().getShow().getVenue().getName()); + assertEquals("Shane's Sock Puppets", booking.getPerformance().getShow().getEvent().getName()); + assertEquals("bob@acme.com", booking.getContactEmail()); + + assertEquals(2 + 1, booking.getTickets().size()); + + List requiredTickets = new ArrayList(); + requiredTickets.add("B @ 199.5 (Adult)"); + requiredTickets.add("D @ 149.5 (Adult)"); + requiredTickets.add("B @ 199.5 (Adult)"); + + checkTickets(requiredTickets, booking); + } + + @Test + @InSequence(10) + public void testPagination() { + + // Test pagination logic + MultivaluedMap queryParameters = new MultivaluedHashMap(); + + queryParameters.add("first", "2"); + queryParameters.add("maxResults", "1"); + + List bookings = bookingService.getAll(queryParameters); + assertNotNull(bookings); + assertEquals(1, bookings.size()); + assertEquals("Sydney Opera House", bookings.get(0).getPerformance().getShow().getVenue().getName()); + assertEquals("Rock concert of the decade", bookings.get(0).getPerformance().getShow().getEvent().getName()); + } + + @Test + @InSequence(20) + public void testDelete() { + bookingService.deleteBooking(2l); + checkBooking1(); + checkBooking3(); + try { + bookingService.getSingleInstance(2l); + } catch (Exception e) { + if (e.getCause() instanceof NoResultException) { + return; + } + } + fail("Expected NoResultException did not occur."); + } + + private BookingRequest createBookingRequest(Long showId, int performanceNo, int[]... sectionAndCategories) { + Show show = showService.getSingleInstance(showId); + + Performance performance = new ArrayList(show.getPerformances()).get(performanceNo); + + BookingRequest bookingRequest = new BookingRequest(performance, "bob@acme.com"); + + List possibleTicketPrices = new ArrayList(show.getTicketPrices()); + int i = 1; + for (int[] sectionAndCategory : sectionAndCategories) { + for (TicketPrice ticketPrice : possibleTicketPrices) { + int sectionId = sectionAndCategory[0]; + int categoryId = sectionAndCategory[1]; + if(ticketPrice.getSection().getId() == sectionId && ticketPrice.getTicketCategory().getId() == categoryId) { + bookingRequest.addTicketRequest(new TicketRequest(ticketPrice, i)); + i++; + break; + } + } + } + + return bookingRequest; + } + + private void checkTickets(List requiredTickets, Booking booking) { + List bookedTickets = new ArrayList(); + for (Ticket t : booking.getTickets()) { + bookedTickets.add(new StringBuilder().append(t.getSeat().getSection()).append(" @ ").append(t.getPrice()).append(" (").append(t.getTicketCategory()).append(")").toString()); + } + System.out.println(bookedTickets); + for (String requiredTicket : requiredTickets) { + Assert.assertTrue("Required ticket not present: " + requiredTicket, bookedTickets.contains(requiredTicket)); + } + } + +} +------------------------------------------------------------------------------------------ + +First we test booking creation in a test method of its own (`testCreateBookings`). Then, we test that the previously created bookings +are retrieved correctly (`testGetBookings` and `testPagination`). Finally, we test that deletion takes place correctly (`testDelete`). + +The other tests in the application follow roughly the same pattern and are left as an exercise to the reader. You could in fact copy over the `EventServiceTest` and `ShowServiceTest` classes from the project sources. + +=== Running the tests + + +If you have followed the instructions in the introduction and used the Maven archetype to generate the project structure, you should have two profiles already defined in your application. + +./pom.xml +[source,xml] +------------------------------------------------------------------------------------------ + + + 4.0.0 + + ... + + + + + arq-jbossas-managed + + + org.jboss.as + jboss-as-arquillian-container-managed + test + + + + + + + + arq-jbossas-remote + + + org.jboss.as + jboss-as-arquillian-container-remote + test + + + + + + +------------------------------------------------------------------------------------------ + +If you haven't used the archetype, or the profiles don't exist, create them. + +Each profile defines a different Arquillian container. In both cases the tests execute in an application server instance. In one case (`arq-jbossas-managed`) the server instance is started and stopped by the test suite, while in the other (`arq-jbossas-remote`), the test suite expects an already started server instance. + +Once these profiles are defined, we can execute the tests in two ways: + +* from the command-line build +* from an IDE + +==== Executing tests from the command line + + +You can now execute the test suite from the command line by running the Maven build with the appropriate target and profile, as in one of the following examples. + +After ensuring that the `JBOSS_HOME` environment variable is set to a valid JBoss EAP 6.2 installation directory), you can run the following command: + +---- +mvn clean test -Parq-jbossas-managed +---- + +Or, after starting a JBoss EAP 6.2 instance, you can run the following command + +---- +mvn clean test -Parq-jbossas-remote +---- + +These tests execute as part of the Maven build and can be easily included in an automated build and test harness. + +==== Running Arquillian tests from within Eclipse + + +Running the entire test suite as part of the build is an important part of the development process - you may want to make sure that everything is working fine before releasing a new milestone, or just before committing new code. However, running the entire test suite all the time +can be a productivity drain, especially when you're trying to focus on a particular problem. Also, when debugging, you don't want to leave the comfort of your IDE for running the tests. + +Running Arquillian tests from JBoss Developer Studio or JBoss tools is very simple as Arquillian builds on JUnit (or TestNG). + +First enable one of the two profiles in the project. In Eclipse, select the project, right-click on it to open the context menu, drill down into the _Maven_ sub-menu: + +[[eclipse-select-maven-profiles]] +.Select the Maven profiles for the project +image::gfx/eclipse-project-maven-profiles.png + +Activate the profile as shown in the picture below. + +[[eclipse-update-profiles]] +.Update Maven profiles in Eclipse +image::gfx/eclipse-maven-profile-update.png + +The project configuration will be updated automatically. + +Now, you can click right on one of your test classes, and select *Run As -> JUnit Test*. + +The test suite will run, deploying the test classes to the application server, executing the tests and finally producing the much coveted green bar. + +[[eclipse-green-bar]] +.Running the tests +image::gfx/eclipse-green-bar.png[scaledwidth="50%"] diff --git a/tutorial/CONTRIBUTING.md b/tutorial/CONTRIBUTING.md new file mode 100644 index 000000000..fdd744bfd --- /dev/null +++ b/tutorial/CONTRIBUTING.md @@ -0,0 +1,10 @@ +Contributing to the TicketMonster tutorial +========================================== + +Each section of the tutorial is contained in a `.asciidoc` file in this git repository. To build the tutorial you'll need: + +* [AsciiDoc](http://www.methods.co.nz/asciidoc/index.html) installed. It's available via most major packaging systems (e.g. Debian, Fedora Extra, MacPorts), and has a Windows installer. +* You'll also need pygments for syntax highlighting. It's available as a python egg. Easiest to install via `easy_install` e.g. `sudo easy_install pygments`. +* dblatex, once again available in most major packaging systems, or as a python egg e.g. `sudo easy_install dblatex` + +Once you have installed AsciiDoc, you can build individual sections by invoking `asciidoc -b `. If you want to generate the whole tutorial as html, you can call `./generate-guides.sh`. diff --git a/tutorial/DashboardHTML5.asciidoc b/tutorial/DashboardHTML5.asciidoc new file mode 100644 index 000000000..3f5e9dac1 --- /dev/null +++ b/tutorial/DashboardHTML5.asciidoc @@ -0,0 +1,1114 @@ += Building The Statistics Dashboard Using HTML5 and JavaScript +:Author: Vineet Reynolds +:thumbnail: http://static.jboss.org/ffe/1/www/origin/ticket-monster-splash-2.png + +== What Will You Learn Here? + + +You've just built the administration view, and would like to collect real-time information about ticket sales and attendance. Now it would be good to implement a dashboard that can collect data and receive real-time updates. After reading this tutorial, you will understand our dashboard design and the choices that we made in its implementation. Topics covered include: + +* Adding a RESTful API to your application for obtaining metrics +* Adding a non-RESTful API to your application for controlling a bot +* Creating Backbone.js models and views to interact with a non-RESTful service + +In this tutorial, we will create a booking monitor using Backbone.js, and add it to the TicketMonster application. It will show live updates on the booking status of all performances and shows. These live updates are powered by a short polling solution that pings the server on regular intervals to obtain updated metrics. + +== Implementing the Metrics API + +The Metrics service publishes metrics for every show. These metrics include the capacity of the venue for the show, as well as the occupied count. Since these metrics are computed from persisted data, we'll not create any classes to represent them in the data model. We shall however create new classes to serve as their representations for the REST APIs: + +.src/main/java/org/jboss/examples/ticketmonster/rest/ShowMetric.java +[source,java] +--------------------------------------------------------------------------------------------------------- +package org.jboss.examples.ticketmonster.rest; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jboss.examples.ticketmonster.model.Performance; +import org.jboss.examples.ticketmonster.model.Show; + +/** + * Metric data for a Show. Contains the identifier for the Show to identify it, + * in addition to the event name, the venue name and capacity, and the metric + * data for the performances of the Show. + */ +class ShowMetric { + + private Long show; + private String event; + private String venue; + private int capacity; + private List performances; + + // Constructor to populate the instance with data + public ShowMetric(Show show, Map occupiedCounts) { + this.show = show.getId(); + this.event = show.getEvent().getName(); + this.venue = show.getVenue().getName(); + this.capacity = show.getVenue().getCapacity(); + this.performances = convertFrom(show.getPerformances(), occupiedCounts); + } + + private List convertFrom(Set performances, + Map occupiedCounts) { + List result = new ArrayList(); + for (Performance performance : performances) { + Long occupiedCount = occupiedCounts.get(performance.getId()); + result.add(new PerformanceMetric(performance, occupiedCount)); + } + return result; + } + + // Getters for Jackson + // NOTE: No setters and default constructors are defined since + // deserialization is not required. + + public Long getShow() { + return show; + } + + public String getEvent() { + return event; + } + + public String getVenue() { + return venue; + } + + public int getCapacity() { + return capacity; + } + + public List getPerformances() { + return performances; + } +} +--------------------------------------------------------------------------------------------------------- + +The `ShowMetric` class is used to represent the structure of a `Show` in the metrics API. It contains the show identifier, the `Event` name for the `Show`, the `Venue` name for the `Show`, the capacity of the `Venue` and a collection of `PerformanceMetric` instances to represent metrics for individual `Performance` s for the `Show`. + +The `PerformanceMetric` is represented as: + +.src/main/java/org/jboss/examples/ticketmonster/rest/PerformanceMetric.java +[source,java] +--------------------------------------------------------------------------------------------------------- +package org.jboss.examples.ticketmonster.rest; + +import java.util.Date; + +import org.jboss.examples.ticketmonster.model.Performance; + +/** + * Metric data for a Performance. Contains the datetime for the performance to + * identify the performance, as well as the occupied count for the performance. + */ +class PerformanceMetric { + + private Date date; + private Long occupiedCount; + + // Constructor to populate the instance with data + public PerformanceMetric(Performance performance, Long occupiedCount) { + this.date = performance.getDate(); + this.occupiedCount = (occupiedCount == null ? 0 : occupiedCount); + } + + // Getters for Jackson + // NOTE: No setters and default constructors are defined since + // deserialization is not required. + + public Date getDate() { + return date; + } + + public Long getOccupiedCount() { + return occupiedCount; + } + +} +--------------------------------------------------------------------------------------------------------- + +This class represents the date-time instance of `Performance` in addition to the count of occupied seats for the venue. + +The next class we need is the `MetricsService` class that responds with representations of `ShowMetric` instances in response to HTTP GET requests: + +.src/main/java/org/jboss/examples/ticketmonster/rest/MetricsService.java +[source,java] +--------------------------------------------------------------------------------------------------------- +package org.jboss.examples.ticketmonster.rest; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ejb.Stateless; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.Query; +import javax.persistence.TypedQuery; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.examples.ticketmonster.model.Show; + +/** + * A read-only REST resource that provides a collection of metrics for shows occuring in the future. Updates to metrics via + * POST/PUT etc. are not allowed, since they are not meant to be computed by consumers. + * + */ +@Path("/metrics") +@Stateless +public class MetricsService { + + @Inject + private EntityManager entityManager; + + /** + * Retrieves a collection of metrics for Shows. Each metric in the collection contains + *
    + *
  • the show id,
  • + *
  • the event name of the show,
  • + *
  • the venue for the show,
  • + *
  • the capacity for the venue
  • + *
  • the performances for the show, + *
      + *
    • the timestamp for each performance,
    • + *
    • the occupied count for each performance
    • + *
    + *
  • + *
+ * + * @return A JSON representation of metrics for shows. + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public List getMetrics() { + return retrieveMetricsFromShows(retrieveShows(), + retrieveOccupiedCounts()); + } + + private List retrieveMetricsFromShows(List shows, + Map occupiedCounts) { + List metrics = new ArrayList(); + for (Show show : shows) { + metrics.add(new ShowMetric(show, occupiedCounts)); + } + return metrics; + } + + private List retrieveShows() { + TypedQuery showQuery = entityManager + .createQuery("select DISTINCT s from Show s JOIN s.performances p WHERE p.date > current_timestamp", Show.class); + return showQuery.getResultList(); + } + + private Map retrieveOccupiedCounts() { + Map occupiedCounts = new HashMap(); + + Query occupiedCountsQuery = entityManager + .createQuery("select b.performance.id, SIZE(b.tickets) from Booking b " + + "WHERE b.performance.date > current_timestamp GROUP BY b.performance.id"); + + List results = occupiedCountsQuery.getResultList(); + for (Object[] result : results) { + occupiedCounts.put((Long) result[0], + ((Integer) result[1]).longValue()); + } + + return occupiedCounts; + } +} +--------------------------------------------------------------------------------------------------------- + +This REST resource responds to a GET request by querying the database to retrieve all the shows and the performances associated with each show. The metric for every performance is also obtained; the performance metric is simply the sum of all tickets booked for the performance. This query result is used to populate the `ShowMetric` and `PerformanceMetric` representation instances that are later serialized as JSON responses by the JAX-RS provider. + + +== Creating the Bot service + +We'd also like to implement a `Bot` service that would mimic a set of real users. Once started, the `Bot` would attempt to book tickets at periodic intervals, until it is ordered to stop. The `Bot` should also be capable of deleting all Bookings so that the system could be returned to a clean state. + +We will implement the `Bot` as an EJB that will utlize the container-provided `TimerService` to periodically perform bookings of a random number of tickets on randomly selected performances: + +.src/main/java/org/jboss/examples/ticketmonster/service/Bot.java +[source,java] +--------------------------------------------------------------------------------------------------------- +package org.jboss.examples.ticketmonster.service; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Resource; +import javax.ejb.Stateless; +import javax.ejb.Timeout; +import javax.ejb.Timer; +import javax.ejb.TimerConfig; +import javax.ejb.TimerService; +import javax.enterprise.event.Event; +import javax.inject.Inject; +import javax.ws.rs.core.Response; + +import org.jboss.examples.ticketmonster.model.Performance; +import org.jboss.examples.ticketmonster.model.Show; +import org.jboss.examples.ticketmonster.model.TicketPrice; +import org.jboss.examples.ticketmonster.rest.*; +import org.jboss.examples.ticketmonster.util.MultivaluedHashMap; +import org.jboss.examples.ticketmonster.util.qualifier.BotMessage; + +@Stateless +public class Bot { + + private static final Random random = new Random(System.nanoTime()); + + /** Frequency with which the bot will book **/ + public static final long DURATION = TimeUnit.SECONDS.toMillis(3); + + /** Maximum number of ticket requests that will be filed **/ + public static int MAX_TICKET_REQUESTS = 100; + + /** Maximum number of tickets per request **/ + public static int MAX_TICKETS_PER_REQUEST = 100; + + public static String [] BOOKERS = {"anne@acme.com", "george@acme.com", "william@acme.com", "victoria@acme.com", "edward@acme.com", "elizabeth@acme.com", "mary@acme.com", "charles@acme.com", "james@acme.com", "henry@acme.com", "richard@acme.com", "john@acme.com", "stephen@acme.com"}; + + @Inject + private ShowService showService; + + @Inject + private BookingService bookingService; + + @Inject @BotMessage + Event event; + + @Resource + private TimerService timerService; + + public Timer start() { + String startMessage = new StringBuilder("==========================\n") + .append("Bot started at ").append(new Date().toString()).append("\n") + .toString(); + event.fire(startMessage); + return timerService.createIntervalTimer(0, DURATION, new TimerConfig(null, false)); + } + + public void stop(Timer timer) { + String stopMessage = new StringBuilder("==========================\n") + .append("Bot stopped at ").append(new Date().toString()).append("\n") + .toString(); + event.fire(stopMessage); + timer.cancel(); + } + + @Timeout + public void book(Timer timer) { + // Select a show at random + Show show = selectAtRandom(showService.getAll(MultivaluedHashMap.empty())); + + // Select a performance at random + Performance performance = selectAtRandom(show.getPerformances()); + + String requestor = selectAtRandom(BOOKERS); + + BookingRequest bookingRequest = new BookingRequest(performance, requestor); + + List possibleTicketPrices = new ArrayList(show.getTicketPrices()); + + List indicies = selectAtRandom(MAX_TICKET_REQUESTS < possibleTicketPrices.size() ? MAX_TICKET_REQUESTS : possibleTicketPrices.size()); + + StringBuilder message = new StringBuilder("==========================\n") + .append("Booking by ") + .append(requestor) + .append(" at ") + .append(new Date().toString()) + .append("\n") + .append(performance) + .append("\n") + .append("~~~~~~~~~~~~~~~~~~~~~~~~~\n"); + + for (int index : indicies) { + int no = random.nextInt(MAX_TICKETS_PER_REQUEST); + TicketPrice price = possibleTicketPrices.get(index); + bookingRequest.addTicketRequest(new TicketRequest(price, no)); + message + .append(no) + .append(" of ") + .append(price.getSection()) + .append("\n"); + + } + Response response = bookingService.createBooking(bookingRequest); + if(response.getStatus() == Response.Status.OK.getStatusCode()) { + message.append("SUCCESSFUL\n") + .append("~~~~~~~~~~~~~~~~~~~~~~~~~\n"); + } + else { + message.append("FAILED:\n") + .append(((Map) response.getEntity()).get("errors")) + .append("~~~~~~~~~~~~~~~~~~~~~~~~~\n"); + } + event.fire(message.toString()); + } + + + + private T selectAtRandom(List list) { + int i = random.nextInt(list.size()); + return list.get(i); + } + + private T selectAtRandom(T[] array) { + int i = random.nextInt(array.length); + return array[i]; + } + + private T selectAtRandom(Collection collection) { + int item = random.nextInt(collection.size()); + int i = 0; + for(T obj : collection) + { + if (i == item) + return obj; + i++; + } + throw new IllegalStateException(); + } + + private List selectAtRandom(int max) { + List indicies = new ArrayList(); + for (int i = 0; i < max;) { + int r = random.nextInt(max); + if (!indicies.contains(r)) { + indicies.add(r); + i++; + } + } + return indicies; + } +} +--------------------------------------------------------------------------------------------------------- + +The `start()` and `stop(Timer timer)` methods are used to control the lifecycle of the `Bot`. When invoked, the `start()` method creates an interval timer that is scheduled to execute every 3 seconds. The complementary `stop(Timer timer)` method accepts a `Timer` handle, and cancels the associated interval timer. The `book(Timer timer)` is the callback method invoked by the container when the interval timer expires; it it therefore invoked every 3 seconds. The callback method selects a show at random, an associated performance for the chosen show at random, and finally attempts to perform a booking of a random number of seats. + +The Bot also fires CDI events containing log messages. To qualify the `String` messages produced by the Bot, we'll use the `BotMesssage` qualifier: + +.src/main/java/org/jboss/examples/ticketmonster/util/qualifier/BotMessage.java +[source,java] +--------------------------------------------------------------------------------------------------------- +package org.jboss.examples.ticketmonster.util.qualifier; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Target({ TYPE, METHOD, PARAMETER, FIELD }) +@Retention(RUNTIME) +@Documented +public @interface BotMessage { + +} +--------------------------------------------------------------------------------------------------------- + +The next step is to create a facade for the Bot that invokes the Bot's `start` and `stop` methods: + +.src/main/java/org/jboss/examples/ticketmonster/service/BotService.java +[source,java] +--------------------------------------------------------------------------------------------------------- +package org.jboss.examples.ticketmonster.service; + +import java.util.List; +import java.util.logging.Logger; + +import javax.ejb.Asynchronous; +import javax.ejb.Singleton; +import javax.ejb.Timer; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.inject.Inject; + +import org.jboss.examples.ticketmonster.model.Booking; +import org.jboss.examples.ticketmonster.rest.BookingService; +import org.jboss.examples.ticketmonster.util.CircularBuffer; +import org.jboss.examples.ticketmonster.util.MultivaluedHashMap; +import org.jboss.examples.ticketmonster.util.qualifier.BotMessage; + +/** + * A Bot service that acts as a Facade for the Bot, providing methods to control the Bot state as well as to obtain the current + * state of the Bot. + */ +@Singleton +public class BotService { + + private static final int MAX_LOG_SIZE = 50; + + private CircularBuffer log; + + @Inject + private Bot bot; + + @Inject + private BookingService bookingService; + + @Inject + private Logger logger; + + @Inject + @BotMessage + private Event event; + + private Timer timer; + + public BotService() { + log = new CircularBuffer(MAX_LOG_SIZE); + } + + public void start() { + synchronized (bot) { + if (timer == null) { + logger.info("Starting bot"); + timer = bot.start(); + } + } + } + + public void stop() { + synchronized (bot) { + if (timer != null) { + logger.info("Stopping bot"); + bot.stop(timer); + timer = null; + } + } + } + + @Asynchronous + public void deleteAll() { + synchronized (bot) { + stop(); + // Delete 10 bookings at a time + while(true) { + MultivaluedHashMap params = new MultivaluedHashMap(); + params.add("maxResults", Integer.toString(10)); + List bookings = bookingService.getAll(params); + for (Booking booking : bookings) { + bookingService.deleteBooking(booking.getId()); + event.fire("Deleted booking " + booking.getId() + " for " + + booking.getContactEmail() + "\n"); + } + if(bookings.size() < 1) { + break; + } + } + } + } + + public void newBookingRequest(@Observes @BotMessage String bookingRequest) { + log.add(bookingRequest); + } + + public List fetchLog() { + return log.getContents(); + } + + public boolean isBotActive() { + return (timer != null); + } + +} +--------------------------------------------------------------------------------------------------------- + +The `start` and `stop` methods of this facade wrap calls to the `start` and `stop` methods of the Bot. These methods are synchronous by nature. The `deleteAll` method is an asynchronous business method in this EJB. It first stops the Bot, and then proceeds to delete all Bookings. Bookings can take quite a while to be deleted depending on the number of existing ones, and hence declaring this method as `@Asynchronous` would be appropriate in this situation. Moreover, retrieving all Bookings in one execution run for deletion can lead to Out-of-Memory errors with a constrained heap space. The `deleteAll` method works around this by chunking the bookings to be deleted to a batch size of 10. You shall see how Java Batch (JSR-352) will aid you here, in a future version of TicketMonster that runs on a Java EE 7 compliant app server. For now, we will manage the batching manually. + +This facade also exposes the log messages produced by the Bot via the `fetchLog()` method. The contents of the log are backed by a `CircularBuffer`. The facade observes all `@BotMessage` events and adds the contents of each event to the buffer. + +Finally, the facade also provides an interface to detect if the bot is active or not: `isBotActive` that returns true if a Timer handle is present. + +We shall now proceed to create a `BotStatusService` class that exposes the operations on the Bot as a web-service. The `BotStatusService` will always return the current status of the Bot - whether the Bot has been started or stopped, and the messages in the Bot's log. The service also allows the client to change the state of the bot - to start the bot, or to stop it, or even delete all the bookings. + +The BotState is just an enumeration: + +.src/main/java/org/jboss/examples/ticketmonster/rest/BotState.java +[source,java] +--------------------------------------------------------------------------------------------------------- +package org.jboss.examples.ticketmonster.rest; + +/** + * An enumeration that represents the possible states for the Bot. + */ +public enum BotState { + RUNNING, NOT_RUNNING, RESET +} +--------------------------------------------------------------------------------------------------------- + +The `RUNNING` and `NOT_RUNNING` values are obvious. The `RESET` value is used to represent the state where the Bot will be stopped and the Bookings would be deleted. Quite naturally, the Bot will eventually enter the `NOT_RUNNING` state after it is `RESET`. + +The `BotStatusService` will be located at the `/bot` path. It would respond to GET requests at the `/messages` sub-path with the contents of the Bot's log. It will respond to GET requests at the `/status` sub-path with the JSON representation of the current BotState. And finally, it will respond to PUT requests containing the JSON representation of the BotState, provided tothe `/status` sub-path, by triggering a state change; a HTTP 204 response is returned in this case. + +.src/main/java/org/jboss/examples/ticketmonster/rest/BotStatusService.java +[source,java] +--------------------------------------------------------------------------------------------------------- +package org.jboss.examples.ticketmonster.rest; + +import java.util.List; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.jboss.examples.ticketmonster.service.BotService; + +/** + * A non-RESTful service for providing the current state of the Bot. This service also allows the bot to be started, stopped or + * the existing bookings to be deleted. + */ +@Path("/bot") +public class BotStatusService { + + @Inject + private BotService botService; + + /** + * Produces a JSON representation of the bot's log, containing a maximum of 50 messages logged by the Bot. + * + * @return The JSON representation of the Bot's log + */ + @Path("messages") + @GET + @Produces(MediaType.APPLICATION_JSON) + public List getMessages() { + return botService.fetchLog(); + } + + /** + * Produces a representation of the bot's current state. This is a string - "RUNNING" or "NOT_RUNNING" depending on whether + * the bot is active. + * + * @return The represntation of the Bot's current state. + */ + @Path("status") + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response getBotStatus() { + BotState state = botService.isBotActive() ? BotState.RUNNING + : BotState.NOT_RUNNING; + return Response.ok(state).build(); + } + + /** + * Updates the state of the Bot with the provided state. This may trigger the bot to start itself, stop itself, or stop and + * delete all existing bookings. + * + * @param updatedStatus The new state of the Bot. Only the state property is considered; any messages provided are ignored. + * @return An empty HTTP 201 response. + */ + @Path("status") + @PUT + public Response updateBotStatus(BotState updatedState) { + if (updatedState.equals(BotState.RUNNING)) { + botService.start(); + } else if (updatedState.equals(BotState.NOT_RUNNING)) { + botService.stop(); + } else if (updatedState.equals(BotState.RESET)) { + botService.deleteAll(); + } + return Response.noContent().build(); + } + +} +--------------------------------------------------------------------------------------------------------- + +[WARNING] +.Should the BotStatusService use JAX-RS? +======================================================================================= +The `BotStatusService` appears to be a RESTful service, but on closer examination it does not +obey the constraints of such a service. It represents a single resource - the `Bot` and not a collection of resources +where each item in the collected is uniquely identified. In other words, no resource like +`/bot/1` exists, and neither does a HTTP POST to `/bot` creates a new bot. This affects +the design of the Backbone.js models in the client, as we shall later see. + +Therefore, it is not necessary to use JAX-RS in this scenario. JAX-RS certainly makes it +easier, since we can continue to use the same programming model with minor changes. There is +no need to parse requests or serialize responses or lookup EJBs; JAX-RS does this for us. +The alternative would be to use a Servlet or a JSON-RPC endpoint. + +We would recommend adoption alternatives in real-life scenarios should they be more suitable. +======================================================================================= + +== Displaying Metrics + +We are set up now and ready to start coding the client-side section of the dashboard. The users will be able to view the list of performances and view the occupied count for that performance. + +=== The Metrics model + +We'll define a Backbone model to represent the metric data for an individual show. + +.src/main/webapp/resources/js/app/models/metric.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * Module for the Metric model + */ +define([ + // Configuration is a dependency + 'configuration', + 'backbone' +], function (config) { + + /** + * The Metric model class definition + * Used for CRUD operations against individual Metric + */ + var Metric = Backbone.Model.extend({ + idAttribute: "show" + }); + + return Metric; + +}); +------------------------------------------------------------------------------------------------------- + +We've specified the `show` property as the `idAttribute` for the model. This is necessary since every resource in the collection is uniquely identified by the show property in the representation. +Also note that the Backbone model does not define a `urlRoot` property unlike other Backbone models. The representation for an individual metric resource cannot be obtained by navigating to `/metrics/X`, but the metrics for all shows can be obtained by navigating to `/metrics`. + +=== The Metrics collection + +We now define a Backbone collection for handling the metrics collection: + +.src/main/webapp/resources/js/app/collections/metrics.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * The module for a collection of Metrics + */ +define([ + 'app/models/metric', + 'configuration', + 'backbone' +], function (Metric, config) { + + // Here we define the Metrics collection + // We will use it for CRUD operations on Metrics + + var Metrics = Backbone.Collection.extend({ + url: config.baseUrl + 'rest/metrics', + model: Metric + }); + + return Metrics; +}); +------------------------------------------------------------------------------------------------------- + +We have thus mapped the collection to the `MetricsService` REST resource, so we can perform CRUD operations against this resource. In practice however, we'll need to only query this resource. + +=== The MetricsView view + +Now that we have the model and the collection, let's create the view to display the metrics: + +.src/main/webapp/resources/js/app/views/desktop/metrics.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +define([ + 'backbone', + 'configuration', + 'utilities', + 'text!../../../../templates/desktop/metrics.html' +], function ( + Backbone, + config, + utilities, + metricsTemplate) { + + var MetricsView = Backbone.View.extend({ + intervalDuration : 3000, + initialize : function() { + _.bind(this.render, this); + _.bind(this.liveUpdate, this); + this.collection.on("add remove change", this.render, this); + var self = this; + $.when(this.collection.fetch({ + error : function() { + utilities.displayAlert("Failed to retrieve metrics from the TicketMonster server."); + } + })).done(function(){ + self.liveUpdate(); + }); + }, + liveUpdate : function() { + this.collection.fetch({ + error : function() { + utilities.displayAlert("Failed to retrieve metrics from the TicketMonster server."); + } + }); + var self = this; + this.timerObject = setTimeout(function(){ + self.liveUpdate(); + }, this.intervalDuration); + }, + render : function () { + utilities.applyTemplate($(this.el), metricsTemplate, {collection:this.collection}); + return this; + }, + onClose : function() { + if(this.timerObject) { + clearTimeout(this.timerObject); + delete this.timerObject; + } + } + }); + + return MetricsView; +}); +------------------------------------------------------------------------------------------------------- + +Like other Backbone views, the view is attached to a DOM element (the el property). When the render method is invoked, it manipulates the DOM and renders the view. The `metricsTemplate` template is used to structure the HTML, thus separating the HTML view code from the view implementation. + +The render method is invoked whenever the underlying collection is modified. The view is associated with a timer that is executed repeatedly with a predetermined interval of 3 seconds. When the timer is triggered, it fetches the updated state of the collection (the metrics) from the server. Any change in the collection at this point, now triggers a refresh of the view as pointed out earlier. + +When the view is closed/destroyed, the associated timer if present is cleared. + +.src/main/webapp/resources/templates/desktop/metrics.html +[source,html] +------------------------------------------------------------------------------------------------------- +
+ +
+ <% + _.each(collection.models, function (show) { + %> +
+
<%=show.get('event')%> @ <%=show.get('venue')%>
+ <%_.each(show.get('performances'), function (performance) {%> +
+
<%=new Date(performance.date).toLocaleString()%>
+
+
+
+
+
+
<%=performance.occupiedCount%> of <%=show.get('capacity')%> tickets booked
+
+ <% }); %> +
+ <% }); %> +
+
+------------------------------------------------------------------------------------------------------- + +The HTML for the view groups the metrics by show. Every performance associated with the show is displayed in this group, with the occupied count used to populate a Bootstrap progress bar. The width of the bar is computed with the occupied count for the performance and the capacity for the show (i.e. capacity for the venue hosting the show). + +== Displaying the Bot interface + +=== The Bot model + +We'll define a plain JavaScript object to represent the Bot on the client-side. Recalling the earlier discussion, the Bot service at the server is not a RESTful service. Since it cannot be identified uniquely, it would require a few bypasses in a Backbone model (like overriding the `url` property) to communicate correctly with the service. Additionally, obtaining the Bot's log messages would require using jQuery since the log messages also cannot be represented cleanly as a REST resource. Given all these factors, it would make sense to use a plain JavaScript object to represent the Bot model. + +.src/main/webapp/resources/js/app/models/bot.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * Module for the Bot model + */ +define([ + 'jquery', + 'configuration', +], function ($, config) { + + /** + * The Bot model class definition + * Used perform operations on the Bot. + * Note that this is not a Backbone model. + */ + var Bot = function() { + this.statusUrl = config.baseUrl + 'rest/bot/status'; + this.messagesUrl = config.baseUrl + 'rest/bot/messages'; + } + + /* + * Start the Bot by sending a request to the Bot resource + * with the new status of the Bot set to "RUNNING". + */ + Bot.prototype.start = function() { + $.ajax({ + type: "PUT", + url: this.statusUrl, + data: "\"RUNNING\"", + dataType: "json", + contentType: "application/json" + }); + } + + /* + * Stop the Bot by sending a request to the Bot resource + * with the new status of the Bot set to "NOT_RUNNING". + */ + Bot.prototype.stop = function() { + $.ajax({ + type: "PUT", + url: this.statusUrl, + data: "\"NOT_RUNNING\"", + dataType: "json", + contentType: "application/json" + }); + } + + /* + * Stop the Bot and delete all bookings by sending a request to the Bot resource + * with the new status of the Bot set to "RESET". + */ + Bot.prototype.reset = function() { + $.ajax({ + type: "PUT", + url: this.statusUrl, + data: "\"RESET\"", + dataType: "json", + contentType: "application/json" + }); + } + + /* + * Fetch the log messages of the Bot and invoke the callback. + * The callback is provided with the log messages (an array of Strings). + */ + Bot.prototype.fetchMessages = function(callback) { + $.get(this.messagesUrl, function(data) { + if(callback) { + callback(data); + } + }); + } + + return Bot; + +}); +------------------------------------------------------------------------------------------------------- + +The start, stop and rest methods issue HTTP requests to the Bot service at the `rest/bot/status` URL with jQuery. The fetchMessages method issues a HTTP request to the Bot service at the `rest/bot/messages` URL with jQuery; it accepts a callback method as a parameter and invokes the callback once it receives a response from the server. + +=== The BotView view + +Now that we have the model, let's create the view to control the Bot: + +.src/main/webapp/resources/js/app/views/desktop/bot.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +define([ + 'jquery', + 'underscore', + 'backbone', + 'configuration', + 'utilities', + 'text!../../../../templates/desktop/bot.html' +], function ( + $, + _, + Backbone, + config, + utilities, + botTemplate) { + + var BotView = Backbone.View.extend({ + intervalDuration : 3000, + initialize : function() { + _.bind(this.liveUpdate, this); + _.bind(this.startBot, this); + _.bind(this.stopBot, this); + _.bind(this.resetBot, this); + utilities.applyTemplate($(this.el), botTemplate, {}); + this.liveUpdate(); + }, + events: { + "click #start-bot" : "startBot", + "click #stop-bot" : "stopBot", + "click #reset" : "resetBot" + }, + liveUpdate : function() { + this.model.fetchMessages(this.renderMessages); + var self = this; + this.timerObject = setTimeout(function() { + self.liveUpdate(); + }, this.intervalDuration); + }, + renderMessages : function(data) { + var displayMessages = data.reverse(); + var botLog = $("textarea").get(0); + // The botLog textarea element may have been removed if the user navigated to a different view + if(botLog) { + botLog.value = displayMessages.join(""); + } + }, + onClose : function() { + if(this.timerObject) { + clearTimeout(this.timerObject); + delete this.timerObject; + } + }, + startBot : function() { + this.model.start(); + // Refresh the log immediately without waiting for the live update to trigger. + this.model.fetchMessages(this.renderMessages); + }, + stopBot : function() { + this.model.stop(); + // Refresh the log immediately without waiting for the live update to trigger. + this.model.fetchMessages(this.renderMessages); + }, + resetBot : function() { + this.model.reset(); + // Refresh the log immediately without waiting for the live update to trigger. + this.model.fetchMessages(this.renderMessages); + } + }); + + return BotView; +}); +------------------------------------------------------------------------------------------------------- + +This view is similar to other Backbone views in most aspects, except for a few. When the view initialized, it manipulates the DOM and renders the view; this is unlike other views that are not rendered on initialization. The `botTemplate` template is used to structure the HTML. An interval timer with a pre-determined duration of 3 seconds is also created when the view is initialized. When the view is closed/destroyed, the timer if present is cleared out. + +When the timer is triggered, it fetches the Bot's log messages. The `renderMessages` method is provided as the callback to the `fetchMessages` invocation. The `renderMessages` callback method is provided with the log messages from the server, and it proceeds to update a textarea with these messages. + +The startBot, stopBot and resetBot event handlers are setup to handle click events on the associated buttons in the view. They merely delegate to the model to perform the actual operations. + +.src/main/webapp/resources/templates/desktop/bot.html +[source,html] +------------------------------------------------------------------------------------------------------- +
+ +
+
+ + + +
+
+
Bot Log
+ +
+
+
+------------------------------------------------------------------------------------------------------- + +The HTML for the view creates a button group for the actions possible on the Bot. It also carries a text area for displaying the Bot's log messages. + +== Creating the dashboard + +Now that we have the constituent views for the dashboard, let's wire it up into the application. + +=== Creating a composite Monitor view + +Let's create a composite Backbone view to hold the MetricsView and BotView as it's constituent sub-views. + +.src/main/webapp/resources/js/app/views/desktop/monitor.js +[source, javascript] +------------------------------------------------------------------------------------------------------- +define([ + 'backbone', + 'configuration', + 'utilities', + 'app/models/bot', + 'app/collections/metrics', + 'app/views/desktop/bot', + 'app/views/desktop/metrics', + 'text!../../../../templates/desktop/monitor.html' +], function ( + Backbone, + config, + utilities, + Bot, + Metrics, + BotView, + MetricsView, + monitorTemplate) { + + var MonitorView = Backbone.View.extend({ + render : function () { + utilities.applyTemplate($(this.el), monitorTemplate, {}); + var metrics = new Metrics(); + this.metricsView = new MetricsView({collection:metrics, el:$("#metrics-view")}); + var bot = new Bot(); + this.botView = new BotView({model:bot,el:$("#bot-view")}); + return this; + }, + onClose : function() { + if(this.botView) { + this.botView.close(); + } + if(this.metricsView) { + this.metricsView.close(); + } + } + }); + + return MonitorView; +}); +------------------------------------------------------------------------------------------------------- + +The render method of this Backbone view creates the two sub-views and renders them. It also initializes the necessary models and collections required by the sub-views. All other aspects of the view like event handling and updates to the DOM are handled by the sub-views. When the composite view is destroyed, it also closes the sub-views gracefully. + +The HTML template used by the composite just lays out a structure for the sub-views to control two distinct areas of the DOM - a div with id `metrics-view` for displaying the metrics, and another div with id `bot-view` to control the bot: + +.src/main/webapp/resources/templates/desktop/monitor.html +[source,html] +------------------------------------------------------------------------------------------------------- +
+
+
+
+
+
+------------------------------------------------------------------------------------------------------- + +=== Configure the router + +Finally, let us wire up the router to display the monitor when the user navigates to the `monitor` route in the Backbone application: + +.src/main/webapp/resources/js/app/router/desktop/router.js +[source, javascript] +------------------------------------------------------------------------------------------------------- +define("router", [ + ... + 'app/views/desktop/monitor', + ... +],function (... + MonitorView, + ...) { + + ... + + var Router = Backbone.Router.extend({ + ... + routes : { + ..., + "monitor":"displayMonitor" + }, + ..., + displayMonitor:function() { + var monitorView = new MonitorView({el:$("#content")}); + utilities.viewManager.showView(monitorView); + }, + }); +------------------------------------------------------------------------------------------------------- + +With this configuration, the user can now navigate to the monitor section of the application, where the metrics and the bot controls would be displayed. The underlying sub-views would poll against the server to update themselves in near real-time offering a dashboard solution to TicketMonster. diff --git a/tutorial/DataPersistence.asciidoc b/tutorial/DataPersistence.asciidoc new file mode 100644 index 000000000..0208455d7 --- /dev/null +++ b/tutorial/DataPersistence.asciidoc @@ -0,0 +1,2061 @@ += Building the persistence layer with JPA2 and Bean Validation +:Author: Pete Muir +:thumbnail: http://static.jboss.org/ffe/1/www/origin/ticket-monster-splash-2.png + +== What will you learn here? + + +You have set up your project successfully. Now it is time to begin working on the TicketMonster +application, and the first step is adding the persistence layer. After reading this guide, +you'll understand what design and implementation choices to make. Topics covered include: + +* RDBMS design using JPA entity beans +* How to validate your entities using Bean Validation +* How to populate test data +* Basic unit testing using JUnit + +We'll round out the guide by revealing the required, yet short and sweet, configuration. + +The tutorial will show you how to perform all these steps in JBoss Developer Studio, including +screenshots that guide you through. For those of you who prefer to watch and learn, the included +videos show you how we performed all the steps. + +TicketMonster contains 14 entities, of varying complexity. In the introduction, you have seen +the basic steps for creating a couple of entities (`Event` and `Venue`) and interacting with them. +In this tutorial we'll go deeper into domain model design, we'll classify the entities, and +walk through designing and creating one of each group. + +[[YourFirstEntity]] +== Your first entity + + +The simplest kind of entities are often those representing lookup tables. `TicketCategory` is a classic lookup table that defines the ticket types available (e.g. Adult, Child, Pensioner). A ticket category has one property - _description_. + +[TIP] +.What's in a name? +===================================================================================== +Using a consistent naming scheme for your entities can help another developer get up +to speed with your code base. We've named all our lookup tables XXXCategory to allow +us to easily spot them. +===================================================================================== + + +Let's start by creating a JavaBean to represent the ticket category: + +.src/main/java/org/jboss/examples/ticketmonster/model/TicketCategory.java +[source,java] +------------------------------------------------------------------------------------------------------- +public class TicketCategory { + + /* Declaration of fields */ + + /** + *

+ * The description of the of ticket category. + *

+ * + */ + private String description; + + /* Boilerplate getters and setters */ + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public String toString() { + return description; + } +} +------------------------------------------------------------------------------------------------------- + +We're going to want to keep the ticket category in collections (for example, to present it as part of drop down in the UI), so it's important that we properly implement `equals()` and `hashCode()`. At this point, we need to define a property (or group of properties) that uniquely identifies the ticket category. We refer to these properties as the "entity's natural identity". + +[TIP] +.Defining an entity's natural identity +===================================================================================== +Using an ORM introduces additional constraints on object identity. Defining the +properties that make up an entity's natural identity can be tricky, but is very +important. Using the object's identity, or the synthetic identity (database generated +primary key) identity can introduce unexpected bugs into your application, so you +should always ensure you use a natural identity. You can read more about the issue at +https://community.jboss.org/wiki/EqualsAndHashCode. +===================================================================================== + +For ticket category, the choice of natural identity is easy and obvious - it must be the one property, _description_ that the entity has! Having identified the natural identity, adding an `equals()` and `hashCode()` method is easy. In Eclipse, choose _Source -> Generate hashCode() and equals()..._ + +[[eclipse-generate-hashcode-equals]] +.Generate hashCode() and equals() in Eclipse +image::gfx/eclipse-generate-hashcode-equals.png + +Now, select the properties to include: + +[[eclipse-generate-hashcode-equals-2]] +.Generate hashCode() and equals() in Eclipse +image::gfx/eclipse-generate-hashcode-equals-2.png + +Now that we have a JavaBean, let's proceed to make it an entity. First, add the `@Entity` annotation to the class: + +.src/main/java/org/jboss/examples/ticketmonster/model/TicketCategory.java +[source,java] +------------------------------------------------------------------------------------------------------- +@Entity +public class TicketCategory { + + ... + +} +------------------------------------------------------------------------------------------------------- + +And, add the synthetic id: + +.src/main/java/org/jboss/examples/ticketmonster/model/TicketCategory.java +[source,java] +------------------------------------------------------------------------------------------------------- +@Entity +public class TicketCategory { + + /* Declaration of fields */ + + /** + * The synthetic id of the object. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + ... + + /* Boilerplate getters and setters */ + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + ... + +} +------------------------------------------------------------------------------------------------------- + +As we decided that our natural identifier was the `description`, we should introduce a unique constraint on the property: + +.src/main/java/org/jboss/examples/ticketmonster/model/TicketCategory.java +[source,java] +------------------------------------------------------------------------------------------------------- +@Entity +public class TicketCategory { + + /* Declaration of fields */ + + ... + + /** + *

+ * The description of the of ticket category. + *

+ * + *

+ * The description forms the natural id of the ticket category, and so must be unique. + *

+ * + */ + @Column(unique = true) + private String description; + + ... + +} +------------------------------------------------------------------------------------------------------- + +It's very important that any data you place in the database is of the highest quality - this data is probably one of your organisations most valuable assets! To ensure that bad data doesn't get saved to the database by mistake, we'll use Bean Validation to enforce constraints on our properties. + +[NOTE] +.What is Bean Validation? +===================================================================================== +Bean Validation (JSR 303) is a Java EE specification which: + +* provides a unified way of declaring and defining constraints on an object model. +* defines a runtime engine to validate objects + +Bean Validation includes integration with other Java EE specifications, such as JPA. +Bean Validation constraints are automatically applied before data is persisted to the +database, as a last line of defence against bad data. +===================================================================================== + +The _description_ of the ticket category should not be empty for two reasons. Firstly, an empty ticket category description is no use to a person trying to book a ticket - it doesn't convey any information. Secondly, as the description forms the natural identity, we need to make sure the property is always populated. + +Let's add the Bean Validation constraint `@NotEmpty`: + +.src/main/java/org/jboss/examples/ticketmonster/model/TicketCategory.java +[source,java] +------------------------------------------------------------------------------------------------------- +@Entity +public class TicketCategory { + + /* Declaration of fields */ + + ... + + /** + *

+ * The description of the of ticket category. + *

+ * + *

+ * The description forms the natural id of the ticket category, and so must be unique. + *

+ * + *

+ * The description must not be null and must be one or more characters, the Bean Validation constraint @NotEmpty + * enforces this. + *

+ * + */ + @Column(unique = true) + @NotEmpty + private String description; + + ... +} +------------------------------------------------------------------------------------------------------- + +And that is our first entity! Here is the complete entity: + +.src/main/java/org/jboss/examples/ticketmonster/model/TicketCategory.java +[source,java] +------------------------------------------------------------------------------------------------------- +/** + *

+ * A lookup table containing the various ticket categories. E.g. Adult, Child, Pensioner, etc. + *

+ */ +@Entity +public class TicketCategory { + + /* Declaration of fields */ + + /** + * The synthetic id of the object. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + *

+ * The description of the of ticket category. + *

+ * + *

+ * The description forms the natural id of the ticket category, and so must be unique. + *

+ * + *

+ * The description must not be null and must be one or more characters, the Bean Validation constraint @NotEmpty + * enforces this. + *

+ * + */ + @Column(unique = true) + @NotEmpty + private String description; + + /* Boilerplate getters and setters */ + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + /* toString(), equals() and hashCode() for TicketCategory, using the natural identity of the object */ + + @Override + public String toString() { + return description; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((description == null) ? 0 : description.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof TicketCategory)) + return false; + TicketCategory other = (TicketCategory) obj; + if (description == null) { + if (other.description != null) + return false; + } else if (!description.equals(other.description)) + return false; + return true; + } +} +------------------------------------------------------------------------------------------------------- + + +TicketMonster contains another lookup tables, `EventCategory`. It's pretty much identical to `TicketCategory`, so we leave it as an exercise to the reader to investigate, and understand. If you are building the application whilst following this tutorial, copy the source over from the TicketMonster example. + +== Database design & relationships + + +First, let's understand the the entity design. + +An `Event` may occur at any number of venues, on various days and at various times. The intersection between an event and a venue is a `Show`, and each show can have a `Performance` which is associated with a date and time. + +Venues are a separate grouping of entities, which, as mentioned, intersect with events via shows. Each venue consists of groupings of seats, each known as a `Section`. + +Every section, in every show is associated with a ticket category via the `TicketPrice` entity. + +Users must be able to book tickets for performances. A `Booking` is associated with a performance, and contains a collection of tickets. + +Finally, both events and venues can have "media items", such as images or videos attached. + +[[database-design]] +.Entity-Relationship Diagram +image::gfx/database-design.png[scaledwidth="70%"] + +=== Media items + + +Storing large binary objects, such as images or videos in the database isn't advisable (as it can lead to performance issues), and playback of videos can also be tricky, as it depends on browser capabilities. For TicketMonster, we decided to make use of existing services to host images and videos, such as YouTube or Flickr. All we store in the database is the URL the application should use to access the media item, and the type of the media item (note that the URL forms a media items natural identifier). We need to know the type of the media item in order to render the media correctly in the view layer. + +In order for a view layer to correctly render the media item (e.g. display an image, embed a media player), it's likely that special code has had to have been added. For this reason we represent the types of media that TicketMonster understands as a closed set, unmodifiable at runtime. An enum is perfect for this! + +Luckily, JPA has native support for enums, all we need to do is add the `@Enumerated` annotation: + +.src/main/java/org/jboss/examples/ticketmonster/model/MediaItem.java +[source,java] +------------------------------------------------------------------------------------------------------- + + ... + + /** + *

+ * The type of the media, required to render the media item correctly. + *

+ * + *

+ * The media type is a closed set - as each different type of media requires support coded into the view layers, it + * cannot be expanded upon without rebuilding the application. It is therefore represented by an enumeration. We instruct + * JPA to store the enum value using it's String representation, so that we can later reorder the enum members, without + * changing the data. Of course, this does mean we can't change the names of media items once the app is put into + * production. + *

+ */ + @Enumerated(STRING) + private MediaType mediaType; + + ... +------------------------------------------------------------------------------------------------------- + +[TIP] +.@Enumerated(STRING) or @Enumerated(ORDINAL)? +===================================================================================== +JPA can store an enum value using it's ordinal (position in the list of declared enums) +or it's STRING (the name it is given). If you choose to store an ordinal, you musn't alter +the order of the list. If you choose to store the name, you musn't change the enum name. +The choice is yours! +===================================================================================== + +The rest of `MediaItem` shouldn't present a challenge to you. If you are building the application whilst following this tutorial, copy both `MediaItem` and `MediaType` from the TicketMonster project. + +=== Events + + +In the section <>, we saw how to build simple entities with properties, identify and apply constraints using Bean Validation, identify the natural id and add a synthetic id. From now on we'll assume you know how to build simple entities - for each new entity that we build, we will start with it's basic structure and properties filled in. + +So, here we modify the Event class (from where we left at the end of the introduction). The below listing also includes some comments reflecting the explanations above. We will remove a few fields - `version`, `major` and `picture`, update the annotations on the `id` field, and update the `toString`, `equals` and `hashCode` methods to use the natural key of the object): + +.src/main/java/org/jboss/examples/ticketmonster/model/Event.java +[source,java] +------------------------------------------------------------------------------------------------------- + +@Entity +public class Event { + + /* Declaration of fields */ + + /** + * The synthetic ID of the object. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + *

+ * The name of the event. + *

+ * + *

+ * The name of the event forms it's natural identity and cannot be shared between events. + *

+ * + *

+ * Two constraints are applied using Bean Validation + *

+ * + *
    + *
  1. @NotNull — the name must not be null.
  2. + *
  3. @Size — the name must be at least 5 characters and no more than 50 characters. This allows for + * better formatting consistency in the view layer.
  4. + *
+ */ + @Column(unique = true) + @NotNull + @Size(min = 5, max = 50, message = "An event's name must contain between 5 and 50 characters") + private String name; + + /** + *

+ * A description of the event. + *

+ * + *

+ * Two constraints are applied using Bean Validation + *

+ * + *
    + *
  1. @NotNull — the description must not be null.
  2. + *
  3. @Size — the name must be at least 20 characters and no more than 1000 characters. This allows for + * better formatting consistency in the view layer, and also ensures that event organisers provide at least some description + * - a classic example of a business constraint.
  4. + *
+ */ + @NotNull + @Size(min = 20, max = 1000, message = "An event's description must contain between 20 and 1000 characters") + private String description; + + + /* Boilerplate getters and setters */ + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + /* toString(), equals() and hashCode() for Event, using the natural identity of the object */ + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Event event = (Event) o; + + if (name != null ? !name.equals(event.name) : event.name != null) + return false; + + return true; + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } + + @Override + public String toString() { + return name; + } +} +------------------------------------------------------------------------------------------------------- + +First, let's add a media item to `Event`. As multiple events (or venues) could share the same media item, we'll model the relationship as _many-to-one_ - many events can reference the same media item. + +[TIP] +.Relationships supported by JPA +===================================================================================== +JPA can model four types of relationship between entities - one-to-one, one-to-many, +many-to-one and many-to-many. A relationship may be bi-directional (both sides of the +relationship know about each other) or uni-directional (only one side knows about the +relationship). + +Many database models are hierarchical (parent-child), as is TicketMonster's. As a result, +you'll probably find you mostly use one-to-many and many-to-one relationships, which +allow building parent-child models. +===================================================================================== + +Creating a many-to-one relationship is very easy in JPA. Just add the `@ManyToOne` annotation to the field. JPA will take care of the rest. Here's the property for `Event`: + +.src/main/java/org/jboss/examples/ticketmonster/model/Event.java +[source,java] +------------------------------------------------------------------------------------------------------- + + ... + + /** + *

+ * A media item, such as an image, which can be used to entice a browser to book a ticket. + *

+ * + *

+ * Media items can be shared between events, so this is modeled as a @ManyToOne relationship. + *

+ * + *

+ * Adding a media item is optional, and the view layer will adapt if none is provided. + *

+ * + */ + @ManyToOne + private MediaItem mediaItem; + + ... + + public MediaItem getMediaItem() { + return mediaItem; + } + + public void setMediaItem(MediaItem picture) { + this.mediaItem = picture; + } + + ... +------------------------------------------------------------------------------------------------------- + +There is no need for a media item to know who references it (in fact, this would be a poor design, as it would reduce the reusability of `MediaItem`), so we can leave this as a uni-directional relationship. + +An event will also have a category. Once again, many events can belong to the same event category, and there is no need for an event category to know what events are in it. To add this relationship, we add the `eventCategory` property, and annotate it with `@ManyToOne`, just as we did for `MediaItem`: + +.src/main/java/org/jboss/examples/ticketmonster/model/Event.java +[source,java] +------------------------------------------------------------------------------------------------------- + + ... + + /** + *

+ * The category of the event + *

+ * + *

+ * Event categories are used to ease searching of available of events, and hence this is modeled as a relationship + *

+ * + *

+ * The Bean Validation constraint @NotNull indicates that the event category must be specified. + */ + @ManyToOne + @NotNull + private EventCategory category; + + ... + + public EventCategory getCategory() { + return category; + } + + public void setCategory(EventCategory category) { + this.category = category; + } + + ... +------------------------------------------------------------------------------------------------------- + +And that's Event created. Here is the full source: + +.src/main/java/org/jboss/examples/ticketmonster/model/Event.java +[source,java] +------------------------------------------------------------------------------------------------------- +/** + *

+ * Represents an event, which may have multiple performances with different dates and venues. + *

+ * + *

+ * Event's principal members are it's relationship to {@link EventCategory} - specifying the type of event it is - and + * {@link MediaItem} - providing the ability to add media (such as a picture) to the event for display. It also contains + * meta-data about the event, such as it's name and a description. + *

+ * + */ +@Entity +public class Event { + + /* Declaration of fields */ + + /** + * The synthetic ID of the object. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + *

+ * The name of the event. + *

+ * + *

+ * The name of the event forms it's natural identity and cannot be shared between events. + *

+ * + *

+ * Two constraints are applied using Bean Validation + *

+ * + *
    + *
  1. @NotNull — the name must not be null.
  2. + *
  3. @Size — the name must be at least 5 characters and no more than 50 characters. This allows for + * better formatting consistency in the view layer.
  4. + *
+ */ + @Column(unique = true) + @NotNull + @Size(min = 5, max = 50, message = "An event's name must contain between 5 and 50 characters") + private String name; + + /** + *

+ * A description of the event. + *

+ * + *

+ * Two constraints are applied using Bean Validation + *

+ * + *
    + *
  1. @NotNull — the description must not be null.
  2. + *
  3. @Size — the name must be at least 20 characters and no more than 1000 characters. This allows for + * better formatting consistency in the view layer, and also ensures that event organisers provide at least some description + * - a classic example of a business constraint.
  4. + *
+ */ + @NotNull + @Size(min = 20, max = 1000, message = "An event's name must contain between 20 and 1000 characters") + private String description; + + /** + *

+ * A media item, such as an image, which can be used to entice a browser to book a ticket. + *

+ * + *

+ * Media items can be shared between events, so this is modeled as a @ManyToOne relationship. + *

+ * + *

+ * Adding a media item is optional, and the view layer will adapt if none is provided. + *

+ * + */ + @ManyToOne + private MediaItem mediaItem; + + /** + *

+ * The category of the event + *

+ * + *

+ * Event categories are used to ease searching of available of events, and hence this is modeled as a relationship + *

+ * + *

+ * The Bean Validation constraint @NotNull indicates that the event category must be specified. + */ + @ManyToOne + @NotNull + private EventCategory category; + + /* Boilerplate getters and setters */ + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public MediaItem getMediaItem() { + return mediaItem; + } + + public void setMediaItem(MediaItem picture) { + this.mediaItem = picture; + } + + public EventCategory getCategory() { + return category; + } + + public void setCategory(EventCategory category) { + this.category = category; + } + + /* toString(), equals() and hashCode() for Event, using the natural identity of the object */ + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Event event = (Event) o; + + if (name != null ? !name.equals(event.name) : event.name != null) + return false; + + return true; + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } + + @Override + public String toString() { + return name; + } +} +------------------------------------------------------------------------------------------------------- + +=== Venue + + +Now, let's build out the entities to represent the venue. + +We start by adding an entity to represent the venue. A venue needs to have a name, a description, +a capacity, an address, an associated media item and a set of sections in which people can sit. If +you completed the introduction chapter, you should already have some of these properties set, so +we will update the `Venue` class to look like in the definition below. + +.src/main/java/org/jboss/examples/ticketmonster/model/Venue.java +[source,java] +------------------------------------------------------------------------------------------------------- + +/** + *

+ * Represents a single venue + *

+ * + */ +@Entity +public class Venue { + + /* Declaration of fields */ + + /** + * The synthetic id of the object. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + *

+ * The name of the event. + *

+ * + *

+ * The name of the event forms it's natural identity and cannot be shared between events. + *

+ * + *

+ * The name must not be null and must be one or more characters, the Bean Validation + * constraint @NotEmpty enforces this. + *

+ */ + @Column(unique = true) + @NotEmpty + private String name; + + /** + * The address of the venue + */ + @Embedded + private Address address = new Address(); + + /** + * A description of the venue + */ + private String description; + + /** + *

+ * A set of sections in the venue + *

+ * + *

+ * The @OneToMany JPA mapping establishes this relationship. + * Collection members are fetched eagerly, so that they can be accessed even after the + * entity has become detached. This relationship is bi-directional (a section knows which + * venue it is part of), and the mappedBy attribute establishes this. We + * cascade all persistence operations to the set of performances, so, for example if a venue + * is removed, then all of it's sections will also be removed. + *

+ */ + @OneToMany(cascade = ALL, fetch = EAGER, mappedBy = "venue") + private Set
sections = new HashSet
(); + + /** + * The capacity of the venue + */ + private int capacity; + + /** + * An optional media item to entice punters to the venue. The @ManyToOne establishes the relationship. + */ + @ManyToOne + private MediaItem mediaItem; + + /* Boilerplate getters and setters */ + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + public MediaItem getMediaItem() { + return mediaItem; + } + + public void setMediaItem(MediaItem description) { + this.mediaItem = description; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Set
getSections() { + return sections; + } + + public void setSections(Set
sections) { + this.sections = sections; + } + + public int getCapacity() { + return capacity; + } + + public void setCapacity(int capacity) { + this.capacity = capacity; + } + + /* toString(), equals() and hashCode() for Venue, using the natural identity of the object */ + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Venue venue = (Venue) o; + + if (address != null ? !address.equals(venue.address) : venue.address != null) + return false; + if (name != null ? !name.equals(venue.name) : venue.name != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (address != null ? address.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return name; + } +} +------------------------------------------------------------------------------------------------------- + +In creating this entity, we've followed all the design and implementation decisions previously discussed, with one new concept. Rather than add the properties for street, city, postal code etc. to this object, we've extracted them into the `Address` object, and included it in the `Venue` object using composition. This would allow us to reuse the Address object in other places (such as a customer's address). + +A RDBMS doesn't have a similar concept to composition, so we need to choose whether to represent the address as a separate entity, and create a relationship between the venue and the address, or whether to map the properties from `Address` to the table for the owning entity, in this case `Venue`. It doesn't make much sense for an address to be a full entity - we're not going to want to run queries against the address in isolation, nor do we want to be able to delete or update an address in isolation - in essence, the address doesn't have a standalone identity outside of the object into which it is composed. + +To _embed_ the `Address` into `Venue` we add the `@Embeddable` annotation to the `Address` class. However, unlike a full entity, there is no need to add an identifier. Here's the source for `Address`: + +.src/main/java/org/jboss/examples/ticketmonster/model/Address.java +[source,java] +------------------------------------------------------------------------------------------------------- + +/** + *

+ * A reusable representation of an address. + *

+ * + *

+ * Addresses are used in many places in an application, so to observe the DRY principle, we model Address as an embeddable + * entity. An embeddable entity appears as a child in the object model, but no relationship is established in the RDBMS.. + *

+ */ +@Embeddable +public class Address { + + /* Declaration of fields */ + private String street; + private String city; + private String country; + + /* Declaration of boilerplate getters and setters */ + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + /* toString(), equals() and hashCode() for Address, using the natural identity of the object */ + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Address address = (Address) o; + + if (city != null ? !city.equals(address.city) : address.city != null) + return false; + if (country != null ? !country.equals(address.country) : address.country != null) + return false; + if (street != null ? !street.equals(address.street) : address.street != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = street != null ? street.hashCode() : 0; + result = 31 * result + (city != null ? city.hashCode() : 0); + result = 31 * result + (country != null ? country.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return street + ", " + city + ", " + country; + } +} +------------------------------------------------------------------------------------------------------- + +=== Sections + + +A venue consists of a number of seating sections. Each seating section has a name, a description, the number of rows in the section, and the number of seats in a row. It's natural identifier is the name of section combined with the venue (a venue can't have two sections with the same name). `Section` doesn't introduce any new concepts, so go ahead and copy the source from the below listing: + +.src/main/java/org/jboss/examples/ticketmonster/model/Section.java +[source,java] +------------------------------------------------------------------------------------------------------- +@SuppressWarnings("serial") +@Entity +@Table(uniqueConstraints=@UniqueConstraint(columnNames={"name", "venue_id"})) +public class Section implements Serializable { + + /* Declaration of fields */ + + /** + * The synthetic id of the object. + */ + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + /** + *

+ * The short name of the section, may be a code such as A12, G7, etc. + *

+ * + *

+ * The + * @NotEmpty Bean Validation constraint means that the section name must be at least 1 character. + *

+ */ + @NotEmpty + private String name; + + /** + *

+ * The description of the section, such as 'Rear Balcony', etc. + *

+ * + *

+ * The + * @NotEmpty Bean Validation constraint means that the section description must be at least 1 character. + *

+ */ + @NotEmpty + private String description; + + /** + *

+ * The venue to which this section belongs. The @ManyToOne JPA mapping establishes this relationship. + *

+ * + *

+ * The @NotNull Bean Validation constraint means that the venue must be specified. + *

+ */ + @ManyToOne + @NotNull + private Venue venue; + + /** + * The number of rows that make up the section. + */ + private int numberOfRows; + + /** + * The number of seats in a row. + */ + private int rowCapacity; + + /* Boilerplate getters and setters */ + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getNumberOfRows() { + return numberOfRows; + } + + public void setNumberOfRows(int numberOfRows) { + this.numberOfRows = numberOfRows; + } + + public int getRowCapacity() { + return rowCapacity; + } + + public void setRowCapacity(int rowCapacity) { + this.rowCapacity = rowCapacity; + } + + public int getCapacity() { + return this.rowCapacity * this.numberOfRows; + } + + public Venue getVenue() { + return venue; + } + + public void setVenue(Venue venue) { + this.venue = venue; + } + + /* toString(), equals() and hashCode() for Section, using the natural identity of the object */ + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Section section = (Section) o; + + if (venue != null ? !venue.equals(section.venue) : section.venue != null) + return false; + if (name != null ? !name.equals(section.name) : section.name != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (venue != null ? venue.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return name; + } + +} +------------------------------------------------------------------------------------------------------- + + +=== Shows + + +A show is an event at a venue. It consists of a set of performances of the show. A show also contains the list of ticket prices available. + +Let's start building Show. Here's is our starting point: + +.src/main/java/org/jboss/examples/ticketmonster/model/Show.java +[source,java] +------------------------------------------------------------------------------------------------------- +/** + *

+ * A show is an instance of an event taking place at a particular venue. A show can have multiple performances. + *

+ */ +@Entity +public class Show { + + /* Declaration of fields */ + + /** + * The synthetic id of the object. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + *

+ * The event of which this show is an instance. The @ManyToOne JPA mapping establishes this relationship. + *

+ * + *

+ * The @NotNull Bean Validation constraint means that the event must be specified. + *

+ */ + @ManyToOne + @NotNull + private Event event; + + /** + *

+ * The venue where this show takes place. The @ManyToOne JPA mapping establishes this relationship. + *

+ * + *

+ * The @NotNull Bean Validation constraint means that the venue must be specified. + *

+ */ + @ManyToOne + @NotNull + private Venue venue; + + /* Boilerplate getters and setters */ + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } + + public Venue getVenue() { + return venue; + } + + public void setVenue(Venue venue) { + this.venue = venue; + } + + /* toString(), equals() and hashCode() for Show, using the natural identity of the object */ + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Show show = (Show) o; + + if (event != null ? !event.equals(show.event) : show.event != null) + return false; + if (venue != null ? !venue.equals(show.venue) : show.venue != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = event != null ? event.hashCode() : 0; + result = 31 * result + (venue != null ? venue.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return event + " at " + venue; + } +} +------------------------------------------------------------------------------------------------------- + +If you've been paying attention, you'll notice that there is a problem here. We've identified that the natural identity of this entity is formed of two properties - the _event_ and the _venue_, and we've correctly coded the `equals()` and `hashCode()` methods (or had them generated for us!). However, we haven't told JPA that these two properties, in combination, must be unique. As there are two properties involved, we can no longer use the `@Column` annotation (which operates on a single property/table column), but now must use the class level `@Table` annotation (which operates on the whole entity/table). Change the class definition to read: + +.src/main/java/org/jboss/examples/ticketmonster/model/Show.java +[source,java] +------------------------------------------------------------------------------------------------------- + +... + +@Entity +@Table(uniqueConstraints = @UniqueConstraint(columnNames = { "event_id", "venue_id" })) +public class Show { + + ... +} +------------------------------------------------------------------------------------------------------- + +You'll notice that JPA requires us to use the column names, rather than property names here. The column names used in the `@UniqueConstraint` annotation are those generated by default for properties called `event` and `venue`. + +Additionally, `Show` is a reserved word in certain databases, most notable MySQL. We'll specify a different table name as a result, so that Hibernate will generate correct DDL statements: + +.src/main/java/org/jboss/examples/ticketmonster/model/Show.java +[source,java] +------------------------------------------------------------------------------------------------------- + +... + +@Entity +@Table(name="Appearance", uniqueConstraints = @UniqueConstraint(columnNames = { "event_id", "venue_id" })) +public class Show { + + ... +} +------------------------------------------------------------------------------------------------------- + +Now, let's add the set of performances to the event. Unlike previous relationships we've seen, the relationship between a show and it's performances is bi-directional. We chose to model this as a bi-directional relationship in order to improve the generated database schema (otherwise you end with complicated mapping tables which makes updates to collections hard). Let's add the set of performances: + +.src/main/java/org/jboss/examples/ticketmonster/model/Show.java +[source,java] +------------------------------------------------------------------------------------------------------- + + ... + + /** + *

+ * The set of performances of this show. + *

+ * + *

+ * The @OneToMany JPA mapping establishes this relationship. Collection members + * are fetched eagerly, so that they can be accessed even after the entity has become detached. + * This relationship is bi-directional (a performance knows which show it is part of), and the mappedBy + * attribute establishes this. + *

+ * + */ + @OneToMany(fetch=EAGER, mappedBy = "show", cascade = ALL) + @OrderBy("date") + private Set performances = new HashSet(); + + ... + + public Set getPerformances() { + return performances; + } + + public void setPerformances(Set performances) { + this.performances = performances; + } + + ... + +------------------------------------------------------------------------------------------------------- + +As the relationship is bi-directional, we specify the `mappedBy` attribute on the `@OneToMany` annotation, which informs JPA to create a bi-directional relationship. The value of the attribute is name of property which forms the other side of the relationship - in this case, not unsuprisingly `show`! + +As `Show` is the owner of `Performance` (and without a show, a performance cannot exist), we add the `cascade = ALL` attribute to the `@OneToMany` annotation. As a result, any persistence operation that occurs on a show, will be propagated to it's performances. For example, if a show is removed, any associated performances will be removed as well. + +When retrieving a show, we will also retrieve its associated performances by adding the `fetch = EAGER` attribute to the `@OneToMany` annotation. This is a design decision which required careful consideration. In general, you should favour the default lazy initialization of collections: their content should be accessible on demand. However, in this case we intend to marshal the contents of the collection and pass it across the wire in the JAX-RS layer, after the entity has become detached, and cannot initialize its members on demand. + +We'll also need to add the set of ticket prices available for this show. Once more, this is a bi-directional relationship, owned by the show. It looks just like the set of performances: + +.src/main/java/org/jboss/examples/ticketmonster/model/Show.java +[source,java] +------------------------------------------------------------------------------------------------------- + + ... + + /** + *

+ * The set of ticket prices available for this show. + *

+ * + *

+ * The @OneToMany JPA mapping establishes this relationship. + * This relationship is bi-directional (a ticket price category knows which show it is part of), and the mappedBy + * attribute establishes this. We cascade all persistence operations to the set of performances, so, for example if a show + * is removed, then all of it's ticket price categories are also removed. + *

+ */ + @OneToMany(mappedBy = "show", cascade = CascadeType.ALL, fetch = FetchType.EAGER) + private Set ticketPrices = new HashSet(); + + ... + + public Set getTicketPrices() { + return ticketPrices; + } + + public void setTicketPrices(Set ticketPrices) { + this.ticketPrices = ticketPrices; + } + + ... + +------------------------------------------------------------------------------------------------------- + +Here's the full source for `Show`: + +.src/main/java/org/jboss/examples/ticketmonster/model/Show.java +[source,java] +------------------------------------------------------------------------------------------------------- + +/** + *

+ * A show is an instance of an event taking place at a particular venue. A show can have multiple performances. + *

+ * + *

+ * A show contains a set of performances, and a set of ticket prices for each section of the venue for this show. + *

+ * + *

+ * The event and venue form the natural id of this entity, and therefore must be unique. JPA requires us to use the class level + * @Table constraint. + *

+ * + */ +/* + * We suppress the warning about not specifying a serialVersionUID, as we are still developing this app, and want the JVM to + * generate the serialVersionUID for us. When we put this app into production, we'll generate and embed the serialVersionUID + */ +@SuppressWarnings("serial") +@Entity +@Table(name="Appearance", uniqueConstraints = @UniqueConstraint(columnNames = { "event_id", "venue_id" })) +public class Show implements Serializable { + + /* Declaration of fields */ + + /** + * The synthetic id of the object. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + *

+ * The event of which this show is an instance. The @ManyToOne JPA mapping establishes this relationship. + *

+ * + *

+ * The @NotNull Bean Validation constraint means that the event must be specified. + *

+ */ + @ManyToOne + @NotNull + private Event event; + + /** + *

+ * The event of which this show is an instance. The @ManyToOne JPA mapping establishes this relationship. + *

+ * + *

+ * The @NotNull Bean Validation constraint means that the event must be specified. + *

+ */ + @ManyToOne + @NotNull + private Venue venue; + + /** + *

+ * The set of performances of this show. + *

+ * + *

+ * The @OneToMany JPA mapping establishes this relationship. TODO Explain EAGER fetch. + * This relationship is bi-directional (a performance knows which show it is part of), and the mappedBy + * attribute establishes this. We cascade all persistence operations to the set of performances, so, for example if a show + * is removed, then all of it's performances will also be removed. + *

+ * + *

+ * Normally a collection is loaded from the database in the order of the rows, but here we want to make sure that + * performances are ordered by date - we let the RDBMS do the heavy lifting. The + * @OrderBy annotation instructs JPA to do this. + *

+ */ + @OneToMany(fetch = EAGER, mappedBy = "show", cascade = ALL) + @OrderBy("date") + private Set performances = new HashSet(); + + /** + *

+ * The set of ticket prices available for this show. + *

+ * + *

+ * The @OneToMany JPA mapping establishes this relationship. + * This relationship is bi-directional (a ticket price category knows which show it is part of), and the mappedBy + * attribute establishes this. We cascade all persistence operations to the set of performances, so, for example if a show + * is removed, then all of it's ticket price categories are also removed. + *

+ */ + @OneToMany(mappedBy = "show", cascade = ALL, fetch = EAGER) + private Set ticketPrices = new HashSet(); + + /* Boilerplate getters and setters */ + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } + + public Venue getVenue() { + return venue; + } + + public void setVenue(Venue venue) { + this.venue = venue; + } + + public Set getPerformances() { + return performances; + } + + public void setPerformances(Set performances) { + this.performances = performances; + } + + public Set getTicketPrices() { + return ticketPrices; + } + + public void setTicketPrices(Set ticketPrices) { + this.ticketPrices = ticketPrices; + } + + /* toString(), equals() and hashCode() for Show, using the natural identity of the object */ + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Show show = (Show) o; + + if (event != null ? !event.equals(show.event) : show.event != null) + return false; + if (venue != null ? !venue.equals(show.venue) : show.venue != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = event != null ? event.hashCode() : 0; + result = 31 * result + (venue != null ? venue.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return event + " at " + venue; + } +} + +------------------------------------------------------------------------------------------------------- + + +=== TicketPrices + + +The `Show` entity references two classes - `TicketPrice` and `Performance`, that are not yet created. Let's first create the `TicketPrice` class which represents the price for a ticket in a particular `Section` at a `Show` for a specific `TicketCategory`. It does not introduce any new concepts, so go ahead and copy the source from the below listing: + + +.src/main/java/org/jboss/examples/ticketmonster/model/TicketPrice.java +[source,java] +------------------------------------------------------------------------------------------------------- +/** + *

+ * Contains price categories - each category represents the price for a ticket in a particular section at a particular venue for + * a particular event, for a particular ticket category. + *

+ * + *

+ * The section, show and ticket category form the natural id of this entity, and therefore must be unique. JPA requires us to use the class level + * @Table constraint + *

+ * + */ +/* + * We suppress the warning about not specifying a serialVersionUID, as we are still developing this app, and want the JVM to + * generate the serialVersionUID for us. When we put this app into production, we'll generate and embed the serialVersionUID + */ +@SuppressWarnings("serial") +@Entity +@Table(uniqueConstraints = @UniqueConstraint(columnNames = { "section_id", "show_id", "ticketcategory_id" })) +public class TicketPrice implements Serializable { + + /* Declaration of fields */ + + /** + * The synthetic id of the object. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + *

+ * The show to which this ticket price category belongs. The @ManyToOne JPA mapping establishes this relationship. + *

+ * + *

+ * The @NotNull Bean Validation constraint means that the show must be specified. + *

+ */ + @ManyToOne + @NotNull + private Show show; + + /** + *

+ * The section to which this ticket price category belongs. The @ManyToOne JPA mapping establishes this relationship. + *

+ * + *

+ * The @NotNull Bean Validation constraint means that the section must be specified. + *

+ */ + @ManyToOne + @NotNull + private Section section; + + /** + *

+ * The ticket category to which this ticket price category belongs. The @ManyToOne JPA mapping establishes this relationship. + *

+ * + *

+ * The @NotNull Bean Validation constraint means that the ticket category must be specified. + *

+ */ + @ManyToOne + @NotNull + private TicketCategory ticketCategory; + + /** + * The price for this category of ticket. + */ + private float price; + + /* Boilerplate getters and setters */ + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Show getShow() { + return show; + } + + public void setShow(Show show) { + this.show = show; + } + + public Section getSection() { + return section; + } + + public void setSection(Section section) { + this.section = section; + } + + public TicketCategory getTicketCategory() { + return ticketCategory; + } + + public void setTicketCategory(TicketCategory ticketCategory) { + this.ticketCategory = ticketCategory; + } + + public float getPrice() { + return price; + } + + public void setPrice(float price) { + this.price = price; + } + + /* equals() and hashCode() for TicketPrice, using the natural identity of the object */ + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + TicketPrice that = (TicketPrice) o; + + if (section != null ? !section.equals(that.section) : that.section != null) + return false; + if (show != null ? !show.equals(that.show) : that.show != null) + return false; + if (ticketCategory != null ? !ticketCategory.equals(that.ticketCategory) : that.ticketCategory != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = show != null ? show.hashCode() : 0; + result = 31 * result + (section != null ? section.hashCode() : 0); + result = 31 * result + (ticketCategory != null ? ticketCategory.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "$ " + price + " for " + ticketCategory + " in " + section; + } +} +------------------------------------------------------------------------------------------------------- + + +=== Performances + + +Finally, let's create the `Performance` class, which represents an instance of a `Show`. Performance is pretty straightforward. It contains the date and time of the performance, and the show of which it is a performance. Together, the show, and the date and time, make up the natural identity of the performance. Here's the source for `Performance`: + +.src/main/java/org/jboss/examples/ticketmonster/model/Performance.java +[source,java] +------------------------------------------------------------------------------------------------------- + +/** + *

+ * A performance represents a single instance of a show. + *

+ * + *

+ * The show and date form the natural id of this entity, and therefore must be unique. JPA requires us to use the class level + * @Table constraint. + *

+ * + */ +@SuppressWarnings("serial") +@Entity +@Table(uniqueConstraints = @UniqueConstraint(columnNames = { "date", "show_id" })) +public class Performance implements Serializable { + + /* Declaration of fields */ + + /** + * The synthetic id of the object. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + *

+ * The date and start time of the performance. + *

+ * + *

+ * A Java {@link Date} object represents both a date and a time, whilst an RDBMS splits out Date, Time and Timestamp. + * Therefore we instruct JPA to store this date as a timestamp using the @Temporal(TIMESTAMP) annotation. + *

+ * + *

+ * The date and time of the performance is required, and the Bean Validation constraint @NotNull enforces this. + *

+ */ + @Temporal(TIMESTAMP) + @NotNull + private Date date; + + /** + *

+ * The show of which this is a performance. The @ManyToOne JPA mapping establishes this relationship. + *

+ * + *

+ * The show of which this is a performance is required, and the Bean Validation constraint @NotNull enforces + * this. + *

+ */ + @ManyToOne + @NotNull + private Show show; + + /* Boilerplate getters and setters */ + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public void setShow(Show show) { + this.show = show; + } + + public Show getShow() { + return show; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + /* equals() and hashCode() for Performance, using the natural identity of the object */ + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Performance that = (Performance) o; + + if (date != null ? !date.equals(that.date) : that.date != null) + return false; + if (show != null ? !show.equals(that.show) : that.show != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = date != null ? date.hashCode() : 0; + result = 31 * result + (show != null ? show.hashCode() : 0); + return result; + } +} +------------------------------------------------------------------------------------------------------- + +Of interest here is the storage of the date and time. + +A Java `Date` represents "a specific instance in time, with millisecond precision" and is the recommended construct for representing date and time in the JDK. A RDBMS's _DATE_ type typically has day precision only, and uses the _DATETIME_ or _TIMESTAMP_ types to represent an instance in time, and often only to second precision. + +As the mapping between Java date and time, and database date and time isn't straightforward, JPA requires us to use the `@Temporal` annotation on any property of type `Date`, and to specify whether the `Date` should be stored as a date, a time or a timestamp (date and time). + + +=== Booking, Ticket & Seat + + +There aren't many new concepts to explore in `Booking`, `Ticket` and `Seat`, so if you are following along with the tutorial, you should copy in the `Booking`, `Ticket` and `Seat` classes. + +Once the user has selected an event, identified the venue, and selected a performance, they have the opportunity to request a number of seats in a given section, and select the category of tickets required. Once they've chosen their seats, and entered their email address, a `Booking` is created. + +A booking consists of the date the booking was created, an email address (as TicketMonster doesn't yet have fully fledged user management), a set of tickets and the associated performance. The set of tickets shows us how to create a uni-directional one-to-many relationship: + +.src/main/java/org/jboss/examples/ticketmonster/model/Booking.java +[source,java] +------------------------------------------------------------------------------------------------------- + + ... + + /** + *

+ * The set of tickets contained within the booking. The @OneToMany JPA mapping establishes this relationship. + *

+ * + *

+ * The set of tickets is eagerly loaded because FIXME . All operations are cascaded to each ticket, so for example if a + * booking is removed, then all associated tickets will be removed. + *

+ * + *

+ * This relationship is uni-directional, so we need to inform JPA to create a foreign key mapping. The foreign key mapping + * is not visible in the {@link Ticket} entity despite being present in the database. + *

+ * + */ + @OneToMany(fetch = EAGER, cascade = ALL) + @JoinColumn + @NotEmpty + @Valid + private Set tickets = new HashSet(); + + ... +------------------------------------------------------------------------------------------------------- + +We add the `@JoinColumn` annotation, which sets up a foreign key in `Ticket`, but doesn't expose the booking on Ticket. This prevents the use of messy mapping tables, whilst preserving the integrity of the entity model. + +A ticket embeds the seat allocated, and contains a reference to the category under which it was sold. It also contains the price at which it was sold. + + +=== SectionAllocation and SeatAllocationException + + +Finally, we'd like to track the seats to be allocated from a section during the course of booking tickets. We'll use the `SectionAllocation` entity to track the allocations in every section for every performance. You can copy in the `SectionAllocation` class from the project sources. + +The notable member in this class is the two-dimensional array, named `allocated`. It tracks the state of the section - the first dimension represents the rows in the section, and the second represents the state of every seat in the row. A typical RDBMS would have to store such a structure as a LOB (Large Object) or a BLOB (Binary Large Object), since a n-dimensional array does not map easily to a native data type supported by the database. Thus, we denote the field as a `@Lob` using the JPA annotation: + + +.src/main/java/org/jboss/examples/ticketmonster/model/SectionAllocation.java +[source,java] +------------------------------------------------------------------------------------------------------- + ... + + @Lob + private long[][] allocated; + + ... +------------------------------------------------------------------------------------------------------- + +The rest of the class contains business logic to update the state of the `allocated` field. These methods will come in handy later, when we write the business services. + +Remember to also copy the `SeatAllocationException` class, that is referenced in this class, from the project sources. This class represents an Application exception that will be recognized by the EJB container as one that should force a transaction rollback. When this exception is thrown by the business logic in the `SectionAllocation` entity, and propagated to the EJB container, it will implcitly cause the current transaction to roll back. It is to be noted that, this exception class is not a checked exception (it extends `RuntimeException`), and thus the compiler does not complain when it is uncaught in the business services that will consume the methods in the `SectionAllocation` entity. + + +== Connecting to the database + + +In this example, we are using the in-memory H2 database, which is very easy to set up on JBoss AS. JBoss AS allows you deploy a datasource inside your application's `WEB-INF` directory. You can locate the source in `src/main/webapp/WEB-INF/ticket-monster-ds.xml` (which should have been created in the previous chapter): + +.src/main/webapp/WEB-INF/ticket-monster-ds.xml +[source,xml] +------------------------------------------------------------------------------------------------------- + + + + + jdbc:h2:mem:ticket-monster;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1 + + h2 + + sa + sa + + + +------------------------------------------------------------------------------------------------------- + +The datasource configures an H2 in-memory database, called _ticket-monster_, and registers a datasource in JNDI at the address: + + java:jboss/datasources/ticket-monsterDS + +Now we need to configure JPA to use the datasource. This is done in `src/main/resources/META-INF/persistence.xml`: + +.src/main/resources/persistence.xml +[source,xml] +------------------------------------------------------------------------------------------------------- + + + + + java:jboss/datasources/ticket-monsterDS + + + + + + + +------------------------------------------------------------------------------------------------------- + +As our application has only one datasource, and hence one persistence unit, the name given to the persistence unit doesn't really matter. We call ours `primary`, but you can change this as you like. We tell JPA about the datasource bound in JNDI. + +Hibernate includes the ability to generate tables from entities, which we have configured here. We don't recommend using this outside of development. Updates to databases in production should be done in a staged manner by a database administrator. + +== Populating test data + + +Whilst we develop our application, it's useful to be able to populate the database with test data. Luckily, Hibernate makes this easy. Just add a file called `import.sql` onto the classpath of your application (we keep it in `src/main/resources/import.sql`). In it, we just write standard sql statements suitable for the database we are using. To do this, you need to know the generated column and table names for your entities. The best way to work these out is to look at the h2console. + +The h2console is included in the JBoss AS quickstarts, along with instructions on how to use it. For more information, see http://www.jboss.org/quickstarts/eap/h2-console/ + +[TIP] +.Where do I look for my data? +===================================================================================== +The database URL is `jdbc:h2:mem:ticket-monster`. After +you have downloaded `h2console.war` and deployed it on the server, make sure that the +application is running on the server and use this value to connect to your running application's +database. +[[h2console_settings]] +.h2console settings +image::gfx/h2console_settings.png[] +===================================================================================== + +You should copy over the `import.sql` file from the project sources, to populate the database with the same data, as the one used in the OpenShift-hosted TicketMonster application. The contents of this file already account for the generated table and column names. + +== Conclusion + + +You now have a working data model for your TicketMonster application, our next tutorial will show you how to create the business services layer or something like that - it seems to end abruptly. diff --git a/tutorial/HybridUI.asciidoc b/tutorial/HybridUI.asciidoc new file mode 100644 index 000000000..fa52b1396 --- /dev/null +++ b/tutorial/HybridUI.asciidoc @@ -0,0 +1,462 @@ += Creating hybrid mobile versions of the application with Apache Cordova +:Author: Marius Bogoevici +:thumbnail: http://static.jboss.org/ffe/1/www/origin/ticket-monster-splash-2.png + +== What will you learn here? + + +You finished creating the front-end for your application, and it has mobile support. You would now like to provide native client applications that your users can download from an application store. After reading this tutorial, you will understand how to reuse the existing HTML5 code for create native mobile clients for each target platform with Apache Cordova. + +You will learn how to: + +* make changes to an existing web application to allow it to be deployed as a hybrid mobile application +* create a native application for Android and iOS with Apache Cordova + +== What are hybrid mobile applications? + + +Hybrid mobile applications are developed in HTML5 - unlike native applications that are compiled to platform-specific binaries. The client code - which consists exclusively of HTML, CSS, and JavaScript - is packaged and installed on the client device just as any native application, and executes in a browser process created by a surrounding native shell. + +Besides wrapping the browser process, the native shell also allows access to native device capabilities, such as the accelerometer, GPS, contact list, etc., made available to the application through JavaScript libraries. + +In this example, we use Apache Cordova to implement a hybrid application using the existing HTML5 mobile front-end for TicketMonster, interacting with the RESTful services of a TicketMonster deployment running on JBoss A7 or JBoss EAP. + +[[ticket_monster_hybrid]] +.Architecture of hybrid TicketMonster +image::gfx/ticket_monster_hybrid.png[] + +== Tweak your application for remote access + + +Before we make the application hybrid, we need to make some changes in the way in which it accesses remote services. Note that the changes have already been implemented in the user front end, here we show you the code that we needed to modify. + +In the web version of the application the client code is deployed together with the server-side code, so the models and collections (and generally any piece of code that will perform REST service invocations) can use URLs relative to the root of the application: all resources are serviced from the same server, so the browser will do the correct invocation. This also respects the same origin policy enforced by default by browsers, to prevent cross-site scripting attacks. + +If the client code is deployed separately from the services, the REST invocations must use absolute URLs (we will cover the impact on the same-origin policy later). Furthermore, since we want to be able to deploy the application to different hosts without rebuilding the source, it must be configurable. + +You already caught a glimpse of this in the user front end chapter, where we defined the `configuration` module for the mobile version of the application. + +.src/main/webapp/resources/js/configurations/mobile.js +[source,javascript] +------------------------------------------------------------------------------------------------------ +... +define("configuration", function() { + if (window.TicketMonster != undefined && TicketMonster.config != undefined) { + return { + baseUrl: TicketMonster.config.baseRESTUrl + }; + } else { + return { + baseUrl: "" + }; + } +}); +... +------------------------------------------------------------------------------------------------------ + +This module has a `baseURL` property that is either set to an empty string for relative URLs or to a prefix, such as a domain name, depending on whether a global variable named `TicketMonster` has already been defined, and it has a `baseRESTUrl` +property. + +All our code that performs REST services invocations depends on this module, thus the base REST URL can be configured in a single place and injected throughout the code, as in the following code example: + +.src/main/webapp/resources/js/app/models/event.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * Module for the Event model + */ +define([ + 'configuration', + 'backbone' +], function (config) { + /** + * The Event model class definition + * Used for CRUD operations against individual events + */ + var Event = Backbone.Model.extend({ + urlRoot: config.baseUrl + 'rest/events' // the URL for performing CRUD operations + }); + // export the Event class + return Event; +}); +------------------------------------------------------------------------------------------------------- + +The prefix is used in a similar fashion by all the other modules that perform REST service invocations. You don't need to do anything right now, because the code we created in the user front end tutorial was written like this originally. Be warned, if you have a mobile web application that uses any relative URLs, you will need to refactor them to include some form of URL configuration. + +== Install Hybrid Mobile Tools and CordovaSim + +Hybrid Mobile Tools and CordovaSim are not installed as part of JBoss Developer Studio yet. They can be installed from JBoss Central as shown below: + +1. To install these plug-ins, drag the following link into JBoss Central: https://devstudio.jboss.com/central/install?connectors=org.jboss.tools.aerogear.hybrid. Alternatively, in JBoss Central select the *Software/Update* tab. In the *Find* field, type *JBoss Hybrid Mobile Tools* or scroll through the list to locate *JBoss Hybrid Mobile Tools + CordovaSim*. Select the corresponding check box and click *Install*. ++ +.Start the Hybrid Mobile Tools and CordovaSim Installation Process with the Link +image::gfx/start-hybrid-mobile-tools-cordovasim-installation-with-link.png[] ++ +.Find Hybrid Mobile Tools and CordovaSim in JBoss Central Software/Update Tab +image::gfx/find-hybrid-mobile-tools-cordovasim.png[] + +2. In the *Install* wizard, ensure the check boxes are selected for the software you want to install and click *Next*. It is recommended that you install all of the selected components. + +3. Review the details of the items listed for install and click Next. After reading and agreeing to the license(s), click *I accept the terms of the license agreement(s)* and click *Finish*. The *Installing Software* window opens and reports the progress of the installation. + +4. During the installation process you may receive warnings about installing unsigned content. If this is the case, check the details of the content and if satisfied click *OK* to continue with the installation. ++ +.Warning Prompt for Installing Unsigned Content +image::gfx/warning-prompt-unsigned-content.png[] + +5. Once the installation is complete, you will be prompted to restart the IDE. Click *Yes* to restart now and *No* if you need to save any unsaved changes to open projects. Note that changes do not take effect until the IDE is restarted. + +Once installed, you must inform Hybrid Mobile Tools of the Android SDK location before you can use Hybrid Mobile Tools actions involving Android. + +To set the Android SDK location, click *Window* → *Preferences* and select *Hybrid Mobile*. In the *Android SDK Directory* field, type the path of the installed SDK or click *Browse* to navigate to the location. Click *Apply* and click *OK* to close the *Preferences* window. + +.Hybrid Mobile Pane of Preferences Window +image::gfx/hybrid-mobile-pane-preferences-window.png[] + +== Creating a Hybrid Mobile project + +1. To create a new Hybrid Mobile Project, click _File → New → Other_ and select "Hybrid Mobile (Cordova) Application Project". ++ +.Starting a new Hybrid Mobile Application project +image::gfx/start-new-hybrid-mobile-application-project.png[] +2. Enter the project information: application name, project name, package. ++ + Project Name:: + TicketMonster-Cordova + Name:: + TicketMonster-Cordova + ID:: + org.jboss.examples.ticketmonster.cordova ++ +.Creating a new Hybrid Mobile Application project +image::gfx/create-new-hybrid-mobile-application-project.png[] ++ +Click `Next` to choose the Hybrid Mobile engine for the project. If you have never setup a Hybrid Mobile engine in JBoss Developer Studio before, you will be prompted to download or search for engines to use. We'll click on the `Download` button to perform the former. ++ +.Setting up a Hybrid Mobile engine for the first time +image::gfx/setup_hybrid_mobile_engine_from_scratch.png[] ++ +You'll be prompted with a dialog where you can download all available hybrid mobile engines. ++ +.Choose the Hybrid Mobile engine to download +image::gfx/setup_hybrid_mobile_engine_version.png[] ++ +We'll choose Android and iOS variants of version 3.4.0. ++ +.Select Android and iOS for 3.4.0 +image::gfx/setup_hybrid_mobile_engine_340.png[] ++ +Now that we have downloaded and setup a hybrid mobile engine, let's use it in our project. Select the newly configured engine and click `Next`. ++ +.Creating a new Hybrid Mobile Application project +image::gfx/select_hybrid_mobile_engine_for_project.png[] ++ +We will now be provided the opportunity to add Cordova plug-ins to our project. ++ +.Adding Cordova plugins when a new Hybrid Mobile Application project +image::gfx/cordova_choose_to_add_plugins.png[] ++ +We will be using the Status Bar plugin from Cordova, to ensure that the status bar on iOS 7 does not overlap the UI. The Device plugin will be used to obtain device information for use in device detection. We'll also use the Notification plugin to display alerts and notifications to the end-user using the native mobile UI. We'll proceed to add the required Cordova plugins to the project. ++ +.Add Cordova Device plugin +image::gfx/cordova_add_device_plugin.png[] ++ +.Add Cordova Notification plugin +image::gfx/cordova_add_notifications_plugin.png[] ++ +.Add Cordova StatusBar plugin +image::gfx/cordova_add_statusbar_plugin.png[] ++ +Let's proceed to add these, by searching for them and selecting them. Click `Next` once you have finished selecting the necessary plug-ins. We will now confirm the plugins to be added to the project. Click `Finish` to create the new Hybrid Mobile application project. ++ +.Confirm plugins to add +image::gfx/cordova_confirm_plugin_versions.png[] ++ +Once you have finished creating the project, navigate to the `www` directory, that will contain the HTML5 code of the application. Since we are reusing the TicketMonster code you can simply replace the `www` directory with a symbolic link to the `webapp` directory of TicketMonster; the `config.xml` file and `res` directory would need to be copied over to the `webapp` directory of TicketMonster. Alternatively, you can copy the code of TicketMonster and make all necessary changes there (however, in that case you will have to maintain the code of the application in both places). + +---- +$ cp config.xml $TICKET_MONSTER_HOME/demo/src/main/webapp +$ cp res $TICKET_MONSTER_HOME/demo/src/main/webapp +$ cd .. +$ rm -rf www +---- + +Once the necessary files have been copied and the original `www` folder deleted, you can re-create the `www` directory as an Eclipse linked folder in the Hybrid Mobile project. + +First select the project, right-click on it, and select the `New` -> `Folder` option in the context menu. + +.Create a new Folder +image::gfx/create_new_folder.png[] + +Specify the name of the folder as `www`, but do not click `Finish` yet - we need to create a linked folder. + +.Specify www as the folder name +image::gfx/create_www_folder.png[] + +In the same dialog box, click the `Advanced` button, to create a new `Linked Folder` (Link to alternate location). Enter the following value: `PROJECT_LOC/../ticket-monster/src/main/webapp/` in the textbox. Click _Finish_ to create the linked folder. This will link the folder to the TicketMonster project, assuming the Hybrid Mobile Project and the Java EE project reside in the same directory. + +.Establish link between www and the webapp directory +image::gfx/setup_linked_folder_to_webapp.png[] + +Replace `ticket-monster` with the name of the Java EE project, if you specified a different name when you originally created the Java EE project. If the TicketMonster project sources are in a different location, you should correct the link, and specify the correct location instead. + +[NOTE] +================================================== +We use Linked Folders instead of file-system dependent symbolic links, as the latter are not portable across operating systems. + +The `PROJECT_LOC` variable that we use in the link, represents the location of the current project. `PROJECT_LOC/../ticket-monster/` represents the `ticket-monster` (aka the Java EE project we originally created in the Introductory chapter) in the parent directory of the Hybrid Mobile project (i.e. as a sibling project, and if you've used the default locations, both of these projects would be in the same top-level workspace directory). The rest of the link's value translates to the `webapp` directory under the Java EE project. +================================================== + +[WARNING] +================================================== +Once you switch to using an Eclipse Linked Folder, you may not be able to add or remove Cordova plugins from the Hybrid Mobile application project. +This is a known issue, that you can track in the https://issues.jboss.org/browse/JBIDE-18711[JBoss JIRA]. + +If you wish to do work around this, simply drop the linked folder, recreate a simple folder with the `config.xml` file, and proceed to add/remove the plugins. +================================================== + +.The result of linking www to the webapp directory +image::gfx/link-www-directory-to-webapp.png[] + +The Hybrid Mobile tooling requires that the cordova.js file be loaded in the application's start page. +Since we do not want to load this file in the existing `index.html` file, we shall create a new start page to be used only by the Cordova app. + +.src/main/webapp/mobileapp.html +[source,html] +------------------------------------------------------------------------------------------------------- + + + + Ticket Monster + + + + + + + + + +------------------------------------------------------------------------------------------------------- + +Let's now modify the Hybrid Mobile project configuration to use this page as the application start page. +Additionally, we will add our REST service URL to the domain whitelist in the config.xml file (you can use `"*"` too, for simplicity, during development) : + +.src/main/webapp/config.xml +[source,xml] +------------------------------------------------------------------------------------------------------- + + + + ... + + + + + + + + ... + + +------------------------------------------------------------------------------------------------------- + +Next, we need to load the library in the application. We will create a separate module, that will load the rest of the mobile application, as well as the Apache Cordova JavaScript library for Android. We also need to configure a base URL for the application. For this example, we will use the URL of the cloud deployment of TicketMonster. + +.src/main/webapp/resources/js/configurations/hybrid.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +// override configuration for RESTful services +var TicketMonster = { + config:{ + baseRESTUrl:"http://ticketmonster-jdf.rhcloud.com/" + } +}; + +require(['../../../cordova'], function() { + + var bootstrap = { + initialize: function() { + document.addEventListener('deviceready', this.onDeviceReady, false); + }, + onDeviceReady: function() { + // Detect if iOS 7 or higher and disable overlaying the status bar + if(window.device && window.device.platform.toLowerCase() == "ios" && + parseFloat(window.device.version) >= 7.0) { + StatusBar.overlaysWebView(false); + StatusBar.styleDefault(); + StatusBar.backgroundColorByHexString("#e9e9e9"); + } + // Load the mobile module + require (["mobile"]); + } + }; + + bootstrap.initialize(); +}); +------------------------------------------------------------------------------------------------------- + +[NOTE] +============================================================== +We'll use the OpenShift hosted version of the TicketMonster application because it is easier to access in all environments - the smartphone simulators and emulators can also access it with relatively little or no configuration. On the other hand, accessing the locally running JBoss EAP instance may require some complicated network configuration, especially if the instance needs to be opened up to the internet for access from smartphones through a mobile internet link. +============================================================== + +The above snippet of code contains a device-specific check for iOS 7. + +Finally, we'll configure the loader module launched from `mobileapp.html` to use the above defined `hybrid` module: + +.src/main/webapp/resources/js/configurations/loader.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +//detect the appropriate module to load +define(function () { + + /* + A simple check on the client. For touch devices or small-resolution screens) + show the mobile client. By enabling the mobile client on a small-resolution screen + we allow for testing outside a mobile device (like for example the Mobile Browser + simulator in JBoss Tools and JBoss Developer Studio). + */ + + var environment; + + if (document.URL.indexOf("mobileapp.html") > -1) { + environment = "hybrid"; + } + else if (Modernizr.touch || Modernizr.mq("only all and (max-width: 768px)")) { + environment = "mobile"; + } else { + environment = "desktop"; + } + + require([environment]); +}); +------------------------------------------------------------------------------------------------------- + +In the above code snippet, we detect if the URL of the page contains `mobileapp.html` or not, and then proceed to activate the `hybrid` module if so. Since Apache Cordova is configured to use `mobileapp.html` as the application start page, the desired objective is achieved. This way, we avoid loading the `mobile` or `desktop` modules that do not have any logic in them to detect the `deviceready` event of Cordova. + +The final step will involve adjusting `src/main/webapp/resources/js/configurations/loader.js` to load this module when running on Android, using the query string we have already configured in the project. We'll also tweak `src/main/webapp/resources/js/app/utilities.js` to use the Notification plugin to display alerts in the context of a Hybrid Mobile app. + +.src/main/webapp/resources/js/configurations/loader.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +//detect the appropriate module to load +define(function () { + + /* + A simple check on the client. For touch devices or small-resolution screens) + show the mobile client. By enabling the mobile client on a small-resolution screen + we allow for testing outside a mobile device (like for example the Mobile Browser + simulator in JBoss Tools and JBoss Developer Studio). + */ + + var environment; + + if (document.URL.indexOf("mobileapp.html") > -1) { + environment = "hybrid"; + } + else if (Modernizr.touch || Modernizr.mq("only all and (max-width: 768px)")) { + environment = "mobile"; + } else { + environment = "desktop"; + } + + require([environment]); +}); +------------------------------------------------------------------------------------------------------- + +We'll now examine the `displayAlert` function in the utilities object. It is set to use the Notification plugin when available: + +.src/main/webapp/resources/js/app/utilities.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +... + // utility functions for rendering templates + var utilities = { + ... + applyTemplate:function (target, template, data) { + return target.empty().append(this.renderTemplate(template, data)); + }, + displayAlert: function(msg) { + if(navigator.notification) { + navigator.notification.alert(msg); + } else { + alert(msg); + } + } + }; +... +------------------------------------------------------------------------------------------------------- + +The function automatically works in non-mobile environments due to the absence of the `navigator.notification` object in such environments. + +== Run the hybrid mobile application + +You are now ready to run the application. The hybrid mobile application can be run on devices and simulators using the Hybrid Mobile Tools. + +=== Run on an Android device or emulator + +[NOTE] +.What do you need for Android? +===================================================================================== +For running on an Android device or emulator, you need to install the Android +Developer Tools, which require an Eclipse instance (JBoss Developer Studio could be used), and can run on +Windows (XP, Vista, 7), Mac OS X (10.5.8 or later), Linux (with GNU C Library - glibc 2.7 or +later, 64-bit distributions having installed the libraries for running 32-bit applications). + +You must have Android API 17 or later installed on your system to use the *Run on Android Emulator* action. +===================================================================================== + +To run the project on a device, in the *Project Explorer* view, right-click the project name and click *Run As* → *Run on Android Device*. This option calls the external Android SDK to package the workspace project and run it on an Android device if one is attached. Note that the Android SDK must be installed and the IDE correctly configured to use the Android SDK for this option to execute successfully. + +To run the project on an emulator, in the *Project Explorer* view, right-click the project name and click *Run As* → *Run on Android Emulator*. + +.Running the application on an Android emulator +image::gfx/run-on-android-emulator.png[] + +This requires that you create an Android AVD to run the application in a virtual device. + +Once deployed, the application is now available for interaction in the emulator. + +.The app running on an Android AVD +image::gfx/android-emulator.png[] + + +=== Run on an iOS Simulator + +[NOTE] +.What do you need for iOS? +===================================================================================== +This option is only displayed when using OS X operating systems, for which the iOS Simulator is available. +You must install *Xcode 4.5+* which includes the *iOS 6 SDK*. You must also install a Simulator for iOS 5.x or higher, to run the project on a simulator. +Depending on various Cordova plugins that you may use, you may need higher versions of simulators to run your applications. +===================================================================================== + +In the Project Explorer view, right-click the project name and click *Run As* → *Run on iOS Emulator*. + +.Running the application on an iOS simulator +image::gfx/run-on-ios-simulator.png[] + +This option calls the external iOS SDK to package the workspace project into an XCode project and run it on the iOS Simulator. + +.The app running on an iOS Simulator +image::gfx/ios-simulator.png[] + +=== Run on CordovaSim + +CordovaSim allows you to run your hybrid mobile applications in your local workspace. You can develop the application without requiring a deployment to a real device or even to emulators and simulators to realize your application's behavior. +There are some limitations on what you can achieve with CordovaSim, for instance, some Cordova plugins may not work with CordovaSim. But for the most part, you get to experience a faster development cycle. + +In the Project Explorer view, right-click the project name and click *Run As* → *Run with CordovaSim*. This opens the application in CordovaSim, which is composed of a BrowserSim simulated device and a device input panel. + +.The app running on CordovaSim +image::gfx/cordovasim.png[] + +== Conclusion + +This concludes our tutorial for building a hybrid application with Apache Cordova. You have seen how we have turned a working HTML5 web application into one that can run natively on Android and iOS. diff --git a/tutorial/Introduction.asciidoc b/tutorial/Introduction.asciidoc new file mode 100644 index 000000000..836920212 --- /dev/null +++ b/tutorial/Introduction.asciidoc @@ -0,0 +1,1131 @@ += Introduction & Getting Started +:Author: Burr Sutter +:thumbnail: http://static.jboss.org/ffe/1/www/origin/ticket-monster-splash-2.png + +== Purpose and Target Audience + +The target audience for this tutorial are those individuals who do not yet have a great deal of experience with: + +* Eclipse + JBoss Tools (JBoss Developer Studio) +* JBoss Enterprise Application Platform 6.3 +* Java EE 6 features like JAX-RS +* HTML5 & jQuery for building an mobile web front-end. + +This tutorial sets the stage for the creation of TicketMonster - our sample application that illustrates how to bring together the best features of *Java EE 6 + HTML5 + JBoss* to create a rich, mobile-optimized and dynamic application. + +TicketMonster is developed as an open source application, and you can find it link:https://github.com/jboss-jdf/ticket-monster[at github]. + +If you prefer to watch instead of read, a large portion of this content is also covered in link:http://docs.jboss.org/tools/movies/[video form]. + +In this tutorial, we will cover the following topics: + +- Working with JBoss Developer Studio (Eclipse + JBoss Tools) +- Creating of a Java EE 6 project via a Maven archetype +- Leveraging m2e and m2e-wtp +- Using Forge to create a JPA entity +- Using Hibernate Tools +- Database Schema Generation +- Deployment to a local JBoss Server +- Adding a JAX-RS endpoint +- Adding a jQuery Mobile client +- Using the Mobile BrowserSim + +[[jbds5_mobile_browsersim_image]] +.JBoss Developer Studio 8 with Mobile BrowserSim +image::gfx/introduction/jbds8_mobile_browsersim.png[] + +== Installation + + +The first order of business is to get your development environment setup and JBoss Developer Studio v8 installed. JBoss Developer Studio is Eclipse Luna (e4.4) for Java EE Developers plus select JBoss Tools and is available for free. Visit http://www.jboss.org/products/devstudio/download to download it. You may also choose to install JBoss Tools 4.2 into your existing Eclipse for Java EE Developers installation. This document uses screenshots depicting JBoss Developer Studio. + +You must have a Java Development Kit (JDK) installed. Java 7 JDK is recommended - whilst a JVM runtime will work for most use cases, for a developer environment it is normally best to have the full JDK. + +[TIP] +============================================================== +If you prefer to see JBoss Developer Studio being installed, +then check out link:http://vimeo.com/39606090[this video]. + +To see JBoss Tools being installed into Eclipse, see +link:http://vimeo.com/39743315[this video]. +============================================================== + +The JBoss Developer Studio installer has a (very long!) name such as `jboss-devstudio-8.0.0.GA-v20141020-1042-B317-installer-standalone.jar` +where the latter portion of the file name relates to build date and version information and the text near the front related to the target operating system. The "universal" installer is for any operating system. To launch the installer you may simply be able to double-click on the .jar file name or you may need to issue the following from the operating system command line: + + java -jar jboss-devstudio-8.0.0.GA-v20141020-1042-B317-installer-standalone.jar + +We recommend using the "universal" installer as it handles Windows, Mac OS X and Linux - 32-bit and 64-bit versions. + +[NOTE] +=============================================================== +Even if you are installing on a 64-bit OS, you may still wish +to use the 32-bit JVM for the JBoss Developer Studio (or +Eclipse + JBoss Tools). Only the 32-bit version provides a +supported version of the Visual Page Editor - a split-pane +editor that gives you a glimpse of what your HTML/XHTML (JSF, + JSP, etc) will look like. +Also, the 32-bit version uses less memory than the 64-bit +version. You may still run your application server in 64-bit +JVMs if needed to insure compatibility with the production +environment whilst keeping your IDE in 32-bit mode. +Visual Page Editor has experimental support for 64-bit JVMs in JBoss +Developer Studio 8. Please refer https://community.jboss.org/wiki/JBosstoolsVisualEditorFAQ[the JBoss Tools Visual Editor FAQ] for details. +=============================================================== + +[[installer-wizard_image]] +.Installation Wizard, Step 1 of 9 +image::gfx/introduction/installer_wizard_page1.png[] + +The rest of the steps are fairly self explanatory. If you run into trouble, please consult the videos above as they explore a few troubleshooting tips related to JRE/JDK setup. + +You can skip the step in the installation wizard that allows you to install JBoss Enterprise Application Platform 6.3 as we will do this in the next step. + +Once installed, launch JBoss Developer Studio. Please make sure to say *Yes* to the prompt that says "Will you allow JBoss Tools team to receive anonymous usage statistics for this Eclipse instance with JBoss Tools?". This information is very helpful to us when it comes to prioritizing our QA efforts in terms of operating system platforms. More information concerning our usage tracking can be found at http://www.jboss.org/tools/usage + +== Creating a new Java EE 6 project with Maven + + +[TIP] +================================================================= +For a deeper dive into the world of Maven and how it is used with +JBoss Developer Studio and JBoss Enterprise Application +Platform 6 review http://vimeo.com/39796236[this video]. +================================================================= + +Now that everything is properly installed, configured, running and verified to work, let's build something "from scratch". + +We recommend that you switch to the JBoss Perspective if you have not already. + +[TIP] +================================================================= +If you close JBoss Central, it is only a click away - simply +click on the JBoss icon in the Eclipse toolbar - it is normally +the last icon, on the last row - assuming you are in the JBoss +Perspective. +================================================================= + +First, select *Start from scratch -> Java EE Web Project* in JBoss Central. Under the covers, this uses a Maven archetype which creates a Java EE 6 web application (.war), based around Maven. The project can be built outside of the IDE, and in continuous integration solutions like Hudson/Jenkins. + +[[jboss-central_image]] +.JBoss Central +image::gfx/introduction/jboss_dev_studio_jboss_central.png[] + +You will be prompted with a dialog box that verifies that JBoss Developer Studio is configured correctly. If you are in a brand new workspace, the application server will not be configured yet and you will notice the lack of a check mark on the server/runtime row. + +[[new-project-wizard_image]] +.New Project Wizard +image::gfx/introduction/new_project_wizard.png[] + +[NOTE] +================================================================= +There are several ways to add JBoss Enterprise Application +Platform 6 to JBoss Developer Studio. The +*Install...* button on the new project wizard is probably the +easiest, but you can use any of the methods you are familiar +with! +================================================================= + +To add JBoss Enterprise Application Platform, click on the *Install...* button, or if you have not yet downloaded and unzipped the server, click on the *Download and Install...* button. + +[CAUTION] +================================================================= +The download option only works with the community application +server. Although the enterprise application server is listed, it +still needs to be manually downloaded. +================================================================= + +Selecting *Install...* will pop up the JBoss Runtime Detection section of Preferences. You can always get back to this dialog by selecting *Preferences -> JBoss Tools -> JBoss Tools Runtime Detection*. + +[[jboss_tools_runtime_detection_image]] +.JBoss Tools Runtime Detection +image::gfx/introduction/jboss_tools_runtime_detection.png[] + +Select the *Add* button which will take you to a file browser dialog where you should locate your unzipped JBoss server. + +[[runtime_open_dialog_image]] +.Runtime Open Dialog +image::gfx/introduction/runtime_open_dialog.png[] + +Select *Open* and JBoss Developer Studio will pop up the *Searching for runtimes...* window. + +[[searching_for_runtimes_dialog_image]] +.Searching for runtimes window +image::gfx/introduction/searching_for_runtimes_dialog.png[] + +Simply select *OK*. You should see the added runtime in the Paths list. + +[[jboss_tools_runtime_detection_after_image]] +.JBoss Tools Runtime Detection Completed +image::gfx/introduction/jboss_tools_runtime_detection_after.png[] + +Select *OK* to close the *Preferences* dialog, and you will be returned to the *New Project Example* dialog, with the the server/runtime found. + +[[as_eap_found_image]] +.JBoss AS 7.0/7.1 or EAP 6 Found +image::gfx/introduction/as_eap_found.png[] + +The *Target Runtime* allows you to choose between JBoss Enterprise Application Platform and JBoss AS 7. If it is left empty, JBoss AS 7 will be elected. + +Proceed to select the EAP 6.3 runtime, you just created. + +[[eap_selected_image]] +.JBoss EAP 6 runtime selected +image::gfx/introduction/as_eap_selected.png[] + +[CAUTION] +=================================================================================== +Choosing an enterprise application server as the runtime will require you to +configure Maven to use the JBoss Enterprise Maven repositories. For detailed instructions on +configure the Maven repositories, visit the link:https://access.redhat.com/site/documentation/en-US/JBoss_Enterprise_Application_Platform/6.3/html-single/Development_Guide/index.html#Install_the_JBoss_Enterprise_Application_Platform_6_Maven_Repository[JBoss Enterprise Application Platform 6.3 documentation]. +=================================================================================== + +You may see a warning (like the one in the screenshot), if you do not have the JBoss Enterprise Maven repository configured in your environment. Should this be the case, select the *repository* link in the warning, to open the JBoss Maven Integration wizard. The wizard dialog will prompt you to add the JBoss Enterprise Maven repository. + +[[add_jboss_maven_repo_image]] +.Add the JBoss Enterprise Maven repository +image::gfx/introduction/jboss_maven_repository.png[] + +Click *Ok*. + +You'll now be shown the proposed changes to your Maven settings.xml file. Click *Finish* after reviewing the proposed updates. + +[[update_settings_xml_image]] +.Update the Maven settings.xml file +image::gfx/introduction/jboss_maven_repo_settings_xml.png[] + +You'll be prompted to confirm the update. Click *Yes*. + +[[confirm_update_settings_xml_image]] +.Confirm the changes to the Maven settings.xml file +image::gfx/introduction/prompt_update_settings_xml.png[] + +The updates will now be persisted, and you'll be returned to the original wizard. + +Now, select *Next* in the *New Project* wizard to proceed to the next step. + +[[new-project-wizard-step_2_image]] +.New Project Wizard Step 2 +image::gfx/introduction/new_project_example_step_2.png[] + +The default *Project name* is `jboss-javaee6-webapp`. If this field appears blank, it is because your workspace already contains a "jboss-javaee6-webapp" in which case just provide another name for your project. Change the project name to `ticket-monster`, and the package name to `org.jboss.examples.ticketmonster`. + +Select *Finish*. + +JBoss Tools/JBoss Developer Studio will now generate the template project and import it into the workspace. You will see it pop up into the Project Explorer and a message that asks if you would like to open the cheatsheet file associated with the project. + +[[prompt_for_readme_image]] +.New Project Wizard Step 3 +image::gfx/introduction/prompt_for_cheatsheet.png[] + +Select *Finish* + +== Exploring the newly generated project + +Using the *Project Explorer*, open up the generated project, and double-click on the `pom.xml`. + +The generated project is a Maven-based project with a `pom.xml` in its root directory. + +[[newly_generated_project_explorer_image]] +.Project Explorer +image::gfx/introduction/newly_generated_project_explorer.png[] + +JBoss Developer Studio and JBoss Tools include m2e and m2e-wtp. m2e is the Maven Eclipse plug-in and provides a graphical editor for editing `pom.xml` files, along with the ability to run maven goals directly from within Eclipse. m2e-wtp allows you to deploy your Maven-based project directly to any Web Tools Project (WTP) compliant application server. This means you can drag & drop, use *Run As -> Run on Server* and other mechanisms to have the IDE deploy your application. + +The `pom.xml` editor has several tabs along its bottom edge. + +[[pom_xml_tabs_image]] +.pom.xml Editor Tabs +image::gfx/introduction/pom_xml_tabs.png[] + +For this tutorial, we do not need to edit the `pom.xml` as it already provides the Java EE 6 APIs that we will need (e.g. JPA, JAX-RS, CDI). You should spend some time exploring the *Dependencies* and the *pom.xml* (source view) tabs. + +One key element to make note of is `6.3.1.GA` which establishes the version of the JBoss Enterprise Application Platform dependencies. The BOM (Bill of Materials) specifies the versions of the Java EE (and other) APIs defined in the dependency section. + +If you are using community version of the JBoss Application Server and you selected that as your Target Runtime, you will find a different property as the version string. + +[CAUTION] +======================================================================================== +The specific version of the BOM (e.g. `6.3.1.GA`) is likely to change, so do not +be surprised if the version is slightly different. + +The recommended version of the BOM for a runtime (EAP 6) can be +obtained by visiting link:https://www.jboss.org/developer-materials/#!keyword=Java%20EE&formats=jbossdeveloper_archetype%20jbossdeveloper_bom[the JBoss Stacks site]. +======================================================================================== + +[[project_explorer_java_packages_image]] +.Project Explorer Java Packages +image::gfx/introduction/project_explorer_java_packages.png[] + +Using the *Project Explorer*, drill-down into `src/main/java` under *Java Resources*. + +The initial project includes the following Java packages: + +`.controller`:: + contains the backing beans for `#{newMember}` and `#{memberRegistration}` in the JSF page `index.xhtml` +`.data`:: + contains a class which uses `@Produces` and `@Named` to return the list of members for `index.xhtml` +`.model`:: + contains the JPA entity class, a POJO annotated with `@Entity`, annotated with Bean Validation (JSR 303) constraints +`.rest`:: + contains the JAX-RS endpoints, POJOs annotated with `@Path` +`.service`:: + handles the registration transaction for new members +`.util`:: + contains Resources.java which sets up an alias for `@PersistenceContext` to be injectable via `@Inject` + +Now, let's explore the resources in the project. + +[[project_explorer_resources_image]] +.Project Explorer Resources +image::gfx/introduction/project_explorer_resources.png[] + +Under src you will find: + +`main/resources/import.sql`:: + contains insert statements that provides initial database data. This is particularly useful when `hibernate.hbm2dll.auto=create-drop` is set in `persistence.xml`. `hibernate.hbm2dll.auto=create-drop` causes the schema to be recreated each time the application is deployed. +`main/resources/META-INF/persistence.xml`:: + establishes that this project contains JPA entities and it identifies the datasource, which is deployed alongside the project. It also includes the `hibernate.hbm2dll.auto` property set to `create-drop` by default. + +`test/java/test`:: + provides the `.test` package that contains `MemberRegistrationTest.java`, an Arquillian based test that runs both from within JBoss Developer Studio via *Run As -> JUnit Test* and at the command line: ++ +`mvn test –Parq-jbossas-remote` ++ +Note that you will need to start the JBoss Enterprise Application Platform 6.3 server before running the test. + +`src/main/webapp`:: + contains `index.xhtml`, the JSF-based user interface for the sample application. If you double-click on that file you will see Visual Page Editor allows you to visually navigate through the file and see the source simultaneously. Changes to the source are immediately reflected in the visual pane. + +In `src/main/webapp/WEB-INF`, you will find three key files: + +`beans.xml`:: + is an empty file that indicates this is a CDI capable EE6 application +`faces-config.xml`:: + is an empty file that indicates this is a JSF capable EE6 application +`ticket-monster-ds.xml`:: + when deployed, creates a new datasource within the JBoss container + + +== Adding a new entity using Forge + + +There are several ways to add a new JPA entity to your project: + +Starting from scratch:: + Right-click on the `.model` package and select *New -> Class*. JPA entities are annotated POJOs so starting from a simple class is a common approach. +Reverse Engineering:: + Right-click on the "model" package and select New -> JPA Entities from Tables. For more information on this technique see link:https://vimeo.com/39608294[this video] +Using Forge:: + to create a new entity for your project using a CLI (we will explore this in more detail below) +Reverse Engineering with Forge:: + Forge has a Hibernate Tools plug-in that allows you to script the conversion of RDBMS schema into JPA entities. For more information on this technique see link:https://vimeo.com/39608326[this video]. + +For the purposes of this tutorial, we will take advantage of Forge to add a new JPA entity. This requires the least keystrokes, and we do not yet have a RDBMS schema to reverse engineer. There is also an optional section for adding an entity using *New -> Class*. + +Select the project in the *Project Navigator* view of JBoss Developer Studio and enter the *Ctrl + 4* (in Windows/Linux) or *Cmd + 4* (Mac) key combination. +This will launch Forge if it is not started already. + +[[starting_forge_for_first_time_image]] +.Starting Forge for the first time +image::gfx/introduction/forge_is_starting.png[] + +The list of commands that you can execute in Forge will be visible in the Forge quick action menu. + +[[forge_action_menu_image]] +.Forge action menu +image::gfx/introduction/forge_action_menu.png[] + +[TIP] +======================================================================================== +If you do not see a lot of commands in the quick action menu, then you may not have selected the project. +The Forge quick action menu is contextual in nature, and therefore, it displays commands relevant to the current selection in the project explorer. + +When nothing is selected, then fewer commands are shown. +======================================================================================== + +[TIP] +============================================================================== +An alternative method to activate Forge is: + +* *Window -> Show View -> Forge Console*. Click the *Start* button in the view. + +[[show_view_image]] +.Launch the *Show View* dialog +image::gfx/introduction/show_forge_view.png[] + +[[select_forge_view_image]] +.Select the Forge Console view +image::gfx/introduction/select_forge_view.png[] + +Note: Activating Forge this way displays the Forge console that allows you to +execute the same commands via a shell interface. +============================================================================== + +[TIP] +============================================================================== +You can always start Forge using the green arrow (or +stop via the red square) in the Forge Console tab. + +[[forge_start_stop_image]] +.Show Forge Start/Stop +image::gfx/introduction/forge_console_tab.png[] +============================================================================== + +Forge is a multi-faceted rapid application development tool that allows you to enter commands that generate classes and code. You could use either a GUI within your IDE that offers a familar wizard and dialog based UI, or a shell-like interface to perform operations. It will automatically update the IDE for you. A key feature is "contentual command activation", launched by running the Forge shortcut (*Ctrl + 4* or *Cmd + 4*). For instance, launching Forge on a selected project activates different commands, than launching it in isolation, or for that matter launching Forge with a selected Java source file. + +We'll generate an entity using the Forge GUI. Let's work through this, step by step. + +We start by selecting the TicketMonster project. Launch Forge through the shortcut (*Ctrl + 4* or *Cmd + 4*). +Type `jpa` in the command filter textbox located in the menu. The menu will filter out irrelevant entries, leaving you with JPA-specific commands. + +[[filter_commands_in_forge_menu_image]] +.Filter commands in the Forge menu +image::gfx/introduction/forge_quick_action_menu_filter_jpa.png[] + +Select the "JPA: New Entity" entry in the menu. Click it or hit the `Enter` key to execute the command. You will be presented with a dialog where you can provide certain inputs that control how the new entity would be generated, like the package where the entity would be created, the name of the JPA entity/class, the primary-key strategy used for the entity etc. + +[[jpa_new_entity_forge_command_image]] +.The new JPA entity command in Forge +image::gfx/introduction/forge_jpa_new_entity.png[] + +Specify the value of the entity as `Event` and click `Finish`. The defaults for other values are sufficient - note how Forge intelligently constructs the value for the package field from the Maven group Id and artifact Id values of the project. + +[[create_event_entity_image]] +.Create the Event entity in Forge +image::gfx/introduction/forge_jpa_new_entity_event.png[] + +You should see a notification bubble in Eclipse when Forge completes the action. + +[[create_event_entity_image_bubble]] +.The Forge notification bubble in Eclipse +image::gfx/introduction/forge_jpa_new_entity_created.png[] + +Forge would have created a JPA entity as instructed, and it would also open the Java source file in Eclipse. Note that it would have created not only a new class with the `@Entity` annotation, but also created a primary-key field named `id`, a `version` field, along with getters and setters for both, in addition to `equals`, `hashCode` and `toString` methods. + +[[event_entity_image]] +.The newly created Event entity +image::gfx/introduction/forge_event_entity_source.png[] + +Let's add a new field to this entity. Select the `Event` class in the project navigator and launch the Forge menu once again. Filter on `jpa` as usual, and launch the "JPA: New Field" command. +Specify the field name as `name`, to store the name of the event. The defaults are sufficient for other input fields. Click `Finish` or hit the `Enter` button as usual. + +[[jpa_new_field_wizard_image]] +.The JPA field wizard in Forge +image::gfx/introduction/forge_jpa_new_field_name.png[] + +You will now notice that the `Event` class is enhanced with a `name` field of type `String`, as well as a getter and setter, along with modifications to the `toString` method. + +[[jpa_newly_created_field_image]] +.The newly created field in the Event class +image::gfx/introduction/forge_added_name.png[] + +Let's now add Bean Validation (JSR-303) capabilities to the project. Launch the Forge menu, and filter for the "Constraint: Setup" command. Execute the command. + +[[filter_constraint_commands_in_forge_menu_image]] +.Filter for constraint commands in the Forge menu +image::gfx/introduction/forge_quick_action_menu_filter_constraint.png[] + +You'll be presented with a choice on what Bean Validation providers you'd like to setup in the project. The defaults are sufficient - we'll use the Bean Validation provider supplied by the Java EE application. Click `Finish` or hit `Enter` to setup Bean valdiation. + +[[setup_bean_validation_image]] +.Setup Bean Validation +image::gfx/introduction/forge_setup_constraint_wizard.png[] + +We'll now add a constraint on the newly added `name` field in the `Event` class. Select the `Event` class in the project navigator and proceed to launch the "Constraint: Add" command from the Forge menu. Note that selecting the `Event` class allows Forge to provide commands relevant to this class in the action menu, as well as populating this class in input fields where it is fit to populate them. + +[[launch_add_constraint_wizard_image]] +.Select the Event class and launch the "Constraint: Add" wizard +image::gfx/introduction/forge_add_constraint_on_event.png[] + +This launches a wizard where one can add Bean Validation constraints. The class to operate on will default to the currently selected class, i.e. `Event`. If you want to switch to a different class, you can do so in the wizard. There is no need to re-launch the wizard. + +[[default_value_for_constraint_add_command_image]] +.The constraint is added to the selected class +image::gfx/introduction/forge_select_event_entity_for_constraint.png[] + +Proceed to select the `name` field, on which we add a `NotNull` constraint. Click `Finish` or hit `Enter`. + +[[add_notnull_constraint_event_name_image]] +.Add a NotNull constraint on Event name +image::gfx/introduction/forge_constraint_add_notnull_on_name.png[] + +Similarly, add a `Size` constraint with `min` and `max` values of 5 and 50 respectively on the `name` field. + +[[add_size_constraint_event_name_image]] +.Add a Size constraint on Event name +image::gfx/introduction/forge_constraint_add_size_on_name.png[] + +[[attribute_values_size_constraint_on_name_image]] +.Specify attribute values for the Size constraint +image::gfx/introduction/forge_constraint_add_set_size_attributes_on_name.png[] + +From this point forward, we will assume you have the basics of using Forge's menu and the commands executed thus far. Add a new field `description` to the Event class. + +[[add_description_field_to_event_image]] +.Add the description field to Event +image::gfx/introduction/forge_jpa_new_field_description.png[] + +Add a `Size` constraint on the description field to the event class, with `min` and `max` values of 20 and 1000 respectively. + +[[add_size_constraint_event_description_image]] +.Add a Size constraint on Event name +image::gfx/introduction/forge_constraint_add_size_on_description.png[] + +[[attribute_values_size_constraint_on_description_image]] +.Specify attribute values for the Size constraint +image::gfx/introduction/forge_constraint_add_set_size_attributes_on_description.png[] + +Add a new `boolean` field `major`. Note - you will need to change the type to `boolean` from the default value of `String`. + +[[add_major_field_to_event_image]] +.Add the major field to Event +image::gfx/introduction/forge_jpa_new_field_major.png[] + +Add another field `picture` to the Event class. + +[[add_picture_field_to_event_image]] +.Add the picture field to Event +image::gfx/introduction/forge_jpa_new_field_picture.png[] + +The easiest way to see the results of Forge operating on the `Event.java` JPA Entity is to use the *Outline View* of JBoss Developer Studio. It is normally on the right-side of the IDE when using the JBoss Perspective. + +[[outline_of_event_image]] +.Outline View +image::gfx/introduction/outline_of_event.png[] + +Alternatively, you could perform the same sequence of operations in the Forge Console, using these commands: + +[source,fsh] +---------------------------------------------------------------------------- +jpa-new-entity --named Event --targetPackage org.jboss.examples.ticketmonster.model ; +jpa-new-field --named name ; +constraint-setup ; +constraint-add --onProperty name --constraint NotNull ; +constraint-add --onProperty name --constraint Size --min 5 --max 50 --message "An event's name must contain between 5 and 50 characters" ; +jpa-new-field --named description ; +constraint-add --onProperty description --constraint Size --min 20 --max 1000 --message "An event's description must contain between 20 and 1000 characters" ; +jpa-new-field --named major --type boolean ; +jpa-new-field --named picture ; +---------------------------------------------------------------------------- + + +== Reviewing persistence.xml & updating import.sql + + +By default, the entity classes generate the database schema, and is controlled by `src/main/resources/persistence.xml`. + +The two key settings are the `` and the `hibernate.hbm2ddl.auto` property. The datasource maps to the datasource defined in `src\main\webapp\ticket-monster–ds.xml`. + +The `hibernate.hbm2ddl.auto=create-drop` property indicates that all database tables will be dropped when an application is undeployed, or redeployed, and created when the application is deployed. + +The `import.sql` file contains SQL statements that will inject sample data into your initial database structure. Add the following insert statements: + +[source,sql] +---------------------------------------------------------------------------------------------------- +insert into Event (id, name, description, major, picture, version) values (1, 'Shane''s Sock Puppets', 'This critically acclaimed masterpiece...', true, 'http://dl.dropbox.com/u/65660684/640px-Carnival_Puppets.jpg', 1); +insert into Event (id, name, description, major, picture, version) values (2, 'Rock concert of the decade', 'Get ready to rock...', true, 'http://dl.dropbox.com/u/65660684/640px-Weir%2C_Bob_(2007)_2.jpg', 1); +---------------------------------------------------------------------------------------------------- + +== Adding a new entity using JBoss Developer Studio + + +Alternatively, we can add an entity with JBoss Developer Studio or JBoss Tools. + +First, right-click on the `.model` package and select *New -> Class*. Enter the class name as `Venue` - our concerts & shows happen at particular stadiums, concert halls and theaters. + +First, add some private fields representing the entities properties, which translate to the columns in the database table. + +[source,java] +---------------------------------------------------------------------------------------------------- +package org.jboss.examples.ticketmonster.model; + +public class Venue { + private Long id; + private String name; + private String description; + private int capacity; +} +---------------------------------------------------------------------------------------------------- + +Now, right-click on the editor itself, and from the pop-up, context menu select *Source -> Generate Getters and Setters*. + +[[generate_getters_setters_menu_image]] +.Generate Getters and Setters Menu +image::gfx/introduction/generate_getters_setters.png[] + +This will create accessor and mutator methods for all your fields, making them accessible properties for the entity class. + +[[generate_getters_setters_dialog_image]] +.Generate Getters and Setters Dialog +image::gfx/introduction/getter_setter_dialog.png[] + +Click *Select All* and then *OK*. + +[[venue_after_getters_setters_image]] +.Venue.java with gets/sets +image::gfx/introduction/venue_after_getters_setters.png[] + +Now, right-click on the editor, from the pop-up context menu select *Source -> Generate Hibernate/JPA Annotations*. + +If you are prompted to save `Venue.java`, simply select OK. + +[[save_modified_resources_image]] +.Save Modified Resources +image::gfx/introduction/save_modified_resources.png[] + +The *Hibernate: add JPA annotations* wizard will start up. First, verify that `Venue` is the class you are working on. + +[[hibernate_add_jpa_image]] +.Hibernate: add JPA annotations +image::gfx/introduction/hibernate_add_jpa_annotations.png[] + +Select *Next*. + +The next step in the wizard will provide a sampling of the refactored sources – describing the basic changes that are being made to `Venue`. + +[[hibernate_add_jpa_annotations_step2_image]] +.Hibernate: add JPA annotations Step 2 +image::gfx/introduction/hibernate_add_jpa_annotations_step2.png[] + +Select *Finish*. + +Now you may wish to add the Bean Validation constraint annotations, such as `@NotNull` to the fields. + +== Deployment + + +At this point, if you have not already deployed the application, right click on the project name in the Project Explorer and select *Run As -> Run on Server*. If needed, this will startup the application server instance, compile & build the application and push the application into the `JBOSS_HOME/standalone/deployments` directory. This directory is scanned for new deployments, so simply placing your war in the directory will cause it to be deployed. + +[CAUTION] +================================================================= +If you have been using another application server or web server +such as Tomcat, shut it down now to avoid any port conflicts. +================================================================= + +[[run_as_run_on_server_image]] +.Run As -> Run on Server +image::gfx/introduction/run_as_run_on_server.png[] + +Now, deploy the h2console webapp. It can be found in the JBoss EAP quickstarts. You can read more on how to do this in the link:http://www.jboss.org/quickstarts/eap/h2-console/[h2console quickstart]. + +[[h2_console_app_image]] +.Obtain the H2 console app for deployment +image::gfx/introduction/quickstarts_directory_layout.png[] + +You need to deploy the `h2console.war` application, located in the quickstarts, to the JBoss Application Server. You can deploy this application by copying the WAR file to the `$JBOSS_HOME/standalone/deployments` directory. + +[[deploy_h2_console_app_image]] +.Deploy the H2 console app +image::gfx/introduction/h2console_deployments.png[] + +The *Run As -> Run on Server* option will also launch the internal Eclipse browser with the appropriate URL so that you can immediately begin interacting with the application. + +[[result_run_on_server_image]] +.Eclipse Browser after Run As -> Run on Server +image::gfx/introduction/result_run_on_server.png[] + +Now, go to http://localhost:8080/h2console to start up the h2 console. + +[[h2console_in_browser_image]] +.h2console in browser +image::gfx/introduction/h2console_in_browser.png[] + +Use `jdbc:h2:mem:ticket-monster` as the JDBC URL (this is defined in `src/main/webapp/WEB-INF/ticket-monster-ds.xml`), `sa` as the username and `sa` as the password. + +Click *Connect* + +You will see both the `EVENT` table, the `VENUE` table and the `MEMBER` tables have been added to the H2 schema. + +And if you enter the SQL statement: `select * from event` and select the *Run* (Ctrl-Enter) button, it will display the data you entered in the `import.sql` file in a previous step. With these relatively simple steps, you have verified that your new EE 6 JPA entities have been added to the system and deployed successfully, creating the supporting RDBMS schema as needed. + +[[h2console_select_from_event.png]] +.h2console Select * from Event +image::gfx/introduction/h2console_select_from_event.png[] + + +== Adding a JAX-RS RESTful web service + + +The goal of this section of the tutorial is to walk you through the creation of a POJO with the JAX-RS annotations. + +Right-click on the `.rest` package, select *New -> Class* from the context menu, and enter `EventService` as the class name. + +[[new_class_eventservice_image]] +.New Class EventService +image::gfx/introduction/new_class_eventservice.png[] + +Select *Finish*. + +Replace the contents of the class with this sample code: + +[source,java] +--------------------------------------------------------------------------------------------------------- +package org.jboss.examples.ticketmonster.rest; + +@Path("/events") +@RequestScoped +public class EventService { + @Inject + private EntityManager em; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List getAllEvents() { + final List results = + em.createQuery( + "select e from Event e order by e.name").getResultList(); + return results; + } +} +--------------------------------------------------------------------------------------------------------- + +This class is a JAX-RS endpoint that returns all Events. + +[[event_service_copy_paste_image]] +.EventService after Copy and Paste +image::gfx/introduction/event_service_copy_paste.png[] + +You'll notice a lot of errors, relating to missing imports. The easiest way to solve this is to right-click inside the editor and select *Source -> Organize Imports* from the context menu. + +[[source_organize_imports_image]] +.Source -> Organize -> Imports +image::gfx/introduction/source_organize_imports.png[] + +Some of the class names are not unique. Eclipse will prompt you with any decisions around what class is intended. Select the following: + +* `javax.ws.rs.core.MediaType` +* `org.jboss.examples.ticketmonster.model.Event` +* `javax.ws.rs.Produces` +* `java.util.List` +* `java.inject.Inject` +* `java.enterprise.context.RequestScoped` + +The following screenshots illustrate how you handle these decisions. The Figure description indicates the name of the class you should select. + +[[organize_imports_1_image]] +.javax.ws.rs.core.MediaType +image::gfx/introduction/organize_imports_1.png[] + +[[organize_imports_2_image]] +.org.jboss.examples.ticketmonster.model.Event +image::gfx/introduction/organize_imports_2.png[] + +[[organize_imports_3_image]] +.javax.ws.rs.Produces +image::gfx/introduction/organize_imports_3.png[] + +[[organize_imports_4_image]] +.java.util.List +image::gfx/introduction/organize_imports_4.png[] + +[[organize_imports_5_image]] +.javax.inject.Inject +image::gfx/introduction/organize_imports_5.png[] + +[[organize_imports_6_image]] +.javax.enterprise.context.RequestScoped +image::gfx/introduction/organize_imports_6.png[] + +You should end up with these imports: + +[source,java] +--------------------------------------------------------------------------------------------------------- +import java.util.List; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.examples.ticketmonster.model.Event; +--------------------------------------------------------------------------------------------------------- + +Once these import statements are in place you should have no more compilation errors. When you save `EventService.java`, you will see it listed in JAX-RS REST Web Services in the Project Explorer. + +[[project_explorer_jax_rs_services_image]] +.Project Explorer JAX-RS Services +image::gfx/introduction/project_explorer_jax_rs_services.png[] + +This feature of JBoss Developer Studio and JBoss Tools provides a nice visual indicator that you have successfully configured your JAX-RS endpoint. + +You should now redeploy your project via *Run As -> Run on Server*, or by right clicking on the project in the *Servers* tab and select *Full Publish*. + +[[full_publish_image]] +.Full Publish +image::gfx/introduction/full_publish.png[] + +Using a browser, visit http://localhost:8080/ticket-monster/rest/events to see the results of the query, formatted as JSON (JavaScript Object Notation). + +[[json_event_results_image]] +.JSON Response +image::gfx/introduction/json_event_results.png[] + +[NOTE] +================================================================================= +The `rest` prefix is setup in a file called `JaxRsActivator.java` which contains +a small bit of code that sets up the application for JAX-RS endpoints. +================================================================================= + +== Adding a jQuery Mobile client application + + +Now, it is time to add a HTML5, jQuery based client application that is optimized for the mobile web experience. + +There are numerous JavaScript libraries that help you optimize the end-user experience on a mobile web browser. We have found that jQuery Mobile is one of the easier ones to get started with but as your skills mature, you might investigate solutions like Sencha Touch, Zepto or Jo. This tutorial focuses on jQuery Mobile as the basis for creating the UI layer of the application. + +The UI components interact with the JAX-RS RESTful services (e.g. `EventService.java`). + +[TIP] +================================================================================= +For more information on building HTML5 + REST applications with JBoss technologies, check +out link:http://www.jboss.org/aerogear[Aerogear]. +================================================================================= + +These next steps will guide you through the creation of a file called `mobile.html` that provides a mobile friendly version of the application, using jQuery Mobile. + +First, using the Project Explorer, navigate to `src/main/webapp`, and right-click on `webapp`, and choose *New HTML file*. +[[new_html_file_image]] +.New HTML File +image::gfx/introduction/new_html_file.png[] + +[CAUTION] +================================================================================= +In certain versions of JBoss Developer Studio, the New HTML File Wizard may start +off with your target location being `m2e-wtp/web-resources`, this is an +incorrect location and it is a bug, link:https://issues.jboss.org/browse/JBIDE-11472[JBIDE-11472]. + +This issue has been corrected in JBoss Developer Studio 6. +================================================================================= + +Change directory to `ticket-monster/src/main/webapp` and enter name the file `mobile.html`. + +[[new_html_file_correct_location_image]] +.New HTML File src/main/webapp +image::gfx/introduction/new_html_file_correct_location.png[] + +Select *Next*. + +On the *Select HTML Template* page of the *New HTML File* wizard, select *New HTML File (5)*. This template will get you started with a boilerplate HTML5 document. + +[[select_html_template]] +.Select New HTML File (5) Template +image::gfx/introduction/select_html_template.png[] + +Select *Finish*. + +The document must start with `` as this identifies the page as HTML 5 based. For this particular phase of the tutorial, we are not introducing a bunch of HTML 5 specific concepts like the new form fields (type=email), websockets or the new CSS capabilities. For now, we simply wish to get our mobile application completed as soon as possible. The good news is that jQuery and jQuery Mobile make the consumption of a RESTful endpoint very simple. + +You will now notice the Palette View visible in the JBoss perspective. This view contains a collection of popular jQuery Mobile widgets that can be dragged and dropped into the HTML pages to speed up construction of jQuery Mobile pages. + +[[jquery_mobile_palette]] +.The jQuery Mobile Palette +image::gfx/introduction/jquery_mobile_palette.png[] + +[TIP] +================================================================= +For a deeper dive into the jQuery Mobile palette feature in +JBoss Developer Studio review http://vimeo.com/67480300[this video]. +================================================================= + +Let us first set the title of the HTML5 document as: + +[source,html] +---------------------------------------------------------------------------------------------------- + + + + +TicketMonster + + + + + +---------------------------------------------------------------------------------------------------- + +We shall now add the jQuery and jQuery Mobile JavaScript and CSS files to the HTML document. Luckily for us we can do this by clicking the _JS/CSS_ widget in the palette. + +[[js_css_widget]] +.Click the JS/CSS widget +image::gfx/introduction/js_css_widget.png[] + +[[js_css_versions]] +.Select the versions of libraries to add +image::gfx/introduction/js_css_widget_library_versions.png[] + +This results in the following document with the jQuery JavaScript file and the jQuery Mobile JavaScript and CSS files being added to the _head_ element. + +[source,html] +---------------------------------------------------------------------------------------------------- + + + + + + + + + TicketMonster + + + + + +---------------------------------------------------------------------------------------------------- + +We shall now proceed to setup the page layout. Click the _page_ widget in the palette to do so. Ensure that the cursor is in the `` element of the document when you do so. + +[[jquery_mobile_page_widget]] +.Click the page widget +image::gfx/introduction/jquery_mobile_page_widget.png[] + +[CAUTION] +================================================================= +When you click some of the widgets in the palette, it is important +to have the cursor in the right element of the document. +Failing to observe this will result in the widget being added in +undesired locations. Alternatively, you can drag and drop the +widget to the desired location in the document. +================================================================= + +This opens a dialog to configure the jQuery Mobile page. + +[[jquery_mobile_page]] +.Create a new jQuery Mobile page +image::gfx/introduction/jquery_mobile_page.png[] + +Set the page title as "TicketMonster", footer as blank, and the ID as "page1". Click *Finish* to add a new jQuery Mobile page to the document. The layout is now established. + +[source,html] +---------------------------------------------------------------------------------------------------- + + + + + + + + + TicketMonster + + +
+
+

TicketMonster

+
+
+

Page content goes here.

+
+
+

+
+
+ + +---------------------------------------------------------------------------------------------------- + +To populate the page content, remove the paragraph element: `

Page content goes here.

` to start with a blank content section. Click the _Listview_ widget in the palette to start populating the content section. + +[[jquery_mobile_listview_widget]] +.Click the Listview widget +image::gfx/introduction/jquery_mobile_listview_widget.png[] + +This opens a new dialog to configure the jQuery Mobile listview widget. + +[[jquery_mobile_listview]] +.Add a jQuery Mobile Listview widget +image::gfx/introduction/jquery_mobile_listview.png[] + +Select the inset checkbox to display the list as an inset list. Inset lists do not span the entire widget of the display. Set the ID as "listOfItems". Retain the number of items in the list as three, modify the label values to `One`, `Two` and `Three` respectively, and finally, set the URL values to '#'. Retain the default values for the other fields, and click *Finish*. This will create a listview widget with 3 item entries in the list. The jQuery Mobile page is now structurally complete. + +[source,html] +---------------------------------------------------------------------------------------------------- + + + + + + + + + TicketMonster + + +
+
+

TicketMonster

+
+
+ +
+
+

+
+
+ + +---------------------------------------------------------------------------------------------------- + +You might notice that in the *Visual Page Editor*, the visual portion is not that attractive, this is because the majority of jQuery Mobile magic happens at runtime and our visual page editor simply displays the HTML without embellishment. + +Visit link:http://localhost:8080/ticket-monster/mobile.html[]. + +[NOTE] +================================================================================= +Note: Normally HTML files are deployed automatically, if you find it missing, +just use Full Publish or Run As Run on Server as demonstrated in previous steps. +================================================================================= + +As soon as the page loads, you can view the jQuery Mobile enhanced page. + +[[jquery_mobile_template_image]] +.jQuery Mobile Template +image::gfx/introduction/jquery_mobile_template.png[] + +One side benefit of using a HTML5 + jQuery-based front-end to your application is that it allows for fast turnaround in development. Simply edit the HTML file, save the file and refresh your browser. + +Now the secret sauce to connecting your front-end to your back-end is simply observing the jQuery Mobile _pageinit_ JavaScript event and including an invocation of the previously created Events JAX-RS service. + +Insert the following block of code as the last item in the `` element + +[source,html] +---------------------------------------------------------------------------------------------------- + + ... + TicketMonster + + +---------------------------------------------------------------------------------------------------- + +Note: + +* On triggering _pageinit_ on the page having id "page1" +* using `$.getJSON("rest/events")` to hit the `EventService.java` +* a commented out `// console.log`, causes problems in IE +* Getting a reference to `listOfItems` which is declared in the HTML using an `id` attribute +* Calling `.empty` on that list - removing the exiting `One, Two, Three` items +* For each event - based on what is returned in step 1 + +* another commented out `// console.log` +* `append` the found event to the UL in the HTML +* `refresh` the `listOfItems` + + +[NOTE] +================================================================================= +You may find the `.append("
  • ...")` syntax unattractive, embedding HTML inside +of the JS .append method, this can be corrected using various JS templating +techniques. +================================================================================= + +The result is ready for the average mobile phone. Simply refresh your browser to see the results. + +[[jquery_mobile_results_image]] +.jQuery Mobile REST Results +image::gfx/introduction/jquery_mobile_results.png[] + +JBoss Developer Studio and JBoss Tools includes BrowerSim to help you better understand what your mobile application will look like. Look for a "phone" icon in the toolbar, visible in the JBoss Perspective. + +[[mobile_browsersim_in_toolbar_image]] +.Mobile BrowserSim icon in Eclipse Toolbar +image::gfx/introduction/mobile_browsersim_in_toolbar.png[] + +[NOTE] +================================================================================= +The BrowserSim tool takes advantage of a locally installed Safari (Mac & Windows) +on your workstation. It does not package a whole browser by itself. You will +need to install Safari on Windows to leverage this feature – but that is more +economical than having to purchase a MacBook to quickly look at your mobile-web +focused application! +================================================================================= + +[[mobile_browsersim_image]] +.Mobile BrowserSim +image::gfx/introduction/mobile_browsersim.png[] +The Mobile BrowserSim has a Devices menu, on Mac it is in the top menu bar and on Windows it is available via right-click as a pop-up menu. This menu allows you to change user-agent and dimensions of the browser, plus change the orientation of the device. + +[[mobile_browsersim_devices_menu_image]] +.Mobile BrowserSim Devices Menu +image::gfx/introduction/mobile_browsersim_devices_menu.png[] + +[[mobile_browsersim_windows_menu_image]] +.Mobile BrowserSim on Windows 7 +image::gfx/introduction/mobile_browsersim_windows_menu.png[] + +You can also add your own custom device/browser types. + +[[mobile_browsersim_custom_devices_image]] +.Mobile BrowserSim Custom Devices Window +image::gfx/introduction/mobile_browsersim_custom_devices.png[] + +Under the *File* menu, you will find a *View Page Source* option that will open up the mobile-version of the website's source code inside of JBoss Developer Studio. This is a very useful feature for learning how other developers are creating their mobile web presence. + +[[mobile_browsersim_bofa_source_image]] +.Mobile BrowserSim View Source +image::gfx/introduction/mobile_browsersim_bofa_source.png[] + +== Conclusion + +This concludes our introduction to building HTML5 Mobile Web applications using Java EE 6 with Forge and JBoss Developer Studio. At this point, you should feel confident enough to tackle any of the additional exercises to learn how the TicketMonster sample application is constructed. + +=== Cleaning up the generated code + + +Before we proceed with the tutorial and implement TicketMonster, we need to clean up some of the archetype-generated code. The Member management code, while useful for illustrating the general setup of a Java EE 6 web application, will not be part of TicketMonster, so we can safely remove some packages, classes, and resources: + +* All the Member-related persistence and business code: + +** `src/main/java/org/jboss/examples/ticketmonster/controller/` +** `src/main/java/org/jboss/examples/ticketmonster/data/` +** `src/main/java/org/jboss/examples/ticketmonster/model/Member.java` +** `src/main/java/org/jboss/examples/ticketmonster/rest/MemberResourceRESTService.java` +** `src/main/java/org/jboss/examples/ticketmonster/service/MemberRegistration.java` +** `src/test/java/org/jboss/examples/ticketmonster/rest/MemberRegistrationTest.java` + +* Generated web content + +** `src/main/webapp/index.html` +** `src/main/webapp/index.xhtml` +** `src/main/webapp/WEB-INF/templates/` + +* JSF configuration + +** `src/main/webapp/WEB-INF/faces-config.xml` + +* Prototype mobile application (we will generate a proper mobile interface) + +** `src/main/webapp/mobile.html` + +Also, we will update the `src/main/resources/import.sql` file and remove the `Member` entity insertion: + +[source,sql] +---------------------------------------------------------------------------------------------------- +insert into Member (id, name, email, phone_number) values (0, 'John Smith', 'john.smith@mailinator.com', '2125551212' +---------------------------------------------------------------------------------------------------- + +The data file should contain only the Event data import: + +[source,sql] +---------------------------------------------------------------------------------------------------- +insert into Event (id, name, description, major, picture, version) values (1, 'Shane''s Sock Puppets', 'This critically acclaimed masterpiece...', true, 'http://dl.dropbox.com/u/65660684/640px-Carnival_Puppets.jpg', 1); +insert into Event (id, name, description, major, picture, version) values (2, 'Rock concert of the decade', 'Get ready to rock...', true, 'http://dl.dropbox.com/u/65660684/640px-Weir%2C_Bob_(2007)_2.jpg', 1); +---------------------------------------------------------------------------------------------------- diff --git a/tutorial/JBossDeployment.asciidoc b/tutorial/JBossDeployment.asciidoc new file mode 100644 index 000000000..5b247a973 --- /dev/null +++ b/tutorial/JBossDeployment.asciidoc @@ -0,0 +1,255 @@ +[[DeployingToJBossEAP]] += Appendix A - Deploying to JBoss EAP locally +:Author: Vineet Reynolds + +== What Will You Learn Here? + +This appendix demonstrates how to import, develop and deploy the TicketMonster example using JBoss Developer Studio: + +* Obtain and import the TicketMonster example source code +* Deploy the application to JBoss EAP with JBoss Server Tools + +== Pre-requisites + +We don't recommend using the *Internal Web Browser*, although it is configured as the default web browser in the IDE. +In certain environments, it may lack features present in modern web browsers, thus providing a sub-optimal user and developer experience. + +We shall therefore set the IDE default web browser to be your default system web browser. Click *Window*→*Web +Browser*→*Default system web browser*. + +== Import the Project source code + +Once the TicketMonster source code is obtained and unpackaged, you must import it into JBoss +Developer Studio, as detailed in the procedure below. TicketMonster is a Maven-based project so a +specific Import Maven Project wizard is used for the import. + +. Click *File*→*Import* to open the *Import* wizard. +. Expand *Maven*, select *Existing Maven Projects* and click *Next*. +. In the *Root Directory* field, enter the path to the TicketMonster source code. Alternatively, +click *Browse* to navigate to the source code location. The *Import Maven Project* wizard +recursively searches the path for a *pom.xml* file. The *pom.xml* file identifies the project as a +Maven project. The file is listed under *Projects* once it is found. ++ +.pom.xml File Listed in the Projects Pane +image::gfx/pom-file-projects-pane.png[] +. Click *Finish*. When the import process is complete, the project is listed in the *Project Explorer* view. + + +== Deploying to JBoss EAP using JBoss Developer Studio + +Once you have imported the TicketMonster source code into JBoss Developer Studio, the project +application can be deployed to the JBoss EAP server and the running application viewed in the +default system web browser, as detailed in the procedure below: + +. In the *Project Explorer* view, right-click *ticket-monster* and click *Run As*→*Run on +Server*. +. Under *How do you want to select the server?*, ensure *Choose an existing +server* is selected. +. In the *Server* table, expand *localhost*, select *jboss-eap-version* where version +denotes the JBoss EAP version, and click *Next*. ++ +[[jboss-eap-selected-deployment]] +.JBoss EAP 6.x Server Selected +image::gfx/jboss-eap-selected-deployment.png[] +. Ensure *ticket-monster* is listed in the *Configured* column and click Finish. The +*Console* view automatically becomes the view in focus and displays the output from the +JBoss EAP server. Once deploying is complete, the web application opens in the default +system web browser. ++ +[[ticketmonster-configured-jboss-eap]] +.ticket-monster Listed in the Configured Column +image::gfx/ticketmonster-configured-jboss-eap.png[] + + +== Deploying to JBoss EAP using the command-line + +_Start JBoss Enterprise Application Platform 6.3_. + +1. Open a command line and navigate to the root of the JBoss server directory. +2. The following shows the command line to start the server with the web profile: ++ +---- +For Linux: JBOSS_HOME/bin/standalone.sh +For Windows: JBOSS_HOME\bin\standalone.bat +---- + +Then, _deploy TicketMonster_. + + +1. Make sure you have started the JBoss Server as described above. +2. Type this command to build and deploy the archive into a running server instance. ++ +---- +mvn clean package jboss-as:deploy +---- ++ +(You can use the `arq-jbossas-remote` profile for running tests as well) ++ +If you have not configured the Maven settings, to use the Red Hat Enterprise Maven repositories: ++ +---- +mvn clean package jboss-as:deploy -s TICKETMONSTER_MAVEN_PROJECT_ROOT/settings.xml +---- +3. This will deploy `target/ticket-monster.war` to the running instance of the server. +4. Now you can see the application running at http://localhost:8080/ticket-monster. + +== Using MySQL as the database + +You can deploy TicketMonster to JBoss EAP, making use of a 'real' database like MySQL, instead of the default in-memory H2 database. You can follow the procedure outlined as follows: + +. Install the MySQL JBDC driver as a new JBoss module. +.. Define a new JBoss module named `com.mysql` under the `modules` directory of the JBoss EAP installation. Under the `modules/system/layers/base` directory structure, create a directory named `com`, containing sub-directory named `mysql`, containing a sub-directory named `main`. Place the MySQL JBDC driver in the `main` directory. Finally, define the module via a `module.xml` file with the following contents: ++ +.EAP_HOME/system/layers/base/com/mysql/main/module.xml +---- + + + + + + + + + + + +---- ++ +This module declares the MySQL JDBC driver as a resource (from which to load classes) for the module. It also declares a dependency on the `javax.api` and `javax.transaction.api` modules, since the JDBC driver depends on classes from these modules. Remember to make corrections to the JBDC driver resource path, if you are using a driver JAR with a different name. +The JBoss EAP directory structure should now look like this: ++ +---- +modules ++---system +¦ +---layers +¦ ¦ +---base +¦ ¦ ¦ +---com +¦ ¦ ¦ ¦ +---mysql +¦ ¦ ¦ ¦ ¦ +---main +¦ ¦ ¦ ¦ ¦ ¦ +-------module.xml +¦ ¦ ¦ ¦ ¦ ¦ +-------mysql-connector-java-5.1.34-bin.jar +---- +. Register the MySQL datasource used by the application. Edit the server configuration file (`standalone.xml`), to add the datasource definition: ++ +---- + + + jdbc:mysql://localhost:3306/ticketmonster + com.mysql + TRANSACTION_READ_COMMITTED + + 10 + 100 + true + + + test + test + + + 32 + + + + + + com.mysql.jdbc.Driver + com.mysql.jdbc.jdbc2.optional.MysqlXADataSource + + + +---- ++ +Replace the values for the `connection-url`, `user-name` and `password` with the correct ones for your environment. +. Build and deploy the application, using the `mysql` profile defined in the project POM : +.. In JBoss Developer Studio, you can do this by opening the project's context menu: right-click on the project, click *Maven*→*Select Maven Profiles...*, and activate the `mysql` profile by selecting it's checkbox. Once you have activated the profile, you can publish the project to a JBoss EAP instance from JBoss Developer Studio in the same manner described previously. +.. If you are building and deploying from the command-line, activate the `mysql` profile, by specifying it during the build command like so: ++ +---- +mvn clean package jboss-as:deploy -Pmysql +---- +.. If you have not configured the Maven settings, to use the Red Hat Enterprise Maven repositories: ++ +---- +mvn clean package jboss-as:deploy -Pmysql -s TICKETMONSTER_MAVEN_PROJECT_ROOT/settings.xml +---- + + +== Using PostgreSQL as the database + +Just like MySQL, you can deploy TicketMonster to JBoss EAP, making use of a 'real' database like PostgreSQL, instead of the default in-memory H2 database. You can follow the procedure outlined as follows: + +. Install the PostgreSQL JBDC driver as a new JBoss module. +.. Define a new JBoss module named `com.mysql` under the `modules` directory of the JBoss EAP installation. Under the `modules/system/layers/base` directory structure, create a directory named `org`, containing sub-directory named `postgresql`, containing a sub-directory named `main`. Place the PostgreSQL JBDC driver in the `main` directory. Finally, define the module via a `module.xml` file with the following contents: ++ +.EAP_HOME/system/layers/base/com/mysql/main/module.xml +---- + + + + + + + + + + +---- ++ +This module declares the PostgreSQL JDBC driver as a resource (from which to load classes) for the module. It also declares a dependency on the `javax.api` and `javax.transaction.api` modules, since the JDBC driver depends on classes from these modules. Remember to make corrections to the JBDC driver resource path, if you are using a driver JAR with a different name. +The JBoss EAP directory structure should now look like this: ++ +---- +modules ++---system +¦ +---layers +¦ ¦ +---base +¦ ¦ ¦ +---org +¦ ¦ ¦ ¦ +---postgresql +¦ ¦ ¦ ¦ ¦ +---main +¦ ¦ ¦ ¦ ¦ ¦ +-------module.xml +¦ ¦ ¦ ¦ ¦ ¦ +-------postgresql-9.3-1102.jdbc4.jar +---- +. Register the PostgreSQL datasource used by the application. Edit the server configuration file (`standalone.xml`), to add the datasource definition: ++ +---- + + + jdbc:postgresql://localhost:5432/ticketmonster + org.postgresql + TRANSACTION_READ_COMMITTED + + 10 + 100 + true + + + test + test + + + 32 + + + + + + org.postgresql.xa.PGXADataSource + + + +---- ++ +Replace the values for the `connection-url`, `user-name` and `password` with the correct ones for your environment. +. Build and deploy the application, using the `postgresql` profile defined in the project POM : +.. In JBoss Developer Studio, you can do this by opening the project's context menu: right-click on the project, click *Maven*→*Select Maven Profiles...*, and activate the `postgresql` profile by selecting it's checkbox. Once you have activated the profile, you can publish the project to a JBoss EAP instance from JBoss Developer Studio in the same manner described previously. +.. If you are building and deploying from the command-line, activate the `postgresql` profile, by specifying it during the build command like so: ++ +---- +mvn clean package jboss-as:deploy -Ppostgresql +---- +.. If you have not configured the Maven settings, to use the Red Hat Enterprise Maven repositories: ++ +---- +mvn clean package jboss-as:deploy -Ppostgresql -s TICKETMONSTER_MAVEN_PROJECT_ROOT/settings.xml +---- \ No newline at end of file diff --git a/tutorial/OpenShiftDeployment.asciidoc b/tutorial/OpenShiftDeployment.asciidoc new file mode 100644 index 000000000..0b0ad08cf --- /dev/null +++ b/tutorial/OpenShiftDeployment.asciidoc @@ -0,0 +1,326 @@ +[[DeployingToOpenShift]] += Appendix B - Deploying to OpenShift +:Author: Vineet Reynolds + +== What Will You Learn Here? + +This appendix demonstrates how to import, develop and deploy the TicketMonster example using JBoss Developer Studio: + +* Obtain and import the TicketMonster example source code +* Deploy the application to OpenShift Online with OpenShift Tools + +== Import the Project source code + +Once the TicketMonster source code is obtained and unpackaged, you must import it into JBoss +Developer Studio, as detailed in the procedure below. TicketMonster is a Maven-based project so a +specific Import Maven Project wizard is used for the import. + +. Click *File*→*Import* to open the *Import* wizard. +. Expand *Maven*, select *Existing Maven Projects* and click *Next*. +. In the *Root Directory* field, enter the path to the TicketMonster source code. Alternatively, +click *Browse* to navigate to the source code location. The *Import Maven Project* wizard +recursively searches the path for a *pom.xml* file. The *pom.xml* file identifies the project as a +Maven project. The file is listed under *Projects* once it is found. ++ +.pom.xml File Listed in the Projects Pane +image::gfx/pom-file-projects-pane.png[] +. Click *Finish*. When the import process is complete, the project is listed in the *Project Explorer* view. + + +== Pre-requisites + +We will be pushing the TicketMonster sources to a git repository on OpenShift, where the application would be built and deployed. +The build on OpenShift, and hence the `git push` can timeout, since Maven dependencies will have to be fetched from the Red Hat Enterprise Maven repository or other repositories. + +We'll get around this drawback by configuring JBoss Developer Studio to not time out sooner. To do do, set the Git connection timeout to 300 seconds. Click *Window* → *Preferences*, expand *Team* and +select *Git*. In the *Remote connection timeout (seconds)* field, type *300* and click *Apply* and click *OK*. + +[[git-remote-connection-timeout]] +.Modify the git remote connection timeout +image::gfx/git-remote-connection-timeout.png[] + +== Deploying to OpenShift using JBoss Developer Studio + +To deploy TicketMonster to OpenShift Online, you must create a new OpenShift Online application based on the existing workspace project using OpenShift Tools, as detailed in the procedure below. + +[NOTE] +===================================================================================== +This procedure documents the deploying process for first-time OpenShift Online users. This +includes one-time steps, such as signing up for an OpenShift Online account, creating an +OpenShift Online domain and uploading SSH keys. If you have previously used OpenShift +Online and OpenShift Tools, omit the one-time steps as appropriate. +===================================================================================== + +. In JBoss Central, under Start from scratch, click OpenShift Application. +. Click the _Please sign up here_ link to create an OpenShift Online account and follow the +instructions on the OpenShift web page displayed in your default system web browser. Once +you have completed the sign-up process, restart the New OpenShift Application wizard. ++ +. Complete the fields about your OpenShift Online account as follows: +* From the Connection list, select New Connection. +* Ensure the Use default server check box is selected. +* In the Username and Password fields, type your account credentials. ++ +[[completed-username-password-fields]] +.Completed Username and Password Fields +image::gfx/completed-username-password-fields.png[] +. Click Next. +. In the *Domain Name* field, type a name for your new OpenShift Online domain and click Finish. The provided domain name must be unique across all domains on OpenShift Online; if it is not unique, you will be instructed to provide a unique domain name. +. From the Type list, select JBoss Enterprise Application Platform 6 (jbosseap-6). ++ +[[completed-fields-application-wizard]] +.Completed Fields in the New OpenShift Application Wizard +image::gfx/completed-fields-application-wizard.png[] +. Click Next. +. Complete the fields about the new OpenShift Online application as follows: +* In the Domain name field, select an existing OpenShift Online domain. +* In the Name field, type ticketmonster. +* From the Domain list, ensure the domain you have previously created is selected. +* From the Gear profile list, select small. ++ +[[completed-fields-application-wizard-step2]] +.Completed Fields in the New OpenShift Application Wizard +image::gfx/completed-fields-application-wizard-step2.png[] +. Click Next. +* Clear the Create a new project check box. +* In the Use existing project field, type ticket-monster. Alternatively, click Browse to select the ticket-monster project. +* Ensure the Create and set up a server for easy publishing check box is selected. ++ +[[completed-fields-application-wizard-step3]] +.Completed Fields in the New OpenShift Application Wizard +image::gfx/completed-fields-application-wizard-step3.png[] +. Click Next. +. Click SSH Keys wizard and click New. +. Complete the fields about the SSH Keys to be created as follows: +* In the Name field, type a name for the SSH key. +* From the Key Type list, ensure SSH_RSA is selected. +* In the SSH2 Home field, ensure your .ssh directory path is shown. +* In the Private Key File Name field, type a name for the private key file name. The Public Key File Name field populates automatically with the name of the private key file name with .pub appended. +. Click Finish. +. Click OK to close the Manage SSH Keys window. +. Click Finish to create the new OpenShift application based on the existing workspace ticket-monster project. This process may take some time to complete. ++ +[[new-openshift-application-wizard]] +.New OpenShift Application Wizard +image::gfx/new-openshift-application-wizard.png[] +. At the prompt stating OpenShift application ticketmonster will be enabled on project ticket-monster ..., click OK. This configures the workspace ticket-monster project for OpenShift and connects it to the OpenShift Online Git repository system used for version control. ++ +[[import-openShift-application-prompt]] +.Import OpenShift Application Prompt +image::gfx/import-openShift-application-prompt.png[] +. At the prompt stating the authenticity of the host cannot be established and asking if you are sure you want to continue connecting, verify the host information is correct and click Yes. +. At the prompt asking if you want to publish committed changes to OpenShift, click Yes. The Console view automatically becomes the view in focus and displays the output from the OpenShift Online server. Once the OpenShift Online ticketmonster application is created and deployed, the Console view displays the following message: ++ +`Deployment completed with status: success` ++ +[[completed-deployment-openshift]] +.New OpenShift Application Wizard +image::gfx/completed-deployment-openshift.png[] + + +== Deploying to OpenShift using the command-line + +To deploy TicketMonster to OpenShift Online, you must create a new OpenShift Online application based on the existing workspace project using OpenShift Tools, as detailed in the procedure below. + +[NOTE] +===================================================================================== +This procedure documents the deploying process for first-time OpenShift Online users. This +includes one-time steps, such as signing up for an OpenShift Online account, creating an +OpenShift Online domain and uploading SSH keys. If you have previously used OpenShift +Online and OpenShift Tools, omit the one-time steps as appropriate. +===================================================================================== + +=== Create an OpenShift Account and Domain + +If you do not yet have an OpenShift account and domain, https://openshift.com/[browse to OpenShift] to create the account and domain. + +https://openshift.redhat.com/app/getting_started[Get Started with OpenShift] details how to install the OpenShift Client tools. + +=== Create the OpenShift Application + +[NOTE] +===================================================================================== +The following variables are used in these instructions. Be sure to replace them as follows: + +* YOUR_DOMAIN_NAME should be replaced with the OpenShift domain name. +* APPLICATION_UUID should be replaced with the UUID generated by OpenShift for your application, for example: 52864af85973ca430200006f +* TICKETMONSTER_MAVEN_PROJECT_ROOT is the location of the Maven project sources for the TicketMonster application. +===================================================================================== + +Open a shell command prompt and change to a directory of your choice. Enter the following command to create a JBoss EAP 6 application: + +---- +rhc app create -a ticketmonster -t jbosseap-6 +---- + +[NOTE] +===================================================================================== +The domain name for this application will be `ticketmonster-YOUR_DOMAIN_NAME.rhcloud.com` +===================================================================================== + +This command creates an OpenShift application named ticketmonster and will run the application inside the jbosseap-6 container. You should see some output similar to the following: + +---- +Application Options +\------------------- +Domain: YOUR_DOMAIN +Cartridges: jbosseap-6 (addtl. costs may apply) +Gear Size: default +Scaling: no + +Creating application 'ticketmonster' ... done + + +Waiting for your DNS name to be available ... done + +Cloning into 'ticketmonster'... +Warning: Permanently added the RSA host key for IP address '54.90.10.115' to the list of known hosts. + +Your application 'ticketmonster' is now available. + + URL: http://ticketmonster-YOUR_DOMAIN.rhcloud.com/ + SSH to: APPLICATION_UUID@ticketmonster-YOURDOMAIN.rhcloud.com + Git remote: ssh://APPLICATION_UUID@ticketmonster-YOUR_DOMAIN.rhcloud.com/~/git/ticketmonster.git/ + Cloned to: /Users/vineet/openshiftapps/ticketmonster + +Run 'rhc show-app ticketmonster' for more details about your app. +---- + +The create command creates a git repository in the current directory with the same name as the application. + +You do not need the generated default application, so navigate to the new git repository directory created by the OpenShift command and tell git to remove the source and pom files: + +---- +cd ticketmonster +git rm -r src pom.xml +---- + +Copy the TicketMonster application sources into this new git repository: + +---- +cp -r TICKETMONSTER_MAVEN_PROJECT_ROOT/src . +cp -r TICKETMONSTER_MAVEN_PROJECT_ROOT/pom.xml . +---- + +You can now deploy the changes to your OpenShift application using git as follows: + +---- +git add src pom.xml +git commit -m "TicketMonster on OpenShift" +git push +---- + +The final push command triggers the OpenShift infrastructure to build and deploy the changes. + +Note that the `openshift` profile in pom.xml is activated by OpenShift, and causes the WAR build by OpenShift to be copied to the deployments/ directory, and deployed without a context path. + +Now you can see the application running at http://ticketmonster-YOUR_DOMAIN.rhcloud.com/. + +== Using MySQL as the database + +You can deploy TicketMonster to OpenShift, making use of a 'real' database like MySQL, instead of the default in-memory H2 database within the JBoss EAP cartridge. You can follow the procedure outlined as follows, to first deploy the TicketMonster application to a JBoss EAP cartridge, and to then add a : + +. Create the OpenShift application from the TicketMonster project sources, as described in the previous sections. +. Add the MySQL cartridge to the application. +.. If you are using JBoss Developer Studio, select the `ticketmonster` application in the *OpenShift Explorer* view. Open the context-menu by right-clicking on it, and navigate to the *Edit Embedded Cartridges...* menu item. ++ +[[edit-embedded-cartridge-openshift-for-mysql]] +.Edit Embedded Cartidges for an OpenShift application +image::gfx/edit-embedded-cartridge-openshift.png[] ++ +Select the MySQL 5.5 cartidge, and click *Finish*. ++ +[[add-mysql-embedded-cartridge]] +.Add MySQL cartridge +image::gfx/add-mysql-embedded-cartridge.png[] +.. If you are using the command-line, execute the following command, to add the MySQL 5.5 cartridge to the `ticketmonster` application: ++ +---- +rhc cartridge add mysql-5.5 -a ticketmonster +---- +. Configure the OpenShift build process, to use the `mysql-openshift` profile within the project POM. As you would know, the Maven build on OpenShift uses the `openshift` profile by default - this profile does not contain any instructions or configuration to create a WAR file with the JPA deployment descriptor for MySQL on OpenShift. The `mysql-openshift` profile contains this configuration. Since it is not activated during the build on OpenShift, we need to instruct OpenShift to use it as well. ++ +To do so, create a file named `pre_build_jbosseap` under the `.openshift/action_hooks` directory located in the git repository of the OpenShift application, with the following contents: ++ +.TICKET_MONSTER_OPENSHIFT_GIT_REPO/.openshift/build_hooks/pre_build_jbosseap +---- +export MAVEN_ARGS="clean package -Popenshift,mysql-openshift -DskipTests" +---- ++ +This OpenShift action hook sets up the `MAVEN_ARGS` environment variable used by OpenShift to configure the Maven build process. The exported variable now activates the `mysql-openshift` profile, in addition to the default values originally present in the variable. +. Publish the changes to OpenShift: +.. If you are using JBoss Developer Studio, right-click the project, go to *Team*→*Commit...* to commit the changes. Select the `pre_build_jbosseap` file to add to the commit. Choose the *Commit and Push* button during committing, to push the changes to the OpenShift repository. +.. If you are using the command line, add the `pre_build_jbosseap` file to the git index, and commit it, and push to the OpenShift repository, as follows: ++ +---- +cd +git add .openshift/build_hooks/pre_build_jbosseap +git commit -m "Added pre-build action hook for MySQL" +git push +---- + +[NOTE] +====== +On Windows, you will need to run the following command to set the executable bit to the `pre_build_jbosseap` file: + + git update-index --chmod=+x .openshift/build_hooks/pre_build_jbosseap + +This ensures the executable bit is recognized on OpenShift even though the file was committed in Windows. + +Since JBoss Developer Studio does not have a git console, you will need to run this from the command line. +====== + + +== Using PostgreSQL as the database + +You can deploy TicketMonster to OpenShift, making use of a 'real' database like PostgreSQL, instead of the default in-memory H2 database within the JBoss EAP cartridge. You can follow the procedure outlined as follows: + +. Create the OpenShift application from the TicketMonster project sources, as described in the previous sections. +. Add the PostgreSQL cartridge to the application. +.. If you are using JBoss Developer Studio, select the `ticketmonster` application in the *OpenShift Explorer* view. Open the context-menu by right-clicking on it, and navigate to the *Edit Embedded Cartridges...* menu item. ++ +[[edit-embedded-cartridge-openshift-for-postgres]] +.Edit Embedded Cartidges for an OpenShift application +image::gfx/edit-embedded-cartridge-openshift.png[] ++ +Select the PostgreSQL 9.2 cartidge, and click *Finish*. ++ +[[add-postgresql-embedded-cartridge]] +.Add PostgreSQL cartridge +image::gfx/add-postgresql-embedded-cartridge.png[] +.. If you are using the command-line, execute the following command, to add the PostgreSQL 9.2 cartridge to the `ticketmonster` application: ++ +------ +rhc cartridge add postgresql-9.2 -a ticketmonster +------ +. Configure the OpenShift build process, to use the `postgresql-openshift` profile within the project POM. As you would know, the Maven build on OpenShift uses the `openshift` profile by default - this profile does not contain any instructions or configuration to create a WAR file with the JPA deployment descriptor for MySQL on OpenShift. The `postgresql-openshift` profile contains this configuration. Since it is not activated during the build on OpenShift, we need to instruct OpenShift to use it as well. ++ +To do so, create a file named `pre_build_jbosseap` under the `.openshift/action_hooks` directory located in the git repository of the OpenShift application, with the following contents: ++ +.TICKET_MONSTER_OPENSHIFT_GIT_REPO/.openshift/build_hooks/pre_build_jbosseap +---- +export MAVEN_ARGS="clean package -Popenshift,postgresql-openshift -DskipTests" +---- ++ +This OpenShift action hook sets up the `MAVEN_ARGS` environment variable used by OpenShift to configure the Maven build process. The exported variable now activates the `postgresql-openshift` profile, in addition to the default values originally present in the variable. +. Publish the changes to OpenShift: +.. If you are using JBoss Developer Studio, right-click the project, go to *Team*→*Commit...* to commit the changes. Select the `pre_build_jbosseap` file to add to the commit. Choose the *Commit and Push* button during committing, to push the changes to the OpenShift repository. +.. If you are using the command line, add the `pre_build_jbosseap` file to the git index, and commit it, and push to the OpenShift repository, as follows: ++ +------ +cd +git add .openshift/build_hooks/pre_build_jbosseap +git commit -m "Added pre-build action hook for PostgreSQL" +git push +------ + +[NOTE] +====== +On Windows, you will need to run the following command to set the executable bit to the `pre_build_jbosseap` file: + + git update-index --chmod=+x .openshift/build_hooks/pre_build_jbosseap + +This ensures the executable bit is recognized on OpenShift even though the file was committed in Windows. + +Since JBoss Developer Studio does not have a git console, you will need to run this from the command line. +====== \ No newline at end of file diff --git a/tutorial/README.md b/tutorial/README.md new file mode 100644 index 000000000..143e19919 --- /dev/null +++ b/tutorial/README.md @@ -0,0 +1,6 @@ +What is this? +============= + +This is a series of tutorials that will walk you through the process of creating the TicketMonster application from end to end. + +For now, just browse clone the git repo, read CONTRIBUTING.md and run `./generate.sh` - GitHub doesn't process asciidoc very well! diff --git a/tutorial/UserFrontEnd.asciidoc b/tutorial/UserFrontEnd.asciidoc new file mode 100644 index 000000000..249e2080f --- /dev/null +++ b/tutorial/UserFrontEnd.asciidoc @@ -0,0 +1,2574 @@ += Building The User UI Using HTML5 +:Author: Marius Bogoevici +:thumbnail: http://static.jboss.org/ffe/1/www/origin/ticket-monster-splash-2.png + +== What Will You Learn Here? + + +We've just implemented the business services of our application, and exposed them through RESTful endpoints. Now we need to implement a flexible user interface that can be easily used with both desktop and mobile clients. After reading this tutorial, you will understand our front-end design and the choices that we made in its implementation. Topics covered include: + +* Creating single-page applications using HTML5, JavaScript and JSON +* Using JavaScript frameworks for invoking RESTful endpoints and manipulating page content +* Feature and device detection +* Implementing a version of the user interface that is optimized for mobile clients using JavaScript frameworks such as jQuery mobile + +The tutorial will show you how to perform all these steps in JBoss Developer Studio, including screenshots that guide you through. + +== First, the basics + + +In this tutorial, we will build a single-page application. All the necessary code: HTML, CSS and JavaScript is retrieved within a single page load. Rather than refreshing the page every time the user changes a view, the content of the page will be redrawn by manipulating the DOM in JavaScript. The application uses REST calls to retrieve data from the server. + +[[single-page-app_image]] +.Single page application +image::gfx/single-page-app.png[] + +=== Client-side MVC Support + + +Because this is a moderately complex example, which involves multiple views and different types of data, we will use a client-side MVC framework to structure the application, which provides amongst others: + +* routing support within the single page application; +* event-driven interaction between views and data; +* simplified CRUD invocations on RESTful services. + +In this application we use the client-side MVC framework "backbone.js". + +[[use-of-backbone_image]] +.Backbone architecture +image::gfx/backbone-usage.png[] + +=== Modularity + + +In order to provide good separation of concerns, we split the JavaScript code into modules. Ensuring that all the modules of the application are loaded properly at runtime becomes a complex task, as the application size increases. To conquer this complexity, we use the Asynchronous Module Definition mechanism as implemented by the "require.js" library. + +[TIP] +.Asynchronous Module Definition +======================================================================== +The Asynchronous Module Definition (AMD) API specifies a mechanism for defining modules such that the module, and its dependencies, can be asynchronously loaded. This is particularly well suited for the browser where synchronous loading of modules incurs performance, usability, debugging, and cross-domain access problems. +======================================================================== + +=== Templating + + +Instead of manipulating the DOM directly, and mixing up HTML with the JavaScript code, we create HTML markup fragments separately as templates which are applied when the application views are rendered. + +In this application we use the templating support provided by "underscore.js". + +=== Mobile and desktop versions + + +The page flow and structure, as well as feature set, are slightly different for mobile and desktop, and therefore we will build two variants of the single-page-application, one for desktop and one for mobile. As the application variants are very similar, we will cover the desktop version of the application first, and then we will explain what is different in the mobile version. + +== Setting up the structure + + +Before we start developing the user interface, we need to set up the general application structure and add the JavaScript libraries. First, we create the directory structure: + +[[ui-directory-structure]] +.File structure for our web application +image::gfx/ui-file-structure.png[] + +We put stylesheets in `resources/css` folder, images in `resources/img`, and HTML view templates in `resources/templates`. `resources/js` contains the JavaScript code, split between `resources/js/libs` - which contains the libraries used by the application, `resources/js/app` - which contains the application code, and `resources/js/configurations` which contains module definitions for the different versions of the application - i.e. mobile and desktop. The `resources/js/app` folder will contain the application modules, in subsequent subdirectories, for models, collections, routers and views. + +The first step in implementing our solution is adding the stylesheets and JavaScript libraries to the `resources/css` and `resources/js/libs`: + +require.js:: + AMD support, along with the plugin: +** text - for loading text files, in our case the HTML templates +jQuery:: + general purpose library for HTML traversal and manipulation +Underscore:: + JavaScript utility library (and a dependency of Backbone) +Backbone:: + Client-side MVC framework +Bootstrap:: + UI components and stylesheets for page structuring +Modernizr:: + JavaScript library for HTML5 and CSS3 feature detection + + +You can copy these libraries (with associated stylesheets) from the project sources. You can also copy the CSS stylesheet in `screen.css`, since we'll include this stylesheet in the HTML. Addtionally, copy the images from the `src/main/webapp/resources/img` directory in the project sources to the equivalent one in your workspace. + +Now, we create the main page of the application (which is the URL loaded by the browser): + +.src/main/webapp/index.html +[source,html] +------------------------------------------------------------------------------------------------------- + + + + Ticket Monster + + + + + + + + + +------------------------------------------------------------------------------------------------------- + +As you can see, the page does not contain much. It loads Modernizr (for HTML5 and CSS3 feature detection) and RequireJS (for loading JavaScript modules in an asynchronous manner). Once RequireJS is loaded by the browser, it will configure itself to use a `baseUrl` of `resources/js/configurations` (specified via the `data-main` attribute on the `script` tag). All scripts loaded by RequireJS will use this `baseUrl` unless specified otherwise. + +RequireJS will then load a script having a module ID of `loader` (again, specified via the `data-main` attribute): + +.src/main/webapp/resources/js/configurations/loader.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +//detect the appropriate module to load +define(function () { + + /* + A simple check on the client. For touch devices or small-resolution screens) + show the mobile client. By enabling the mobile client on a small-resolution screen + we allow for testing outside a mobile device (like for example the Mobile Browser + simulator in JBoss Tools and JBoss Developer Studio). + */ + + var environment; + + if (Modernizr.touch || Modernizr.mq("only all and (max-width: 480px)")) { + environment = "mobile" + } else { + environment = "desktop" + } + + require([environment]); +}); +------------------------------------------------------------------------------------------------------- + +This script detects the current client (mobile or desktop) based on its capabilities (touch or not) and loads another JavaScript module (`desktop` or `mobile`) defined in the `resources/js/configurations` folder (aka the `baseUrl`) depending on the detected features. In the case of the desktop client, the code is loaded from `resources/js/configurations/desktop.js`. + +.src/main/webapp/resources/js/configurations/desktop.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * Shortcut alias definitions - will come in handy when declaring dependencies + * Also, they allow you to keep the code free of any knowledge about library + * locations and versions + */ +requirejs.config({ + baseUrl: "resources/js", + paths: { + jquery:'libs/jquery-2.0.3', + underscore:'libs/underscore', + text:'libs/text', + bootstrap: 'libs/bootstrap', + backbone: 'libs/backbone', + utilities: 'app/utilities', + router:'app/router/desktop/router' + }, + // We shim Backbone.js and Underscore.js since they don't declare AMD modules + shim: { + 'backbone': { + deps: ['jquery', 'underscore'], + exports: 'Backbone' + }, + + 'underscore': { + exports: '_' + } + } +}); + +define("initializer", ["jquery"], + function ($) { + // Configure jQuery to append timestamps to requests, to bypass browser caches + // Important for MSIE + $.ajaxSetup({cache:false}); + $('head').append(''); + $('head').append(''); + $('head').append(''); + $('head').append(''); +}); + +// Now we load the dependencies +// This loads and runs the 'initializer' and 'router' modules. +require([ + 'initializer', + 'router' +], function(){ +}); + +define("configuration", { + baseUrl : "" +}); +------------------------------------------------------------------------------------------------------- + +The module loads all the utility libraries, converting them to AMD modules where necessary (like it is the case for Backbone). It also defines two modules of its own - an initializer that loads the application stylesheets for the page, and the `configuration` module that allows customizing the REST service URLs (this will become in handy in a further tutorial). + +We also define some utility JavaScript functions that are used in the rest of the front-end in a `utilities` module (also referenced in the `desktop` module above). For convenience, copy the `utilities.js` file from the `src/main/webapp/resources/js/app/` directory in the project sources. + +Before we add any functionality, let us create a first landing page. We will begin by setting up a critical piece of the application, the router. + +=== Routing + + +The router allows for navigation in our application via bookmarkable URLs, and we will define it as follows: + +.src/main/webapp/resources/js/app/router/desktop/router.js +[source, javascript] +------------------------------------------------------------------------------------------------------- +/** + * A module for the router of the desktop application + */ +define("router", [ + 'jquery', + 'underscore', + 'configuration', + 'utilities', + 'text!../templates/desktop/main.html' +],function ($, + _, + config, + utilities, + MainTemplate) { + + $(document).ready(new function() { + utilities.applyTemplate($('body'), MainTemplate) + }) + + /** + * The Router class contains all the routes within the application - + * i.e. URLs and the actions that will be taken as a result. + * + * @type {Router} + */ + + var Router = Backbone.Router.extend({ + initialize: function() { + //Begin dispatching routes + Backbone.history.start(); + }, + routes:{ + } + }); + + // Create a router instance + var router = new Router(); + + return router; +}); +------------------------------------------------------------------------------------------------------- + +Remember, this is a single page application. You can either navigate using urls such as `http://localhost:8080/ticket-monster/index.html#events` or using relative urls (from within the application, this being exactly what the main menu does). The fragment after the hash sign represents the url within the single page, on which the router will act, according to the mappings set up in the `routes` property. + +During the router set up, we load the page template for the entire application. TicketMonster uses a templating library in order to separate application logic from it's actual graphical content. The actual HTML is described in template files, which are applied by the application, when necessary, on a DOM element - effectively populating it's content. So the general content of the page, as described in the `body` element is described in a template file too. Let us define it. + +.src/main/webapp/resources/templates/desktop/main.html +------------------------------------------------------------------------------------------------------- + + +
    + +
    +
    +
    + +
    +
    HTML5
    +
    + +------------------------------------------------------------------------------------------------------- + +The actual HTML code of the template contains a menu definition which will be present on all the pages, as well as an empty element named `content`, which is the placeholder for the application views. When a view is displayed, it will apply a template and populate the `content` element. + +Setting up the initial views +---------------------------- + +Let us complete our application setup by creating an initial landing page. The first thing that we will need to do is to add a view component. + +.src/main/resources/js/app/views/desktop/home.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * The About view + */ +define([ + 'utilities', + 'text!../../../../templates/desktop/home.html' +], function (utilities, HomeTemplate) { + + var HomeView = Backbone.View.extend({ + render:function () { + utilities.applyTemplate($(this.el),HomeTemplate,{}); + return this; + } + }); + + return HomeView; +}); +------------------------------------------------------------------------------------------------------- + +Functionally, this is a very basic component - it only renders the splash page of the application, but it helps us +introduce a new concept that will be heavily used throughout the application views. One main role of a view is to +describe the logic for manipulating the page content. It will do so by defining a function named `render` which +will be invoked by the application. In this very simple case, all that the view does is to create the content of the splash page. +You can proceed by copying the content of `src/main/webapp/resources/templates/desktop/home.html` to your project. + + +[TIP] + +.Backbone Views +======================================================================== +Views are logical representations of user interface elements that can +interact with data components, such as models in an event-driven fashion. +Apart from defining the logical structure of your user interface, views handle +events resulting from the user interaction (e.g. clicking a DOM element or selecting +an element into a list), translating them into logical actions inside the +application. +======================================================================== + +Once we defined a view, we must tell the router to navigate to it whenever requested. We will add the following dependency and mapping to the router: + +.src/main/webapp/resources/js/app/router/desktop/router.js +[source, javascript] +------------------------------------------------------------------------------------------------------- +/** + * A module for the router of the desktop application + */ +define("router", [ + 'jquery', + 'underscore', + 'configuration', + 'utilities', + 'app/views/desktop/home', + 'text!../templates/desktop/main.html' +],function ($, + _, + config, + utilities, + HomeView, + MainTemplate) { + + ... + var Router = Backbone.Router.extend({ + ... + routes : { + "":"home", + "about":"home" + }, + home : function () { + utilities.viewManager.showView(new HomeView({el:$("#content")})); + } + }); + ... +------------------------------------------------------------------------------------------------------- + +We have just told the router to invoke the `home` function whenever the user navigates to the root of the application or +uses a `#about` hash. The method will simply cause the `HomeView` defined above to render. + +Now you can navigate to `http://localhost:8080/ticket-monster/#about` or `http://localhost:8080/ticket-monster` and see the results. + +== Displaying Events + + +The first use case that we implement is event navigation. The users will be able to view the list of events and select the one that they want to attend. After doing so, they will select a venue, and will be able to choose a performance date and time. + +=== The Event model + + +We define a Backbone model for holding event data. Nearly all domain entities (booking, event, venue) are represented by a corresponding Backbone model: + +.src/main/webapp/resources/js/app/models/event.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * Module for the Event model + */ +define([ + 'configuration', + 'backbone' +], function (config) { + /** + * The Event model class definition + * Used for CRUD operations against individual events + */ + var Event = Backbone.Model.extend({ + urlRoot: config.baseUrl + 'rest/events' // the URL for performing CRUD operations + }); + // export the Event class + return Event; +}); +------------------------------------------------------------------------------------------------------- + +The `Event` model can perform CRUD operations against the REST services we defined earlier. + +[TIP] +.Backbone Models +======================================================================== +Backbone models contain data as well as much of the logic surrounding +it: conversions, validations, computed properties, and access control. +They also perform CRUD operations with the REST service. +======================================================================== + +=== The Events collection + + +We define a Backbone collection for handling groups of events (like the events list): + +.src/main/webapp/resources/js/app/collections/events.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * Module for the Events collection + */ +define([ + // The collection element type and configuration are dependencies + 'app/models/event', + 'configuration' +], function (Event, config) { + /** + * Here we define the Bookings collection + * We will use it for CRUD operations on Bookings + */ + var Events = Backbone.Collection.extend({ + url: config.baseUrl + "rest/events", // the URL for performing CRUD operations + model: Event, + id:"id", // the 'id' property of the model is the identifier + comparator:function (model) { + return model.get('category').id; + } + }); + return Events; +}); +------------------------------------------------------------------------------------------------------- + +By mapping the model and collection to a REST endpoint you can perform CRUD operations without having to invoke the services explicitly. You will see how that works a bit later. + +[TIP] + +.Backbone Collections +======================================================================== +Collections are ordered sets of models. They can handle events which are +fired as a result of a change to a individual member, and can perform +CRUD operations for syncing up contents against RESTful services. +======================================================================== + +=== The EventsView view + +Now that we have implemented the data components of the example, we need to create the view that displays them. + +.src/main/webapp/resources/js/app/views/desktop/events.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +define([ + 'utilities', + 'bootstrap', + 'text!../../../../templates/desktop/events.html' +], function ( + utilities, + bootstrap, + eventsTemplate) { + + var EventsView = Backbone.View.extend({ + events:{ + "click a":"update" + }, + render:function () { + var categories = _.uniq( + _.map(this.model.models, function(model){ + return model.get('category') + }), false, function(item){ + return item.id + }); + utilities.applyTemplate($(this.el), eventsTemplate, {categories:categories, model:this.model}) + $(this.el).find('.item:first').addClass('active'); + $(".carousel").carousel(); + $("a[rel='popover']").popover({trigger:'hover',container:'body'}); + return this; + }, + update:function () { + $("a[rel='popover']").popover('hide') + } + }); + + return EventsView; +}); +------------------------------------------------------------------------------------------------------- + +As we explained, earlier, the view is attached to a DOM element (the `el` property). When the `render` method is invoked, it manipulates the DOM and renders the view. We could have achieved this by writing these instructions directly in the method, but that would make it hard to change the page design later on. Instead, we create a template and apply it, thus separating the HTML view code from the view implementation. Note the dependency on the Bootstrap module - we initialize the Bootstrap carousel and popover components when this view is rendered. + +.src/main/webapp/resources/templates/desktop/events.html +[source,html] +------------------------------------------------------------------------------------------------------- +
    +
    +
    + + <% + _.each(categories, function (category) { + %> +
    + +
    +
    + + <% + _.each(model.models, function (model) { + if (model.get('category').id == category.id) { + %> +

    <%=model.attributes.name%>

    + <% } + }); + %> +
    +
    +
    + <% }); %> +
    +
    + + +
    +------------------------------------------------------------------------------------------------------- + + +As well as applying the template and preparing the data that will be used to fill it in (the `categories` and `model` entries in the map), the `render` method also performs the JavaScript calls that are required to initialize the UI components (in this case the Bootstrap carousel and popover). + +A view can also listen to events fired by the children of it's root element (`el`). In this case, the `update` method is configured to listen to clicks on anchors. The configuration occurs within the `events` property of the class. + +Now that the views are in place, we need to add another routing rule to the application. + +.src/main/webapp/resources/js/app/router/desktop/router.js +[source, javascript] +------------------------------------------------------------------------------------------------------- +/** + * A module for the router of the desktop application + */ +define("router", [ + ... + 'utilities', + 'app/collections/events', + 'app/views/desktop/home', + 'app/views/desktop/events', + ... + 'text!../templates/desktop/main.html' +],function ($, + ... + utilities, + Events, + HomeView, + EventsView, + ... + MainTemplate) { + + var Router = Backbone.Router.extend({ + ... + routes : { + ..., + "events":"events" + }, + ..., + events:function () { + var events = new Events(); + var eventsView = new EventsView({model:events, el:$("#content")}); + events.on("reset", + function () { + utilities.viewManager.showView(eventsView); + }).fetch({ + reset : true, + error : function() { + utilities.displayAlert("Failed to retrieve events from the TicketMonster server."); + } + }); + } + }); +------------------------------------------------------------------------------------------------------- + +The `events` function handles the `#events` fragment and will retrieve the events in our application via a REST call. We don't manually perform the REST call as it is triggered the by invocation of `fetch` on the `Events` collection, as discussed earlier. + +The `reset` event on the collection is invoked when the data from the server is received, and the collection is populated. This triggers the rendering of the events view (which is bound to the `#content` div). + +The whole process is event orientated - the models, views and controllers interact through events. + +== Viewing a single event + + +With the events list view now in place, we can add a view to display the details of each individual event, allowing the user to select a venue and performance time. + +We already have the models in place so all we need to do is to create the additional view and expand the router. First, we'll implement the view: + +.src/main/webapp/resources/js/app/views/desktop/event-detail.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +define([ + 'utilities', + 'require', + 'text!../../../../templates/desktop/event-detail.html', + 'text!../../../../templates/desktop/media.html', + 'text!../../../../templates/desktop/event-venue-description.html', + 'configuration', + 'bootstrap' +], function ( + utilities, + require, + eventDetailTemplate, + mediaTemplate, + eventVenueDescriptionTemplate, + config, + Bootstrap) { + + var EventDetail = Backbone.View.extend({ + + events:{ + "click input[name='bookButton']":"beginBooking", + "change select[id='venueSelector']":"refreshShows", + "change select[id='dayPicker']":"refreshTimes" + }, + + render:function () { + $(this.el).empty() + utilities.applyTemplate($(this.el), eventDetailTemplate, this.model.attributes); + $("#bookingOption").hide(); + $("#venueSelector").attr('disabled', true); + $("#dayPicker").empty(); + $("#dayPicker").attr('disabled', true) + $("#performanceTimes").empty(); + $("#performanceTimes").attr('disabled', true) + var self = this + $.getJSON(config.baseUrl + "rest/shows?event=" + this.model.get('id'), function (shows) { + self.shows = shows + $("#venueSelector").empty().append(""); + $.each(shows, function (i, show) { + $("#venueSelector").append("") + }); + $("#venueSelector").removeAttr('disabled') + }) + return this; + }, + beginBooking:function () { + require("router").navigate('/book/' + $("#venueSelector option:selected").val() + '/' + $("#performanceTimes").val(), true) + }, + refreshShows:function (event) { + event.stopPropagation(); + $("#dayPicker").empty(); + + var selectedShowId = event.currentTarget.value; + + if (selectedShowId != 0) { + var selectedShow = _.find(this.shows, function (show) { + return show.id == selectedShowId + }); + this.selectedShow = selectedShow; + utilities.applyTemplate($("#eventVenueDescription"), eventVenueDescriptionTemplate, {venue:selectedShow.venue}); + var times = _.uniq(_.sortBy(_.map(selectedShow.performances, function (performance) { + return (new Date(performance.date).withoutTimeOfDay()).getTime() + }), function (item) { + return item + })); + utilities.applyTemplate($("#venueMedia"), mediaTemplate, selectedShow.venue) + $("#dayPicker").removeAttr('disabled') + $("#performanceTimes").removeAttr('disabled') + _.each(times, function (time) { + var date = new Date(time) + $("#dayPicker").append("") + }); + this.refreshTimes() + $("#bookingWhen").show(100) + } else { + $("#bookingWhen").hide(100) + $("#bookingOption").hide() + $("#dayPicker").empty() + $("#venueMedia").empty() + $("#eventVenueDescription").empty() + $("#dayPicker").attr('disabled', true) + $("#performanceTimes").empty() + $("#performanceTimes").attr('disabled', true) + } + + }, + refreshTimes:function () { + var selectedDate = $("#dayPicker").val(); + $("#performanceTimes").empty() + if (selectedDate) { + $.each(this.selectedShow.performances, function (i, performance) { + var performanceDate = new Date(performance.date); + if (_.isEqual(performanceDate.toYMD(), selectedDate)) { + $("#performanceTimes").append("") + } + }) + } + $("#bookingOption").show() + } + + }); + + return EventDetail; +}); +------------------------------------------------------------------------------------------------------- + +This view is more complex than the global events view, as portions of the page need to be updated when the user chooses a venue. + +[[ui-event-detail]] +.On the event details page some fragments are re-rendered when the user selects a venue +image::gfx/ui-event-details.png[] + +The view responds to three different events: + +* changing the current venue triggers a reload of the venue details and the venue image, as well as the performance times. The application retrieves the performance times through a REST call. +* changing the day of the performance causes the performance time selector to reload. +* once the venue and performance date and time have been selected, the user can navigate to the booking page. + +The corresponding templates for the three fragments rendered above are: + +.src/main/webapp/resources/templates/desktop/event-detail.html +[source,html] +------------------------------------------------------------------------------------------------------- +
    + +
    +
    +
    +
    +
    + + <% if(mediaItem) { %><% } %> +
    +
    +
    <%= description %>
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +------------------------------------------------------------------------------------------------------- + +.src/main/webapp/resources/templates/desktop/event-venue-description.html +[source,html] +------------------------------------------------------------------------------------------------------- +
    +

    <%= venue.description %>

    +

    Address:

    +

    <%= venue.address.street %>

    +

    <%= venue.address.city %>, <%= venue.address.country %>

    +
    +------------------------------------------------------------------------------------------------------- + +.src/main/webapp/resources/templates/desktop/media.html +[source,html] +------------------------------------------------------------------------------------------------------- +<%if (mediaItem) { %><% } %> +------------------------------------------------------------------------------------------------------- + + +Now that the view exists, we add it to the router: + +.src/main/webapp/resources/js/app/router/desktop/router.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * A module for the router of the desktop application + */ +define("router", [ + ... + 'app/models/event', + ..., + 'app/views/desktop/event-detail', + ... +],function ( + ... + Event, + ... + EventDetailView, + ...) { + + var Router = Backbone.Router.extend({ + ... + routes:{ + ... + "events/:id":"eventDetail", + }, + ... + eventDetail:function (id) { + var model = new Event({id:id}); + var eventDetailView = new EventDetailView({model:model, el:$("#content")}); + model.on("change", + function () { + utilities.viewManager.showView(eventDetailView); + }).fetch({ + error : function() { + utilities.displayAlert("Failed to retrieve the event from the TicketMonster server."); + } + }); + } + } + ... +); +------------------------------------------------------------------------------------------------------- + +As you can see, this is very similar to the previous view and route, except that now the application can accept parameterized URLs (e.g. `http://localhost:8080/ticket-monster/index#events/1`). This URL can be entered directly into the browser, or it can be navigated to as a relative path (e.g. `#events/1`) from within the applicaton. + +With this in place, all that remains is to implement the final view of this use case, creating the bookings. + +== Creating Bookings + + +The user has chosen the event, the venue and the performance time, and must now create the booking. Users can select one of the available sections for the show's venue, and then enter the number of tickets required for each category available for this show (Adult, Child, etc.). They then add the tickets to the current order, which causes the summary view to be updated. Users can also remove tickets from the order. When the order is complete, they enter their contact information (e-mail address) and submit the order to the server. + +First, we add the new view: + +.src/main/webapp/resources/js/app/views/desktop/create-booking.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +define([ + 'utilities', + 'require', + 'configuration', + 'text!../../../../templates/desktop/booking-confirmation.html', + 'text!../../../../templates/desktop/create-booking.html', + 'text!../../../../templates/desktop/ticket-categories.html', + 'text!../../../../templates/desktop/ticket-summary-view.html', + 'bootstrap' +],function ( + utilities, + require, + config, + bookingConfirmationTemplate, + createBookingTemplate, + ticketEntriesTemplate, + ticketSummaryViewTemplate){ + + + var TicketCategoriesView = Backbone.View.extend({ + id:'categoriesView', + intervalDuration : 100, + formValues : [], + events:{ + "change input":"onChange" + }, + render:function () { + if (this.model != null) { + var ticketPrices = _.map(this.model, function (item) { + return item.ticketPrice; + }); + utilities.applyTemplate($(this.el), ticketEntriesTemplate, {ticketPrices:ticketPrices}); + } else { + $(this.el).empty(); + } + this.watchForm(); + return this; + }, + onChange:function (event) { + var value = event.currentTarget.value; + var ticketPriceId = $(event.currentTarget).data("tm-id"); + var modifiedModelEntry = _.find(this.model, function (item) { + return item.ticketPrice.id == ticketPriceId + }); + // update model + if ($.isNumeric(value) && value > 0) { + modifiedModelEntry.quantity = parseInt(value); + } + else { + delete modifiedModelEntry.quantity; + } + // display error messages + if (value.length > 0 && + (!$.isNumeric(value) // is a non-number, other than empty string + || value <= 0 // is negative + || parseFloat(value) != parseInt(value))) { // is not an integer + $("#error-input-"+ticketPriceId).empty().append("Please enter a positive integer value"); + $("#ticket-category-fieldset-"+ticketPriceId).addClass("error"); + } else { + $("#error-input-"+ticketPriceId).empty(); + $("#ticket-category-fieldset-"+ticketPriceId).removeClass("error"); + } + // are there any outstanding errors after this update? + // if yes, disable the input button + if ( + $("div[id^='ticket-category-fieldset-']").hasClass("error") || + _.isUndefined(modifiedModelEntry.quantity) ) { + $("input[name='add']").attr("disabled", true) + } else { + $("input[name='add']").removeAttr("disabled") + } + }, + watchForm: function() { + if($("#sectionSelectorPlaceholder").length) { + var self = this; + $("input[name*='tickets']").each( function(index,element) { + if(element.value !== self.formValues[element.name]) { + self.formValues[element.name] = element.value; + $("input[name='"+element.name+"']").change(); + } + }); + this.timerObject = setTimeout(function() { + self.watchForm(); + }, this.intervalDuration); + } else { + this.onClose(); + } + }, + onClose: function() { + if(this.timerObject) { + clearTimeout(this.timerObject); + delete this.timerObject; + } + } + }); + + var TicketSummaryView = Backbone.View.extend({ + tagName:'tr', + events:{ + "click i":"removeEntry" + }, + render:function () { + var self = this; + utilities.applyTemplate($(this.el), ticketSummaryViewTemplate, this.model.bookingRequest); + }, + removeEntry:function () { + this.model.bookingRequest.tickets.splice(this.model.index, 1); + } + }); + + var CreateBookingView = Backbone.View.extend({ + + intervalDuration : 100, + formValues : [], + events:{ + "click input[name='submit']":"save", + "change select[id='sectionSelect']":"refreshPrices", + "keyup #email":"updateEmail", + "change #email":"updateEmail", + "click input[name='add']":"addQuantities", + "click i":"updateQuantities" + }, + render:function () { + + var self = this; + $.getJSON(config.baseUrl + "rest/shows/" + this.model.showId, function (selectedShow) { + + self.currentPerformance = _.find(selectedShow.performances, function (item) { + return item.id == self.model.performanceId; + }); + + var id = function (item) {return item.id;}; + // prepare a list of sections to populate the dropdown + var sections = _.uniq(_.sortBy(_.pluck(selectedShow.ticketPrices, 'section'), id), true, id); + utilities.applyTemplate($(self.el), createBookingTemplate, { + sections:sections, + show:selectedShow, + performance:self.currentPerformance}); + self.ticketCategoriesView = new TicketCategoriesView({model:{}, el:$("#ticketCategoriesViewPlaceholder") }); + self.ticketSummaryView = new TicketSummaryView({model:self.model, el:$("#ticketSummaryView")}); + self.show = selectedShow; + self.ticketCategoriesView.render(); + self.ticketSummaryView.render(); + $("#sectionSelector").change(); + self.watchForm(); + }); + return this; + }, + refreshPrices:function (event) { + var ticketPrices = _.filter(this.show.ticketPrices, function (item) { + return item.section.id == event.currentTarget.value; + }); + var sortedTicketPrices = _.sortBy(ticketPrices, function(ticketPrice) { + return ticketPrice.ticketCategory.description; + }); + var ticketPriceInputs = new Array(); + _.each(sortedTicketPrices, function (ticketPrice) { + ticketPriceInputs.push({ticketPrice:ticketPrice}); + }); + this.ticketCategoriesView.model = ticketPriceInputs; + this.ticketCategoriesView.render(); + }, + save:function (event) { + var bookingRequest = {ticketRequests:[]}; + var self = this; + bookingRequest.ticketRequests = _.map(this.model.bookingRequest.tickets, function (ticket) { + return {ticketPrice:ticket.ticketPrice.id, quantity:ticket.quantity} + }); + bookingRequest.email = this.model.bookingRequest.email; + bookingRequest.performance = this.model.performanceId + $("input[name='submit']").attr("disabled", true) + $.ajax({url: (config.baseUrl + "rest/bookings"), + data:JSON.stringify(bookingRequest), + type:"POST", + dataType:"json", + contentType:"application/json", + success:function (booking) { + this.model = {} + $.getJSON(config.baseUrl +'rest/shows/performance/' + booking.performance.id, function (retrievedPerformance) { + utilities.applyTemplate($(self.el), bookingConfirmationTemplate, {booking:booking, performance:retrievedPerformance }) + }); + }}).error(function (error) { + if (error.status == 400 || error.status == 409) { + var errors = $.parseJSON(error.responseText).errors; + _.each(errors, function (errorMessage) { + $("#request-summary").append('
    ×Error! ' + errorMessage + '
    ') + }); + } else { + $("#request-summary").append('
    ×Error! An error has occured
    ') + } + $("input[name='submit']").removeAttr("disabled"); + }) + + }, + addQuantities:function () { + var self = this; + _.each(this.ticketCategoriesView.model, function (model) { + if (model.quantity != undefined) { + var found = false; + _.each(self.model.bookingRequest.tickets, function (ticket) { + if (ticket.ticketPrice.id == model.ticketPrice.id) { + ticket.quantity += model.quantity; + found = true; + } + }); + if (!found) { + self.model.bookingRequest.tickets.push({ticketPrice:model.ticketPrice, quantity:model.quantity}); + } + } + }); + this.ticketCategoriesView.model = null; + $('option:selected', 'select').removeAttr('selected'); + this.ticketCategoriesView.render(); + this.updateQuantities(); + }, + updateQuantities:function () { + // make sure that tickets are sorted by section and ticket category + this.model.bookingRequest.tickets.sort(function (t1, t2) { + if (t1.ticketPrice.section.id != t2.ticketPrice.section.id) { + return t1.ticketPrice.section.id - t2.ticketPrice.section.id; + } + else { + return t1.ticketPrice.ticketCategory.id - t2.ticketPrice.ticketCategory.id; + } + }); + + this.model.bookingRequest.totals = _.reduce(this.model.bookingRequest.tickets, function (totals, ticketRequest) { + return { + tickets:totals.tickets + ticketRequest.quantity, + price:totals.price + ticketRequest.quantity * ticketRequest.ticketPrice.price + }; + }, {tickets:0, price:0.0}); + + this.ticketSummaryView.render(); + this.setCheckoutStatus(); + }, + updateEmail:function (event) { + if ($(event.currentTarget).is(':valid')) { + this.model.bookingRequest.email = event.currentTarget.value; + $("#error-email").empty(); + } else { + $("#error-email").empty().append("Please enter a valid e-mail address"); + delete this.model.bookingRequest.email; + } + this.setCheckoutStatus(); + }, + setCheckoutStatus:function () { + if (this.model.bookingRequest.totals != undefined && this.model.bookingRequest.totals.tickets > 0 && this.model.bookingRequest.email != undefined && this.model.bookingRequest.email != '') { + $('input[name="submit"]').removeAttr('disabled'); + } + else { + $('input[name="submit"]').attr('disabled', true); + } + }, + watchForm: function() { + if($("#email").length) { + var self = this; + var element = $("#email"); + if(element.val() !== self.formValues["email"]) { + self.formValues["email"] = element.val(); + $("#email").change(); + } + this.timerObject = setTimeout(function() { + self.watchForm(); + }, this.intervalDuration); + } else { + this.onClose(); + } + }, + onClose: function() { + if(this.timerObject) { + clearTimeout(this.timerObject); + delete this.timerObject; + } + this.ticketCategoriesView.close(); + } + }); + + return CreateBookingView; +}); +------------------------------------------------------------------------------------------------------- + +The code above may be surprising! After all, we said that we were going to add a single view, but instead, we added three! This view makes use of two subviews (`TicketCategoriesView` and `TicketSummaryView`) for re-rendering parts of the main view. Whenever the user changes the current section, the list of available tickets is updated. Whenever the user adds the tickets to the booking, the booking summary is re-rendered. Changes in quantities or the target email may enable or disable the submission button - the booking is validated whenever changes to it are made. We do not create separate modules for the subviews, since they are not referenced outside the module itself. + +The booking submission is handled by the `save` method which constructs a JSON object, as required by a POST to `http://localhost:8080/ticket-monster/rest/bookings`, and performs the AJAX call. In case of a successful response, a confirmation view is rendered. On failure, a warning is displayed and the user may continue to edit the form. + +The corresponding templates for the views above are shown below: + +.src/main/webapp/resources/templates/desktop/booking-confirmation.html +[source,html] +------------------------------------------------------------------------------------------------------- +
    +

    Booking #<%=booking.id%> confirmed!

    +
    +
    +
    +
    + +

    Email: <%= booking.contactEmail %>

    +

    Event: <%= performance.event.name %>

    +

    Venue: <%= performance.venue.name %>

    +

    Date: <%= new Date(booking.performance.date).toPrettyString() %>

    +

    Created on: <%= new Date(booking.createdOn).toPrettyString() %>

    +
    +
    +
    +
    + + + + + + + + + + + + + <% $.each(_.sortBy(booking.tickets, function(ticket) {return ticket.id}), function (i, ticket) { %> + + + + + + + + <% }) %> + +
    Ticket #CategorySectionRowSeat
    <%= ticket.id %><%=ticket.ticketCategory.description%><%=ticket.seat.section.name%><%=ticket.seat.rowNumber%><%=ticket.seat.number%>
    +
    +
    +
    +
    + +
    +------------------------------------------------------------------------------------------------------- + +.src/main/webapp/resources/templates/desktop/create-booking.html +[source,html] +------------------------------------------------------------------------------------------------------- +
    +
    +

    <%=show.event.name%> + <%=show.venue.name%>, <%=new Date(performance.date).toPrettyString()%>

    +

    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +

    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +------------------------------------------------------------------------------------------------------- + +.src/main/webapp/resources/templates/desktop/ticket-categories.html +[source,html] +------------------------------------------------------------------------------------------------------- +<% if (ticketPrices.length > 0) { %> +
    + <% _.each(ticketPrices, function(ticketPrice) { %> +
    + + +
    +
    + + @ $<%=ticketPrice.price%> + +

    +
    +
    +
    + <% }) %> + +

     

    + +
    +
    + +
    +
    +
    + +<% } %> +------------------------------------------------------------------------------------------------------- + +.src/main/webapp/resources/templates/desktop/ticket-summary-view.html +[source,html] +------------------------------------------------------------------------------------------------------- +
    + <% if (tickets.length>0) { %> + + + + + + + + + + + + + + + <% _.each(tickets, function (ticketRequest, index, tickets) { %> + + + + + + + + <% }); %> + +
    Requested tickets
    SectionCategoryQuantityPrice
    <%= ticketRequest.ticketPrice.section.name %><%= ticketRequest.ticketPrice.ticketCategory.description %><%= ticketRequest.quantity %>$<%=ticketRequest.ticketPrice.price%>
    +

    +

    +
    Total ticket count: <%= totals.tickets %>
    +
    Total price: $<%=totals.price%>
    +
    + <% } else { %> + No tickets requested. + <% } %> +
    +------------------------------------------------------------------------------------------------------- + +Finally, once the view is available, we can add it's corresponding routing rule: + +.src/main/webapp/resources/js/app/router/desktop/router.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * A module for the router of the desktop application + */ +define("router", [ + ... + 'app/views/desktop/create-booking', + ... +],function ( + ... + CreateBookingView, + ... + ) { + + var Router = Backbone.Router.extend({ + ... + routes:{ + ... + "book/:showId/:performanceId":"bookTickets", + }, + ... + bookTickets:function (showId, performanceId) { + var createBookingView = + new CreateBookingView({ + model:{ showId:showId, + performanceId:performanceId, + bookingRequest:{tickets:[]}}, + el:$("#content") + }); + utilities.viewManager.showView(createBookingView); + } + } + ... +); +------------------------------------------------------------------------------------------------------- + +This concludes the implementation of the booking use case. We started by listing the available events, continued by selecting a venue and performance time, and ended by choosing tickets and completing the order. + +The other use cases: a booking starting from venues and view existing bookings are conceptually similar, so you can just copy the logic for the following routes from `src/main/webapp/resources/js/app/routers/desktop/router.js`: + +* `venues` +* `venues/:id` +* `bookings` +* `bookings/:id` + +Finally, copy the following files in the `src/main/webapp/resources/js/app/models`, `src/main/webapp/resources/js/app/collections`, +`src/main/webapp/resources/js/app/views/desktop` and `src/main/webapp/resources/templates`: + +* `src/main/webapp/resources/js/app/models/booking.js` +* `src/main/webapp/resources/js/app/models/venue.js` +* `src/main/webapp/resources/js/app/collections/bookings.js` +* `src/main/webapp/resources/js/app/collections/venues.js` +* `src/main/webapp/resources/js/app/views/desktop/bookings.js` +* `src/main/webapp/resources/js/app/views/desktop/booking-detail.js` +* `src/main/webapp/resources/js/app/views/desktop/venues.js` +* `src/main/webapp/resources/js/app/views/desktop/venue-detail.js` +* `src/main/webapp/resources/templates/desktop/booking-details.html` +* `src/main/webapp/resources/templates/desktop/booking-table.html` +* `src/main/webapp/resources/templates/desktop/venues.html` +* `src/main/webapp/resources/templates/desktop/venue-detail.html` +* `src/main/webapp/resources/templates/desktop/venue-event-description.html` + + +== Mobile view + + +The mobile version of the application uses approximately the same architecture as the desktop version. Any differences are due to the functional changes in the mobile version and the use of jQuery mobile. + +=== Setting up the structure + + +The first step in implementing our solution is to copy the CSS and JavaScript libraries to `resources/css` and `resources/js/libs`: + +require.js:: + AMD support, along with the plugin: +** text - for loading text files, in our case the HTML templates +jQuery:: + general purpose library for HTML traversal and manipulation +Underscore:: + JavaScript utility library (and a dependency of Backbone) +Backbone:: + Client-side MVC framework +jQuery Mobile:: + user interface system for mobile devices; + +(If you have already built the desktop application, some files may already be in place.) + +For mobile clients, the main page will display the mobile version of the application, by loading the mobile AMD module of the application. Let us create it. + +.src/main/webapp/resources/js/configurations/mobile.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * Shortcut alias definitions - will come in handy when declaring dependencies + * Also, they allow you to keep the code free of any knowledge about library + * locations and versions + */ +require.config({ + baseUrl:"resources/js", + paths: { + jquery:'libs/jquery-2.0.3', + jquerymobile:'libs/jquery.mobile-1.4.2', + text:'libs/text', + underscore:'libs/underscore', + backbone: 'libs/backbone', + utilities: 'app/utilities', + router:'app/router/mobile/router' + }, + // We shim Backbone.js and Underscore.js since they don't declare AMD modules + shim: { + 'backbone': { + deps: ['underscore', 'jquery'], + exports: 'Backbone' + }, + + 'underscore': { + exports: '_' + } + } +}); + +define("configuration", function() { + if (window.TicketMonster != undefined && TicketMonster.config != undefined) { + return { + baseUrl: TicketMonster.config.baseRESTUrl + }; + } else { + return { + baseUrl: "" + }; + } +}); + +define("initializer", [ + 'jquery', + 'utilities', + 'text!../templates/mobile/main.html' +], function ($, + utilities, + MainTemplate) { + // Configure jQuery to append timestamps to requests, to bypass browser caches + // Important for MSIE + $.ajaxSetup({cache:false}); + $('head').append(''); + $('head').append(''); + // Bind to mobileinit before loading jQueryMobile + $(document).bind("mobileinit", function () { + // Prior to creating and starting the router, we disable jQuery Mobile's own routing mechanism + $.mobile.hashListeningEnabled = false; + $.mobile.linkBindingEnabled = false; + $.mobile.pushStateEnabled = false; + + // Fix jQueryMobile header and footer positioning issues for iOS. + // See: https://github.com/jquery/jquery-mobile/issues/4113 and + // https://github.com/jquery/jquery-mobile/issues/5532 + $(document).on('blur', 'input, textarea, select', function() { + setTimeout(function() { + window.scrollTo(document.body.scrollLeft, document.body.scrollTop); + }, 0); + }); + + utilities.applyTemplate($('body'), MainTemplate); + }); + // Then (load jQueryMobile and) start the router to finally start the app + require(['router']); +}); + +// Now we declare all the dependencies +// This loads and runs the 'initializer' module. +require(['initializer']); +------------------------------------------------------------------------------------------------------- + +In this application, we combine Backbone and jQuery Mobile. Each framework has its own strengths; jQuery Mobile provides UI components and touch support, whilst Backbone provides MVC support. There is some overlap between the two, as jQuery Mobile provides its own navigation mechanism which we disable. + +We also define a `configuration` module which allows the customization of the base URLs for RESTful invocations. This module does not play any role in the mobile web version. We will come to it, however, when discussing hybrid applications. + +We also define a special initializer module (`initializer`) that, when loaded, adds the stylesheets and applies the template for the general structure of the page in the `body` element. In the initializer module we make customizations in order to get the two frameworks working together - disabling the jQuery Mobile navigation. Let us add the template definition for the template loaded by the initializer module. + +.src/main/webapp/resources/templates/mobile/main.html +[source,html] +------------------------------------------------------------------------------------------------------- + +
    +------------------------------------------------------------------------------------------------------- + +Copy over the `m.screen.css` file referenced in the `initializer` module, from the project sources, to the appropriate location in the workspace. + +Next, we create the application router. + +.src/main/webapp/resources/js/app/router/mobile/router.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * A module for the router of the mobile application. + * + */ +define("router",[ + 'jquery', + 'jquerymobile', + 'underscore', + 'utilities' +],function ($, + jqm, + _, + utilities) { + + /** + * The Router class contains all the routes within the application - i.e. URLs and the actions + * that will be taken as a result. + * + * @type {Router} + */ + var Router = Backbone.Router.extend({ + initialize: function() { + //Begin dispatching routes + Backbone.history.start(); + }, + execute : function(callback, args) { + $.mobile.loading("show"); + window.setTimeout(function() { + if (callback) { + callback.apply(this, args); + } + $.mobile.loading("hide"); + }, 300); + } + }); + + // Create a router instance + var router = new Router(); + + return router; +}); +------------------------------------------------------------------------------------------------------- + +In the router code we add the `execute` method to the router for handling transitions between routes. Here, we will display the jQuery Mobile loader widget before displaying any Backbone view, and then hide it once the view is rendered. + +Next, we need to create a first page. + +=== The landing page + +The first page in our application is the landing page. First, we add the template for it: + +.src/main/webapp/resources/templates/mobile/home-view.html +[source,html] +------------------------------------------------------------------------------------------------------- +
    +

    Ticket Monster

    +
    +
    + +

    Find events

    + +
    +------------------------------------------------------------------------------------------------------- + +Now we have to add the page to the router: + +.src/main/webapp/resources/js/app/router/mobile/router.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * A module for the router of the mobile application. + * + */ +define("router",[ + ... + 'text!../templates/mobile/home-view.html' +],function ( + ... + HomeViewTemplate) { + + ... + var Router = Backbone.Router.extend({ + ... + routes:{ + "":"home" + }, + ... + home:function () { + utilities.applyTemplate($("#container"), HomeViewTemplate); + $("#container").enhanceWithin(); + } + }); + ... +}); +------------------------------------------------------------------------------------------------------- + +Because jQuery Mobile navigation is disabled, we must tell jQuery Mobile explicitly to enhance the page content in order to create the mobile view. Here, we enhance the page using the `enhanceWithin` method, to ensure that the page gets the appropriate look and feel. + +=== The events view + + +First, we display a list of events (just as in the desktop view). Since mobile interfaces are more constrained, we will just show a simple list view: + +.src/main/webapp/resources/js/app/views/mobile/events.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +define([ + 'utilities', + 'text!../../../../templates/mobile/events.html' +], function ( + utilities, + eventsView) { + + var EventsView = Backbone.View.extend({ + render:function () { + var categories = _.uniq( + _.map(this.model.models, function(model){ + return model.get('category'); + }), false, function(item){ + return item.id; + }); + utilities.applyTemplate($(this.el), eventsView, {categories:categories, model:this.model}); + $(this.el).enhanceWithin(); + return this; + } + }); + + return EventsView; +}); +------------------------------------------------------------------------------------------------------- + +As you can see, the view is very similar to the desktop view, the main difference being the explicit hint to jQuery mobile through the `pagecreate` event invocation. + +Next, we add the template for rendering the view: + +.src/main/webapp/resources/templates/mobile/events.html +[source,html] +------------------------------------------------------------------------------------------------------- +
    + Home +

    Categories

    +
    +
    +
    + <% + _.each(categories, function (category) { + %> +
    +

    <%= category.description %>

    +
      + <% + _.each(model.models, function (model) { + if (model.get('category').id == category.id) { + %> +
    • + <%=model.attributes.name%> +
    • + <% } + }); + %> +
    +
    + <% }); %> +
    +
    +------------------------------------------------------------------------------------------------------- + +And finally, we need to instruct the router to invoke the page: + +.src/main/webapp/resources/js/app/router/mobile/router.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * A module for the router of the desktop application. + * + */ +define("router",[ + ... + 'app/collections/events', + ... + 'app/views/mobile/events' + ... +],function ( + ..., + Events, + ..., + EventsView, + ...) { + + ... + var Router = Backbone.Router.extend({ + ... + routes:{ + ... + "events":"events" + ... + }, + ... + events:function () { + var events = new Events; + var eventsView = new EventsView({model:events, el:$("#container")}); + events.on("reset", function() { + utilities.viewManager.showView(eventsView); + }).fetch({ + reset : true, + error : function() { + utilities.displayAlert("Failed to retrieve events from the TicketMonster server."); + } + }); + } + ... + }); + ... +}); +------------------------------------------------------------------------------------------------------- + +Just as in the case of the desktop application, the list of events will be accessible at `#events` (i.e. `http://localhost:8080/ticket-monster/#events`). + +=== Displaying an individual event + + +Now, we create the view to display an event: + +.src/main/webapp/resources/js/app/views/mobile/event-detail.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +define([ + 'utilities', + 'require', + 'configuration', + 'text!../../../../templates/mobile/event-detail.html', + 'text!../../../../templates/mobile/event-venue-description.html' +], function ( + utilities, + require, + config, + eventDetail, + eventVenueDescription) { + + var EventDetailView = Backbone.View.extend({ + events:{ + "click a[id='bookButton']":"beginBooking", + "change select[id='showSelector']":"refreshShows", + "change select[id='performanceTimes']":"performanceSelected", + "change select[id='dayPicker']":'refreshTimes' + }, + render:function () { + $(this.el).empty() + utilities.applyTemplate($(this.el), eventDetail, _.extend({}, this.model.attributes, config)); + $(this.el).enhanceWithin(); + $("#bookButton").addClass("ui-disabled"); + var self = this; + $.getJSON(config.baseUrl + "rest/shows?event=" + this.model.get('id'), function (shows) { + self.shows = shows; + $("#showSelector").empty().append(""); + $.each(shows, function (i, show) { + $("#showSelector").append(""); + }); + $("#showSelector").selectmenu('refresh', true) + $("#dayPicker").selectmenu('disable') + $("#dayPicker").empty().append("") + $("#performanceTimes").selectmenu('disable') + $("#performanceTimes").empty().append("") + }); + $("#dayPicker").empty(); + $("#dayPicker").selectmenu('disable'); + $("#performanceTimes").empty(); + $("#performanceTimes").selectmenu('disable'); + $(this.el).enhanceWithin(); + return this; + }, + performanceSelected:function () { + if ($("#performanceTimes").val() != 'Choose a show time ...') { + $("#bookButton").removeClass("ui-disabled") + } else { + $("#bookButton").addClass("ui-disabled") + } + }, + beginBooking:function () { + require('router').navigate('book/' + $("#showSelector option:selected").val() + '/' + $("#performanceTimes").val(), true) + }, + refreshShows:function (event) { + + var selectedShowId = event.currentTarget.value; + + if (selectedShowId != 'Choose a venue ...') { + var selectedShow = _.find(this.shows, function (show) { + return show.id == selectedShowId + }); + this.selectedShow = selectedShow; + var times = _.uniq(_.sortBy(_.map(selectedShow.performances, function (performance) { + return (new Date(performance.date).withoutTimeOfDay()).getTime() + }), function (item) { + return item + })); + utilities.applyTemplate($("#eventVenueDescription"), eventVenueDescription, _.extend({},{venue:selectedShow.venue},config)); + $("#detailsCollapsible").show() + $("#dayPicker").removeAttr('disabled') + $("#performanceTimes").removeAttr('disabled') + $("#dayPicker").empty().append("") + _.each(times, function (time) { + var date = new Date(time) + $("#dayPicker").append("") + }); + $("#dayPicker").selectmenu('refresh') + $("#dayPicker").selectmenu('enable') + this.refreshTimes() + } else { + $("#detailsCollapsible").hide() + $("#eventVenueDescription").empty() + $("#dayPicker").empty() + $("#dayPicker").selectmenu('disable') + $("#performanceTimes").empty() + $("#performanceTimes").selectmenu('disable') + } + + + }, + refreshTimes:function () { + var selectedDate = $("#dayPicker").val(); + $("#performanceTimes").empty().append("") + if (selectedDate) { + $.each(this.selectedShow.performances, function (i, performance) { + var performanceDate = new Date(performance.date); + if (_.isEqual(performanceDate.toYMD(), selectedDate)) { + $("#performanceTimes").append("") + } + }) + $("#performanceTimes").selectmenu('enable') + } + $("#performanceTimes").selectmenu('refresh') + this.performanceSelected() + } + + }); + + return EventDetailView; +}); +------------------------------------------------------------------------------------------------------- + +Once again, this is very similar to the desktop version. Now we add the page templates: + +.src/main/webapp/resources/templates/mobile/event-detail.html +[source,html] +------------------------------------------------------------------------------------------------------- +
    + Home +

    Book tickets

    +
    +
    +

    <%=name%>

    + +

    <%=description%>

    +
    + + + + + + + <% _.each(sections, function(section) { %> + + <% }) %> + +
    + +
    + + +
    + + + +
    +
    + +
    + +
    +------------------------------------------------------------------------------------------------------- + +Next, the fragment that contains the input form for tickets, which is re-rendered whenever the section is changed: + +.src/main/webapp/resources/templates/mobile/ticket-entries.html +[source,html] +------------------------------------------------------------------------------------------------------- +<% if (ticketPrices.length > 0) { %> +
    +

    Select tickets by category

    + <% _.each(ticketPrices, function(ticketPrice) { %> +
    + +
    + + + +
    + <% }) %> + +<% } %> +------------------------------------------------------------------------------------------------------- + +Before submitting the request to the server, the order is confirmed: + +.src/main/webapp/resources/templates/mobile/confirm-booking.html +[source,html] +------------------------------------------------------------------------------------------------------- +
    + Home +

    Confirm order

    +
    +
    +

    <%=show.event.name%>

    +

    <%=show.venue.name%>

    +

    <%=new Date(performance.date).toPrettyString()%>

    +

    Buyer: <%=email%>

    +
    + +
    + +
    +
    + + + +
    +
    +------------------------------------------------------------------------------------------------------- + +The confirmation page contains a summary subview: + +.src/main/webapp/resources/templates/mobile/ticket-summary-view.html +[source,html] +------------------------------------------------------------------------------------------------------- + +
    +

    Totals

    +

    Total tickets: <%= totals.tickets %>

    +

    Total price: $ <%= totals.price %>

    +
    +------------------------------------------------------------------------------------------------------- + +Finally, we create the page that displays the booking confirmation: + +.src/main/webapp/resources/templates/mobile/booking-details.html +[source,html] +------------------------------------------------------------------------------------------------------- +
    + Home +

    Booking complete

    +
    +
    + + + + + + + + + + + + + + + <% $.each(_.sortBy(tickets, function(ticket) {return ticket.id}), function (i, ticket) { %> + + + + + + + + <% }) %> + +
    Booking <%=id%>
    Ticket #CategorySectionRowSeat
    <%= ticket.id %><%=ticket.ticketCategory.description%><%=ticket.seat.section.name%><%=ticket.seat.rowNumber%><%=ticket.seat.number%>
    +
    + +
    + Back +
    + +
    +------------------------------------------------------------------------------------------------------- + +The last step is registering the view with the router: + +.src/main/webapp/resources/js/app/router/mobile/router.js +[source,javascript] +------------------------------------------------------------------------------------------------------- +/** + * A module for the router of the desktop application + */ +define("router", [ + ... + 'app/views/mobile/create-booking', + ... +],function ( + ... + CreateBookingView + ...) { + + var Router = Backbone.Router.extend({ + ... + routes:{ + ... + "book/:showId/:performanceId":"bookTickets", + ... + }, + ... + bookTickets:function (showId, performanceId) { + var createBookingView = new CreateBookingView({model:{showId:showId, performanceId:performanceId, bookingRequest:{tickets:[]}}, el:$("#container")}); + utilities.viewManager.showView(createBookingView); + }, + ... + }); + ... +}); +------------------------------------------------------------------------------------------------------- + +The other use case: a booking starting from venues is conceptually similar, so you can just copy the rest of the logic from `src/main/webapp/resources/js/app/routers/mobile/router.js`, and the rest of the files files in the `src/main/webapp/resources/js/app/views/mobile` and `src/main/webapp/resources/templates/mobile` directories. diff --git a/tutorial/WhatIsTicketMonster.asciidoc b/tutorial/WhatIsTicketMonster.asciidoc new file mode 100644 index 000000000..6613855b7 --- /dev/null +++ b/tutorial/WhatIsTicketMonster.asciidoc @@ -0,0 +1,216 @@ += What is TicketMonster? +:Author: Marius Bogoevici +:thumbnail: http://static.jboss.org/ffe/1/www/origin/ticket-monster-splash-2.png + + +== Preamble +TicketMonster is an example application that focuses on Java EE6 - JPA 2, CDI, EJB 3.1 and JAX-RS along with HTML5 and jQuery Mobile. It is a moderately complex application that demonstrates how to build modern web applications optimized for mobile & desktop. TicketMonster is representative of an online ticketing broker - providing access to events (e.g. concerts, shows, etc) with an online booking application. + +Apart from being a demo, TicketMonster provides an already existing application structure that you can use as a starting point for your app. You could try out your use cases, test your own ideas, or, contribute improvements back to the community. + +image::gfx/octocat_social.png[] + +link:http://github.com/jboss-jdf/ticket-monster[Fork us on GitHub!] + +The accompanying tutorials walk you through the various tools & technologies needed to build TicketMonster on your own. Alternatively you can download TicketMonster as a completed application and import it into your favorite IDE. + +Before we dive into the code, let's discuss the requirements for the application. + + +== Use cases + +We have grouped the current use cases in two major categories: end user oriented, and administrative. + + +=== What can end users do? + +The end users of the application want to attend some cool events. They will try to find shows, create bookings, or cancel bookings. The use cases are: + +* look for current events; +* look for venues; +* select shows (events taking place at specific venues) and choose a performance time; +* book tickets; +* view current bookings; +* cancel bookings; + +[[end-user-use-cases-image]] +.End user use cases +image::gfx/ticket-monster-user-use-cases.png[] + + +=== What can administrators do? + +Administrators are more concerned the operation of the business. They will manage the _master data_: information about venues, events and shows, and will want to see how many tickets have been sold. The use cases are: + +* add, remove and update events; +* add, remove and update venues (including venue layouts); +* add, remove and update shows and performances; +* monitor ticket sales for current shows; + +[[administration-use-cases-image]] +.Administration use cases +image::gfx/ticket-monster-administration-use-cases.png[] + + +== Architecture + +[[architecture-image]] +.TicketMonster architecture +image::gfx/ticket-monster-architecture.png[] + +The application uses Java EE 6 services to provide business logic and persistence, utilizing technologies such as CDI, EJB 3.1 and JAX-RS, JPA 2. These services back the user-facing booking process, which is implemented using HTML5 and JavaScript, with support for mobile devices through jQuery Mobile. + +The administration site is centered around CRUD use cases, so instead of writing everything manually, the business layer and UI are generated by Forge, using EJB 3.1, CDI and JAX-RS. For a better user experience, Twitter Bootstrap is used. + +Monitoring sales requires staying in touch with the latest changes on the server side, so this part of the application will be developed in HTML5 and JavaScript using a polling solution. + + +== How can you run it? + +=== Building TicketMonster + +[CAUTION] +=================================================================================== +In order to build the application, you will need you to +configure Maven to use the JBoss Enterprise Maven repositories. For instructions on +configure the Maven repositories, visit the link:https://access.redhat.com/site/documentation/en-US/JBoss_Enterprise_Application_Platform/6.3/html-single/Development_Guide/index.html#Install_the_JBoss_Enterprise_Application_Platform_6_Maven_Repository[JBoss Enterprise Application Platform 6.3 documentation]. +=================================================================================== + +TicketMonster can be built from Maven, by runnning the following Maven command: + +---- +mvn clean package +---- + +This prepares a WAR file that you can deploy right away in a JBoss Enterprise Application Platform instance. It would use the in-built H2 database. + +If you want to run the Arquillian tests as part of the build, you can enable one of the two available Arquillian profiles. + +For running the tests in an _already running_ application server instance, use the `arq-jbossas-remote` profile. + +---- +mvn clean package -Parq-jbossas-remote +---- + +If you want the test runner to _start_ an application server instance, use the `arq-jbossas-managed` profile. You must set up the `JBOSS_HOME` property to point to the server location, or update the `src/main/test/resources/arquillian.xml` file. + +---- +mvn clean package -Parq-jbossas-managed +---- + +If you intend to deploy into link:http://openshift.com[OpenShift] with the PostgreSQL cartridge, you can use the `postgresql-openshift` profile: + +---- +mvn clean package -Ppostgresql-openshift +---- + +If you intend to deploy into link:http://openshift.com[OpenShift] with the MySQL cartridge, you can use the `mysql-openshift` profile: + +---- +mvn clean package -Pmysql-openshift +---- + +=== Running TicketMonster + + +You can run TicketMonster into a local JBoss EAP 6.3 instance or on OpenShift. + + +==== Running TicketMonster locally + +_Start JBoss Enterprise Application Platform 6.3_. + +1. Open a command line and navigate to the root of the JBoss server directory. +2. The following shows the command line to start the server with the web profile: ++ +---- +For Linux: JBOSS_HOME/bin/standalone.sh +For Windows: JBOSS_HOME\bin\standalone.bat +---- + +Then, _deploy TicketMonster_. + + +1. Make sure you have started the JBoss Server as described above. +2. Type this command to build and deploy the archive into a running server instance. ++ +---- +mvn clean package jboss-as:deploy +---- ++ +(You can use the `arq-jbossas-remote` profile for running tests as well) + +3. This will deploy `target/ticket-monster.war` to the running instance of the server. +4. Now you can see the application running at http://localhost:8080/ticket-monster. + +==== Running TicketMonster in OpenShift + + +First, _create an OpenShift project_. + +1. Make sure that you have an OpenShift domain and you have created an application using the `jbosseap-6` cartridge (for more details, get started link:https://openshift.redhat.com/app/getting_started[here]). If you want to use PostgreSQL, add the `postgresql-9.2` cartridge too. Or for MySQL, add the `mysql-5.5` cartridge. +2. Ensure that the Git repository of the project is checked out. + +Then, _build and deploy it_. + +1. Build TicketMonster using either: + * the default profile (with H2 database support) ++ +---- +mvn clean package +---- + + * the `postgresql-openshift` profile (with PostgreSQL support) if the PostgreSQL cartrdige is enabled in OpenShift. ++ +---- +mvn clean package -Ppostgresql-openshift +---- + + * the `mysql-openshift` profile (with MySQL support) if the MySQL cartrdige is enabled in OpenShift. ++ +---- +mvn clean package -Pmysql-openshift +---- + +2. Copy the `target/ticket-monster.war` file in the OpenShift Git repository (located at ``). ++ +---- +cp target/ticket-monster.war /deployments/ROOT.war +---- + +3. Navigate to `` folder. + +4. Remove the existing `src` folder and `pom.xml` file. ++ +---- +git rm -r src +git rm pom.xml +---- + +5. Add the copied file to the repository, commit and push to Openshift ++ +---- +git add deployments/ROOT.war +git commit -m "Deploy TicketMonster" +git push +---- + +6. Now you can see the application running at at `http://-.rhcloud.com` + + +== Learn more + + +The example is accompanied by a series of tutorials that will walk you through the process of +creating the TicketMonster application from end to end. + +After reading this series you will understand how to: + +* set up your project; +* define the persistence layer of the application; +* design and implement the business layer and expose it to the front-end via RESTful endpoints; +* implement a mobile-ready front-end using HTML 5, JSON, JavaScript and jQuery Mobile; +* develop a HTML5-based administration interface rapidly using JBoss Forge; +* thoroughly test your project using JUnit and Arquillian; + +Throughout the series, you will be shown how to achieve these goals using JBoss Developer Studio. diff --git a/tutorial/WriterGuidelines.md b/tutorial/WriterGuidelines.md new file mode 100644 index 000000000..f24908c24 --- /dev/null +++ b/tutorial/WriterGuidelines.md @@ -0,0 +1,4 @@ +* Must use JBDS throughout +* Short videos - 5 mins max +* Start with new Java Project +* See for now TODO Unify these. diff --git a/tutorial/collateral/TicketMonster architecture.graffle b/tutorial/collateral/TicketMonster architecture.graffle new file mode 100644 index 000000000..6165bacba --- /dev/null +++ b/tutorial/collateral/TicketMonster architecture.graffle @@ -0,0 +1,2726 @@ + + + + + ApplicationVersion + + com.omnigroup.OmniGrafflePro + 138.33.0.157554 + + CreationDate + 2012-02-21 21:57:14 +0000 + Creator + Marius Bogoevici + GraphDocumentVersion + 8 + GuidesLocked + NO + GuidesVisible + YES + ImageCounter + 1 + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2012-05-18 04:47:03 +0000 + Modifier + Marius Bogoevici + NotesVisible + NO + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSHorizonalPagination + + int + 0 + + NSLeftMargin + + float + 18 + + NSPaperSize + + coded + BAtzdHJlYW10eXBlZIHoA4QBQISEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAx7X05TU2l6ZT1mZn2WgWQCgRgDhg== + + NSPrintReverseOrientation + + int + 0 + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + ReadOnly + NO + Sheets + + + ActiveLayerIndex + 0 + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 0/72 in = 1.0000 in + GraphicsList + + + Bounds + {{336, 147.00177}, {17.5, 20}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Color + + w + 1 + + + ID + 74 + Shape + Circle + Style + + fill + + Color + + b + 0 + g + 0 + r + 0 + + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf1 4 } + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{196.06154, 105.00177}, {17.5, 20}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Color + + w + 1 + + + ID + 73 + Shape + Circle + Style + + fill + + Color + + b + 0 + g + 0 + r + 0 + + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf1 3 } + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{183.75, 176.95947}, {21.25, 20}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Color + + w + 1 + + + ID + 72 + Shape + Circle + Style + + fill + + Color + + b + 0 + g + 0 + r + 0 + + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf1 2 } + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{95.75, 36.5}, {21.25, 20}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + FontInfo + + Color + + w + 1 + + + ID + 71 + Shape + Circle + Style + + fill + + Color + + b + 0 + g + 0 + r + 0 + + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf1 1 } + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{74, 145.24478}, {43, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 70 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 execute} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{181.31154, 145.24478}, {47, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 69 + Line + + ID + 37 + Position + 0.60377860069274902 + RotationType + 0 + + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 delegate} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{168, 205}, {37, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 68 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 update} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 31 + + ID + 67 + Points + + {154.5, 201} + {218.49997, 201} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 4 + + + + Bounds + {{309.5, 133.00177}, {35, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 66 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 render} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 13 + + ID + 65 + Points + + {304.02798, 124.05056} + {364.25, 137.75} + + Style + + stroke + + HeadArrow + StickArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 30 + + + + Bounds + {{274.5, 162}, {36, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 64 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 events} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{364.25, 81.00708}, {36, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 63 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 events} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 42 + + ID + 61 + Points + + {264.25, 333.5} + {264.25, 371.5} + + Style + + stroke + + HeadArrow + 0 + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 33 + + + + Class + LineGraphic + Head + + ID + 31 + + ID + 60 + Points + + {264.25, 133.50179} + {264.25, 182.49998} + + Style + + stroke + + HeadArrow + StickArrow + LineType + 1 + TailArrow + StickArrow + + + Tail + + ID + 30 + + + + Bounds + {{260, 39.5}, {65, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 58 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 backbone.js} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{446, 88.00177}, {59, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 52 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 user action} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 13 + + ID + 51 + Points + + {496, 138} + {436.24997, 137.75} + + Style + + stroke + + HeadArrow + StickArrow + TailArrow + 0 + + + + + Bounds + {{460, 279}, {36, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 50 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Server} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{460, 234}, {31, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 49 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Client} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + ID + 48 + Points + + {43, 262} + {496, 264} + {495, 265} + + Style + + stroke + + HeadArrow + 0 + Pattern + 1 + TailArrow + 0 + + + + + Class + LineGraphic + Head + + ID + 33 + + ID + 43 + Points + + {264.25, 219.50002} + {264.25, 296.5} + + Style + + stroke + + HeadArrow + StickArrow + TailArrow + 0 + + + Tail + + ID + 31 + + + + Bounds + {{223.25, 372}, {82, 36}} + Class + ShapedGraphic + ID + 42 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Domain} + + + + Class + LineGraphic + Head + + ID + 4 + + ID + 41 + Points + + {125.82267, 133.50119} + {126.67732, 182.50008} + + Style + + stroke + + HeadArrow + StickArrow + TailArrow + 0 + + + Tail + + ID + 28 + + + + Bounds + {{133, 65}, {21, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 39 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 #url} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + Head + + ID + 28 + + ID + 38 + Points + + {124, 41} + {125.12502, 96.502571} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + Class + LineGraphic + Head + + ID + 30 + + ID + 37 + Points + + {151.84752, 185.43102} + {239.56845, 130.46677} + + Style + + stroke + + HeadArrow + StickArrow + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 4 + + + + Class + LineGraphic + Head + + ID + 33 + + ID + 36 + Points + + {148.42276, 218.79378} + {242.19432, 296.68054} + + Style + + stroke + + HeadArrow + StickArrow + TailArrow + 0 + + + Tail + + ID + 4 + + + + Class + LineGraphic + Head + + ID + 13 + + ID + 35 + Points + + {305.15625, 106.79773} + {354, 97.00177} + {400.25, 110.75} + + Style + + stroke + + HeadArrow + 0 + LineType + 1 + TailArrow + StickArrow + + + Tail + + ID + 30 + + + + Bounds + {{364.25, 110.75}, {72, 54}} + Class + ShapedGraphic + ID + 13 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + DocumentShape + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 DOM} + VerticalPad + 0 + + + + Bounds + {{223.25, 297}, {82, 36}} + Class + ShapedGraphic + ID + 33 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 REST\ +service} + + + + Bounds + {{100, 183}, {54, 36}} + Class + ShapedGraphic + ID + 4 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Action} + + + + Bounds + {{219, 183}, {90.5, 36}} + Class + ShapedGraphic + ID + 31 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Model} + VerticalPad + 0 + + + + Bounds + {{219, 97.00177}, {90.5, 36}} + Class + ShapedGraphic + ID + 30 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 View} + VerticalPad + 0 + + + + Bounds + {{84.5, 97.00177}, {82, 36}} + Class + ShapedGraphic + ID + 28 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Router} + VerticalPad + 0 + + + + Bounds + {{67.265839, 62.020248}, {279, 178}} + Class + ShapedGraphic + ID + 57 + Line + + ID + 37 + Position + 0.6260572075843811 + RotationType + 0 + + Shape + Rectangle + Style + + fill + + Draws + NO + + stroke + + CornerRadius + 9 + Pattern + 2 + + + + + GridInfo + + HPages + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canvas 1 + UniqueID + 1 + VPages + 1 + + + ActiveLayerIndex + 0 + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 0/72 in = 1.0000 in + GraphicsList + + + Class + LineGraphic + Head + + ID + 18 + + ID + 27 + Points + + {210.49034, 140.72678} + {255.88409, 131.67522} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 1 + + + + Class + LineGraphic + Head + + ID + 19 + + ID + 26 + Points + + {211.49998, 161.60364} + {272.46503, 194.76111} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + + + Bounds + {{248, 195}, {116, 36}} + Class + ShapedGraphic + ID + 19 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Venue image} + + + + Bounds + {{256, 102}, {116, 36}} + Class + ShapedGraphic + ID + 18 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Performance times} + + + + Class + LineGraphic + Head + + ID + 12 + + ID + 17 + Points + + {237.5, 542.49963} + {306.5, 542.49915} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 15 + + + + Class + LineGraphic + Head + + ID + 15 + + ID + 16 + Points + + {179, 477.5} + {179, 516.5} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 13 + + + + Bounds + {{121, 517}, {116, 51}} + Class + ShapedGraphic + ID + 15 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Ticket quantity \ +inputs} + + + + Bounds + {{138, 441}, {82, 36}} + Class + ShapedGraphic + ID + 13 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Section \ +Selector} + + + + Bounds + {{307, 474.5}, {116, 136}} + Class + ShapedGraphic + ID + 12 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Ticket Summary} + + + + Bounds + {{224.5, 636}, {82, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 11 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Create booking} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{94, 415}, {343, 206}} + Class + ShapedGraphic + ID + 10 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + + + Class + LineGraphic + Head + + ID + 7 + + ID + 9 + Points + + {210.49673, 153.76442} + {271.50266, 160.76877} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 1 + + + + Bounds + {{128, 131}, {82, 36}} + Class + ShapedGraphic + ID + 1 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Venue\ +Selector} + + + + Bounds + {{272, 148.82437}, {116, 36}} + Class + ShapedGraphic + ID + 7 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Venue details} + + + + Bounds + {{214, 282.5}, {69, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 6 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Event details} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{100.38281, 80.324371}, {309, 173}} + Class + ShapedGraphic + ID + 4 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + + + GridInfo + + HPages + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canvas 3 + UniqueID + 3 + VPages + 1 + + + ActiveLayerIndex + 0 + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 0/72 in = 1.0000 in + GraphicsList + + + Class + LineGraphic + Head + + ID + 11 + + ID + 15 + Points + + {174.5, 93} + {270.5, 93} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 75 + + + + Bounds + {{271, 69}, {123, 48}} + Class + ShapedGraphic + ID + 11 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 REST\ +services} + + + + Bounds + {{51, 69}, {123, 48}} + Class + ShapedGraphic + ID + 75 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Singe HTML5\ +page} + + + + GridInfo + + HPages + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canvas 2 + UniqueID + 2 + VPages + 1 + + + ActiveLayerIndex + 0 + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 0/72 in = 1.0000 in + GraphicsList + + + Bounds + {{207, 55}, {81, 36}} + Class + ShapedGraphic + ID + 16 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Errai} + + + + Bounds + {{409, 198}, {63, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 15 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Persistence} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{414, 66}, {111, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 14 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Front-end interaction} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{409, 132}, {97, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 13 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Business services} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{73, 187}, {311, 36}} + Class + ShapedGraphic + ID + 8 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 JPA} + + + + Bounds + {{303, 55}, {81, 36}} + Class + ShapedGraphic + ID + 7 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Errai} + + + + Bounds + {{201, 121}, {183, 36}} + Class + ShapedGraphic + ID + 5 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Internal services} + + + + Bounds + {{73, 55}, {115, 36}} + Class + ShapedGraphic + ID + 4 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 JAX-RS services} + + + + GridInfo + + HPages + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canvas 4 + UniqueID + 4 + VPages + 1 + + + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UseEntirePage + + WindowInfo + + CurrentSheet + 3 + ExpandedCanvases + + + name + Canvas 1 + + + name + Canvas 2 + + + Frame + {{792, -2}, {909, 774}} + ListView + + OutlineWidth + 142 + RightSidebar + + ShowRuler + + Sidebar + + SidebarWidth + 120 + VisibleRegion + {{-99, 0}, {774, 619}} + Zoom + 1 + ZoomValues + + + Canvas 1 + 1 + 4 + + + Canvas 2 + 1 + 1 + + + Canvas 3 + 1 + 1 + + + Canvas 4 + 1 + 1 + + + + saveQuickLookFiles + YES + + diff --git a/tutorial/collateral/TicketMonster-HTML5.graffle b/tutorial/collateral/TicketMonster-HTML5.graffle new file mode 100644 index 000000000..4d57c0897 --- /dev/null +++ b/tutorial/collateral/TicketMonster-HTML5.graffle @@ -0,0 +1,306 @@ + + + + + ActiveLayerIndex + 0 + ApplicationVersion + + com.omnigroup.OmniGraffle.MacAppStore + 138.33 + + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + CreationDate + 2012-04-16 23:48:23 +0000 + Creator + Marius Bogoevici + DisplayScale + 1 0/72 in = 1.0000 in + GraphDocumentVersion + 8 + GraphicsList + + + Class + LineGraphic + Head + + ID + 11 + + ID + 15 + Points + + {174.5, 93} + {270.5, 93} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 4 + + + + Bounds + {{271, 69}, {123, 48}} + Class + ShapedGraphic + ID + 11 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 REST\ +services} + + + + Bounds + {{51, 69}, {123, 48}} + Class + ShapedGraphic + ID + 4 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Singe HTML5\ +page} + + + + GridInfo + + GuidesLocked + NO + GuidesVisible + YES + HPages + 1 + ImageCounter + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2012-06-04 14:24:43 +0000 + Modifier + Pete Muir + NotesVisible + NO + Orientation + 2 + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSHorizonalPagination + + int + 0 + + NSLeftMargin + + float + 18 + + NSPaperName + + string + na-letter + + NSPaperSize + + coded + BAtzdHJlYW10eXBlZIHoA4QBQISEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAx7X05TU2l6ZT1mZn2WgWQCgRgDhg== + + NSPrintReverseOrientation + + int + 0 + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + PrintOnePage + + ReadOnly + NO + RowAlign + 1 + RowSpacing + 36 + SheetTitle + HTML5 + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UniqueID + 1 + UseEntirePage + + VPages + 1 + WindowInfo + + CurrentSheet + 0 + ExpandedCanvases + + + name + HTML5 + + + Frame + {{285, 4}, {710, 774}} + ListView + + OutlineWidth + 142 + RightSidebar + + ShowRuler + + Sidebar + + SidebarWidth + 120 + VisibleRegion + {{0, 0}, {575, 619}} + Zoom + 1 + ZoomValues + + + HTML5 + 1 + 1 + + + + saveQuickLookFiles + YES + + diff --git a/tutorial/collateral/TicketMonster-diagrams.graffle b/tutorial/collateral/TicketMonster-diagrams.graffle new file mode 100755 index 000000000..e6dd614c0 --- /dev/null +++ b/tutorial/collateral/TicketMonster-diagrams.graffle @@ -0,0 +1,3642 @@ + + + + + ApplicationVersion + + com.omnigroup.OmniGraffle + 139.18.0.187838 + + CreationDate + 2012-01-12 15:24:24 +0000 + Creator + Marius Bogoevici + GraphDocumentVersion + 8 + GuidesLocked + NO + GuidesVisible + YES + ImageCounter + 2 + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2013-10-04 13:13:28 +0000 + Modifier + Vineet Reynolds + NotesVisible + NO + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSHorizonalPagination + + int + 0 + + NSLeftMargin + + float + 18 + + NSPaperSize + + size + {612, 792} + + NSPrintReverseOrientation + + int + 0 + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + ReadOnly + NO + Sheets + + + ActiveLayerIndex + 0 + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 0/72 in = 1.0000 in + GraphicsList + + + Bounds + {{412, 228}, {92, 72}} + Class + ShapedGraphic + ID + 15 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Monitoring Dashboard\ +(HTML5, Backbone.js)} + + + + Bounds + {{163.5, 174}, {115, 36}} + Class + ShapedGraphic + FontInfo + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + + ID + 14 + Shape + Rectangle + Style + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red153\green153\blue153;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf2 Native Mobile\ +(Apache Cordova) } + + + + Bounds + {{298, 310}, {102, 49}} + Class + ShapedGraphic + ID + 13 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + Pattern + 1 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Forge \ +Scaffold} + + + + Bounds + {{92.5, 228}, {132, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 12 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 User Front-end (HTML5)} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{164, 252}, {108, 36}} + Class + ShapedGraphic + ID + 11 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Mobile UI} + + + + Bounds + {{41, 252}, {108, 36}} + Class + ShapedGraphic + ID + 10 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Classic UI} + + + + Bounds + {{298, 228}, {102, 72}} + Class + ShapedGraphic + ID + 7 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Admin Front-end \ +(HTML5, AngularJS)} + + + + Bounds + {{35, 224}, {244, 72}} + Class + ShapedGraphic + ID + 6 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + + + Bounds + {{35, 310}, {244, 49}} + Class + ShapedGraphic + ID + 5 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Business Layer (CDI, EJB, JAX-RS)} + + + + Bounds + {{35, 371}, {469, 40}} + Class + ShapedGraphic + ID + 4 + Shape + Rectangle + Style + + stroke + + CornerRadius + 9 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Persistence (JPA)} + + + + GridInfo + + HPages + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canvas 1 + UniqueID + 1 + VPages + 1 + + + ActiveLayerIndex + 0 + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 0/72 in = 1.0000 in + GraphicsList + + + Class + LineGraphic + Head + + ID + 65 + + ID + 66 + Points + + {148.87577644629079, 471.00004000000001} + {254.05496073758383, 626.22505020903884} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + Tail + + ID + 42 + + + + Bounds + {{222, 624.97069999999997}, {99, 54}} + Class + ShapedGraphic + ID + 65 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs24 \cf0 Monitor sales} + VerticalPad + 0 + + + + Bounds + {{345.99999983699462, 470.48536493449024}, {50, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 64 + Line + + ID + 63 + Position + 0.5 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 <uses>} + VerticalPad + 0 + + Wrap + NO + + + AllowLabelDrop + + Class + LineGraphic + Head + + ID + 49 + + ID + 63 + Points + + {305.72153195823375, 525.92884933921187} + {436.27846771575548, 429.04188052976866} + + Style + + stroke + + HeadArrow + StickArrow + HeadScale + 2.2142860889434814 + Legacy + + Pattern + 4 + TailArrow + 0 + + + Tail + + ID + 50 + + + + Bounds + {{254.74999967497445, 488.9926899930735}, {50, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 62 + Line + + ID + 61 + Position + 0.5 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 <uses>} + VerticalPad + 0 + + Wrap + NO + + + AllowLabelDrop + + Class + LineGraphic + Head + + ID + 48 + + ID + 61 + Points + + {277.9818644666608, 520.50737284641389} + {281.5181348832881, 471.47800713973311} + + Style + + stroke + + HeadArrow + StickArrow + HeadScale + 2.2142860889434814 + Legacy + + Pattern + 4 + TailArrow + 0 + + + Tail + + ID + 50 + + + + Bounds + {{352.76952194780739, 529.73714046015766}, {50, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 56 + Line + + ID + 55 + Position + 0.57360780239105225 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 <uses>} + VerticalPad + 0 + + Wrap + NO + + + AllowLabelDrop + + Class + LineGraphic + Head + + ID + 54 + + ID + 55 + Points + + {325.01627659710255, 542.56776191002325} + {416.98372392052784, 532.40293949031252} + + Style + + stroke + + HeadArrow + StickArrow + HeadScale + 2.2142860889434814 + Legacy + + Pattern + 4 + TailArrow + 0 + + + Tail + + ID + 50 + + + + Bounds + {{416.5, 499.98534999999998}, {99, 54}} + Class + ShapedGraphic + ID + 54 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs24 \cf0 Manage \ +Layout} + VerticalPad + 0 + + + + Class + LineGraphic + Head + + ID + 50 + + ID + 53 + Points + + {150.5, 421.98538000000002} + {252.0103789660931, 523.900158521246} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + Class + LineGraphic + Head + + ID + 49 + + ID + 52 + Points + + {150.500022, 406.98541061490675} + {415.99997522314845, 406.98538474287636} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + Tail + + ID + 42 + + + + Class + LineGraphic + Head + + ID + 48 + + ID + 51 + Points + + {150.500022, 416.3430476135224} + {236.74624498469376, 434.27771414076761} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + Tail + + ID + 42 + + + + Bounds + {{226.5, 520.98535000000004}, {99, 54}} + Class + ShapedGraphic + ID + 50 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs24 \cf0 Manage \ +Shows} + VerticalPad + 0 + + + + Bounds + {{416.5, 379.98538000000002}, {99, 54}} + Class + ShapedGraphic + ID + 49 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs24 \cf0 Manage Venues} + VerticalPad + 0 + + + + Bounds + {{234, 417.00002999999998}, {99, 54}} + Class + ShapedGraphic + ID + 48 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs24 \cf0 Manage Events} + VerticalPad + 0 + + + + Bounds + {{352.50815011489306, 193.00000087744107}, {50, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 46 + Line + + ID + 45 + Position + 0.5 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 <uses>} + VerticalPad + 0 + + Wrap + NO + + + AllowLabelDrop + + Class + LineGraphic + Head + + ID + 43 + + ID + 45 + Points + + {325.37300229172189, 202.04435267984394} + {429.64329793806428, 197.9556490750382} + + Style + + stroke + + HeadArrow + StickArrow + HeadScale + 2.2142860889434814 + Legacy + + Pattern + 4 + TailArrow + 0 + + + Tail + + ID + 24 + + + + Bounds + {{430.0163, 169}, {99, 54}} + Class + ShapedGraphic + ID + 43 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs24 \cf0 Select Show} + VerticalPad + 0 + + + + Class + Group + Graphics + + + Bounds + {{60.499969, 447.07549999999998}, {90.000052999999994, 23.278046}} + Class + ShapedGraphic + ID + 34 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 Administrator} + VerticalPad + 0 + + + + Class + Group + Graphics + + + AllowLabelDrop + + Class + LineGraphic + ID + 36 + Points + + {135.49996999999999, 389.52697999999998} + {105.49996, 389.52697999999998} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + AllowLabelDrop + + Class + LineGraphic + ID + 37 + Points + + {105.50002000000001, 389.52697999999998} + {75.500015000000005, 389.52697999999998} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + AllowLabelDrop + + Class + LineGraphic + ID + 38 + Points + + {105.50001, 412.80484000000001} + {120.5, 447.72185999999999} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + AllowLabelDrop + + Class + LineGraphic + ID + 39 + Points + + {105.49999, 412.80484000000001} + {90.499984999999995, 447.72185999999999} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 40 + Points + + {105.50003, 377.88790999999998} + {105.50003, 412.80495999999999} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + Bounds + {{90.5, 354.60986000000003}, {30.000008000000001, 23.278046}} + Class + ShapedGraphic + ID + 41 + Shape + Circle + Style + + + + ID + 35 + + + Bounds + {{60.499969, 342.97079000000002}, {90.000052999999994, 128.02924999999999}} + Class + ShapedGraphic + ID + 42 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + + ID + 33 + + + Bounds + {{329.50000004124882, 124.99269101351778}, {50, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 31 + Line + + ID + 30 + Position + 0.5 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 <uses>} + VerticalPad + 0 + + Wrap + NO + + + AllowLabelDrop + + Class + LineGraphic + Head + + ID + 12 + + ID + 30 + Points + + {301.2938551781491, 180.48928988866115} + {407.70614490434855, 83.496092138374379} + + Style + + stroke + + HeadArrow + StickArrow + HeadScale + 2.2142860889434814 + Legacy + + Pattern + 4 + TailArrow + 0 + + + Tail + + ID + 24 + + + + Bounds + {{248.5000022196823, 150.99841297129845}, {50, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 29 + Line + + ID + 28 + Position + 0.5 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 <uses>} + VerticalPad + 0 + + Wrap + NO + + + AllowLabelDrop + + Class + LineGraphic + Head + + ID + 22 + + ID + 28 + Points + + {274.30473821966638, 176.50800008319916} + {272.69526621969817, 139.48882585939774} + + Style + + stroke + + HeadArrow + StickArrow + HeadScale + 2.2142860889434814 + Legacy + + Pattern + 4 + TailArrow + 0 + + + Tail + + ID + 24 + + + + Bounds + {{350.49999979176539, 242.00000013731318}, {50, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 19 + Line + + ID + 14 + Position + 0.5 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 <uses>} + VerticalPad + 0 + + Wrap + NO + + + AllowLabelDrop + + Class + LineGraphic + Head + + ID + 26 + + ID + 14 + Points + + {314.14154601248453, 221.38869579488724} + {436.85845357104625, 276.61130447973909} + + Style + + stroke + + HeadArrow + StickArrow + HeadScale + 2.2142860889434814 + Legacy + + Pattern + 4 + TailArrow + 0 + + + Tail + + ID + 24 + + + + Bounds + {{426, 267}, {99, 54}} + Class + ShapedGraphic + ID + 26 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs24 \cf0 Reserve Seats} + VerticalPad + 0 + + + + Class + LineGraphic + Head + + ID + 24 + + ID + 25 + Points + + {110, 80.184834904208003} + {245.93339140924945, 181.88039556644529} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + Tail + + ID + 11 + + + + Bounds + {{226, 177}, {99, 54}} + Class + ShapedGraphic + ID + 24 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Book ticket} + VerticalPad + 0 + + + + Class + LineGraphic + Head + + ID + 22 + + ID + 23 + Points + + {109.99999999999999, 67.435297083689008} + {226.83591804511127, 99.672988497056707} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + Tail + + ID + 11 + + + + Bounds + {{222, 84.996825999999999}, {99, 54}} + Class + ShapedGraphic + ID + 22 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs24 \cf0 Select Venue} + VerticalPad + 0 + + + + Class + LineGraphic + Head + + ID + 12 + + ID + 21 + Points + + {110, 59.985382000000001} + {383.49997523947536, 59.985382000000001} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + Tail + + ID + 11 + + + + Bounds + {{384, 32.985382000000001}, {99, 54}} + Class + ShapedGraphic + ID + 12 + Shape + Circle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs24 \cf0 Select Event} + VerticalPad + 0 + + + + Class + Group + Graphics + + + Bounds + {{56, 90.985382000000001}, {54, 18}} + Class + ShapedGraphic + ID + 3 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 User} + VerticalPad + 0 + + + + Class + Group + Graphics + + + AllowLabelDrop + + Class + LineGraphic + ID + 5 + Points + + {101, 46.485382000000001} + {83, 46.485382000000001} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + AllowLabelDrop + + Class + LineGraphic + ID + 6 + Points + + {83, 46.485382000000001} + {65, 46.485382000000001} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + AllowLabelDrop + + Class + LineGraphic + ID + 7 + Points + + {83, 64.485382000000001} + {92, 91.485382000000001} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + AllowLabelDrop + + Class + LineGraphic + ID + 8 + Points + + {83, 64.485382000000001} + {74, 91.485382000000001} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 9 + Points + + {83, 37.485382000000001} + {83, 64.485382000000001} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + Bounds + {{74, 19.485382000000001}, {18, 18}} + Class + ShapedGraphic + ID + 10 + Shape + Circle + Style + + + + ID + 4 + + + Bounds + {{56, 10.485382}, {54, 99}} + Class + ShapedGraphic + ID + 11 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + + ID + 20 + + + GridInfo + + HPages + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canvas 2 + UniqueID + 2 + VPages + 1 + + + ActiveLayerIndex + 0 + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 0/72 in = 1.0000 in + GraphicsList + + + Class + LineGraphic + Head + + ID + 91 + + ID + 108 + Points + + {95.5, 443.25} + {95.5, 512.5} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 92 + + + + Class + LineGraphic + Head + + ID + 92 + + ID + 107 + Points + + {95.5, 336.5} + {95.5, 406.25} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 52 + + + + Class + LineGraphic + Head + + ID + 1 + + ID + 106 + Points + + {252.11913999999999, 417} + {252.11913999999999, 337} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 93 + + + + Class + LineGraphic + Head + + ID + 85 + + ID + 105 + Points + + {319.61914000000002, 435} + {451, 435} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 93 + + + + Class + LineGraphic + Head + + ID + 93 + + ID + 104 + Points + + {455.5, 318.5} + {319.61914000000002, 435} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 54 + + + + Class + LineGraphic + Head + + ID + 54 + + ID + 103 + Points + + {436.30957000793535, 318.5} + {455.5, 318.5} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 60 + + + + Class + LineGraphic + Head + + ID + 1 + + ID + 102 + Points + + {316.80957000000001, 318.5} + {297.61914000793536, 318.5} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 60 + + + + Class + LineGraphic + Head + + ID + 1 + + ID + 101 + Points + + {159, 318.5} + {206.61914000649429, 318.5} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 52 + + + + Class + LineGraphic + Head + + ID + 54 + + ID + 100 + Points + + {500.5, 199} + {500.5, 300.5} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 4 + + + + Class + LineGraphic + Head + + ID + 18 + + ID + 99 + Points + + {207.11913999999999, 181} + {126, 181} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 49 + + + + Class + LineGraphic + Head + + ID + 4 + + ID + 98 + Points + + {297.11914000000002, 181} + {462, 181} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 49 + + + + Class + LineGraphic + Head + + ID + 89 + + ID + 97 + Points + + {500.5, 163} + {500.5, 115.75} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 4 + + + + Class + LineGraphic + Head + + ID + 88 + + ID + 96 + Points + + {462, 181} + {287.30664000000002, 97.75} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 4 + + + + Class + LineGraphic + Head + + ID + 90 + + ID + 95 + Points + + {252.11913999999999, 79.25} + {252.11913999999999, 43.5} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 88 + + + + Class + LineGraphic + Head + + ID + 88 + + ID + 94 + Points + + {126, 181} + {216.93163999999999, 97.75} + + Style + + stroke + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 18 + + + + Bounds + {{184.61913999999999, 417}, {135, 36}} + Class + ShapedGraphic + ID + 93 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 TicketPriceCategory} + VerticalPad + 0 + + + + Bounds + {{50.5, 406.75}, {90, 36}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 92 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\b\fs24 \cf0 Ticket} + + + + Bounds + {{50.5, 513}, {90, 36}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 91 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\b\fs24 \cf0 Seat} + + + + Bounds + {{213.61913999999999, 7.5}, {77, 36}} + Class + ShapedGraphic + ID + 90 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 MediaType} + VerticalPad + 0 + + + + Bounds + {{462, 79.75}, {77, 36}} + Class + ShapedGraphic + ID + 89 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 Address} + VerticalPad + 0 + + + + Bounds + {{216.93163999999999, 79.75}, {70.375, 36}} + Class + ShapedGraphic + ID + 88 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 MediaItem} + VerticalPad + 0 + + + + Bounds + {{451, 417}, {106, 36}} + Class + ShapedGraphic + ID + 85 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 TicketCategory} + VerticalPad + 0 + + + + Class + LineGraphic + Head + + ID + 1 + + ID + 83 + Points + + {252.11913999999999, 199.50000000000006} + {252.11913999999999, 300} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + Tail + + ID + 49 + + + + Bounds + {{207.11913999999999, 300.5}, {90, 36}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 1 + Shape + Rectangle + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\b\fs24 \cf0 Performance} + + + + Bounds + {{423.5, 18.5}, {77, 14}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 81 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Administration} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{255.55957000000001, 281}, {16, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 67 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 *} + VerticalPad + 0 + + + + Bounds + {{401, 147.5}, {16, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 66 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720 + +\f0\fs24 \cf0 *} + VerticalPad + 0 + + + + Bounds + {{316.80957000000001, 300.5}, {119, 36}} + Class + ShapedGraphic + ID + 60 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 SectionAllocation} + VerticalPad + 0 + + + + Bounds + {{455.5, 300.5}, {90, 36}} + Class + ShapedGraphic + ID + 54 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 Section} + VerticalPad + 0 + + + + Bounds + {{32, 300.5}, {127, 36}} + Class + ShapedGraphic + ID + 52 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 Booking} + VerticalPad + 0 + + + + Bounds + {{207.11913999999999, 163}, {90, 36}} + Class + ShapedGraphic + ID + 49 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 Show} + VerticalPad + 0 + + + + Bounds + {{462, 163}, {77, 36}} + Class + ShapedGraphic + ID + 4 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 Venue} + VerticalPad + 0 + + + + Class + LineGraphic + Head + + ID + 18 + + ID + 16 + Points + + {87.5, 115.75} + {87.5, 163} + + Style + + stroke + + HeadArrow + 0 + Legacy + + LineType + 1 + TailArrow + StickArrow + + + Tail + + ID + 17 + + + + Bounds + {{49, 163}, {77, 36}} + Class + ShapedGraphic + ID + 18 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 Event} + VerticalPad + 0 + + + + Bounds + {{39, 79.75}, {97, 36}} + Class + ShapedGraphic + ID + 17 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 EventCategory} + VerticalPad + 0 + + + + GridInfo + + HPages + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Event-venue + UniqueID + 3 + VPages + 1 + + + ActiveLayerIndex + 0 + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {576, 733}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 0/72 in = 1.0000 in + GraphicsList + + + Bounds + {{300, 81}, {77, 36}} + Class + ShapedGraphic + ID + 90 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 MediaType} + VerticalPad + 0 + + + + Bounds + {{196, 81}, {70.375, 36}} + Class + ShapedGraphic + ID + 88 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 MediaItem} + VerticalPad + 0 + + + + Bounds + {{196, 156}, {77, 36}} + Class + ShapedGraphic + ID + 18 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\b\fs24 \cf0 Event} + VerticalPad + 0 + + + + GridInfo + + HPages + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + Orientation + 2 + PrintOnePage + + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Event metadata + UniqueID + 4 + VPages + 1 + + + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UseEntirePage + + WindowInfo + + CurrentSheet + 0 + ExpandedCanvases + + + name + Canvas 1 + + + Frame + {{127, 73}, {1098, 673}} + ListView + + OutlineWidth + 142 + RightSidebar + + ShowRuler + + Sidebar + + SidebarWidth + 120 + VisibleRegion + {{-186, 0}, {949, 534}} + Zoom + 1 + ZoomValues + + + Canvas 1 + 1 + 1 + + + Canvas 2 + 1 + 1 + + + Event-venue + 1 + 1 + + + Event metadata + 1 + 1 + + + + + diff --git a/tutorial/custom-asciidoc-dblatex.sty b/tutorial/custom-asciidoc-dblatex.sty new file mode 100644 index 000000000..d4ad14273 --- /dev/null +++ b/tutorial/custom-asciidoc-dblatex.sty @@ -0,0 +1,78 @@ +%% +%% This style is derived from the docbook one. +%% +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{asciidoc}[2008/06/05 AsciiDoc DocBook Style] +%% Just use the original package and pass the options. +\RequirePackageWithOptions{docbook} + +% Sidebar is a boxed minipage that can contain verbatim. +% Changed shadow box to double box. +\renewenvironment{sidebar}[1][0.95\textwidth]{ + \hspace{0mm}\newline% + \noindent\begin{Sbox}\begin{minipage}{#1}% + \setlength\parskip{\medskipamount}% +}{ + \end{minipage}\end{Sbox}\doublebox{\TheSbox}% +} + +% For DocBook literallayout elements, see `./dblatex/dblatex-readme.txt`. +\usepackage{alltt} + +% Set custom colors +\definecolor{code}{gray}{0} +\definecolor{canvas}{gray}{0.96} +\definecolor{comment}{rgb}{0, 0.456, 0} +\definecolor{keyword}{rgb}{0.5, 0, 0.5} + +% To handle upquotes in the docbook +\usepackage{upquote} + +% For source listings +\usepackage{fancyvrb} +\usepackage[T1]{fontenc} + +% JavaScript is not part of the listings package... +% but thankfully this page shows how to add it: +% http://lenaherrmann.net/2010/05/20/javascript-syntax-highlighting-in-the-latex-listings-package + +\lstdefinelanguage{javascript}{ + keywords={typeof, new, true, false, catch, function, return, null, catch, switch, var, if, for, in, while, do, else, case, break}, + ndkeywords={class, export, boolean, throw, implements, import, this}, + sensitive=false, + comment=[l]{//}, + morecomment=[s][\color{blue}\ttfamily]{/*}{*/}, + morestring=[b]', + morestring=[b]" +} + +% Forge Shell Script +% Add other Forge plugins as keywords if needed +\lstdefinelanguage{fsh}{ + keywords={forge, project, entity, field, validation, constraint, scaffold, richfaces}, + sensitive=false, + comment=[s][\color{blue}\ttfamily]{@/*}{*/}, + morestring=[b]', + morestring=[b]", +} + +% Parameters for formatting code (affects all code listings) +% Break at whitespace instead of arbitrary characters +% Prevents arrows from occurring on line breaks +\lstset{ + backgroundcolor=\color{canvas}, + basicstyle=\ttfamily\small\color{code}, + commentstyle=\color{comment}, + identifierstyle=\color{black}, + keywordstyle=\color{keyword}\bfseries, + ndkeywordstyle=\color{keyword}\bfseries, + stringstyle=\color{blue}\ttfamily, + showspaces=false, + breakatwhitespace=true, + breaklines=true, + showstringspaces=false, + stringstyle=\itshape, + keepspaces=true, + captionpos=t, + prebreak=\space +} \ No newline at end of file diff --git a/tutorial/generate-guides.sh b/tutorial/generate-guides.sh new file mode 100755 index 000000000..583a50ad7 --- /dev/null +++ b/tutorial/generate-guides.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Require BASH 3 or newer + +REQUIRED_BASH_VERSION=3.0.0 + +if [[ $BASH_VERSION < $REQUIRED_BASH_VERSION ]]; then + echo "You must use Bash version 3 or newer to run this script" + exit +fi + +# Canonicalise the source dir, allow this script to be called anywhere +DIR=$(cd -P -- "$(dirname -- "$0")" && pwd -P) + +# DEFINE + +TARGET=target/guides +MASTER=ticket-monster.asciidoc + +OUTPUT_FORMATS=("xml" "epub" "pdf") +OUTPUT_CMDS=("asciidoctor -d book -b docbook -o \${output_filename} \$MASTER" "a2x -d book -f epub -D \$dir \$MASTER" "a2x -d book -f pdf --dblatex-opts \"-s custom-asciidoc-dblatex.sty -P latex.output.revhistory=0\" -D \$dir \$MASTER") + +echo "** Building tutorial" + +echo "**** Cleaning $TARGET" +rm -rf $TARGET +mkdir -p $TARGET + +output_format=html +dir=$TARGET/$output_format +mkdir -p $dir +echo "**** Copying shared resources to $dir" +cp -r gfx $dir + +for file in *.asciidoc +do + output_filename=$dir/${file//.asciidoc/.$output_format} + echo "**** Processing $file > ${output_filename}" + asciidoctor -d book -b html5 -a toc2 -a copycss -a source-highlighter=highlightjs -o ${output_filename} $file +done + +for ((i=0; i < ${#OUTPUT_FORMATS[@]}; i++)) +do + output_format=${OUTPUT_FORMATS[i]} + dir=$TARGET/$output_format + output_filename=$dir/${file//.asciidoc/.$output_format} + mkdir -p $dir + echo "**** Copying shared resources to $dir" + cp -r gfx $dir + echo "**** Processing $file > ${output_filename}" + eval ${OUTPUT_CMDS[i]} +done + diff --git a/tutorial/gfx/add-cordova-config.png b/tutorial/gfx/add-cordova-config.png new file mode 100644 index 000000000..8682bd22c Binary files /dev/null and b/tutorial/gfx/add-cordova-config.png differ diff --git a/tutorial/gfx/add-cordova-jar.png b/tutorial/gfx/add-cordova-jar.png new file mode 100644 index 000000000..9dc3a28c8 Binary files /dev/null and b/tutorial/gfx/add-cordova-jar.png differ diff --git a/tutorial/gfx/add-mysql-embedded-cartridge.png b/tutorial/gfx/add-mysql-embedded-cartridge.png new file mode 100644 index 000000000..6a2556d63 Binary files /dev/null and b/tutorial/gfx/add-mysql-embedded-cartridge.png differ diff --git a/tutorial/gfx/add-postgresql-embedded-cartridge.png b/tutorial/gfx/add-postgresql-embedded-cartridge.png new file mode 100644 index 000000000..e7867dad2 Binary files /dev/null and b/tutorial/gfx/add-postgresql-embedded-cartridge.png differ diff --git a/tutorial/gfx/android-activity-name.png b/tutorial/gfx/android-activity-name.png new file mode 100644 index 000000000..540dd335e Binary files /dev/null and b/tutorial/gfx/android-activity-name.png differ diff --git a/tutorial/gfx/android-activity-type.png b/tutorial/gfx/android-activity-type.png new file mode 100644 index 000000000..dd2754dd1 Binary files /dev/null and b/tutorial/gfx/android-activity-type.png differ diff --git a/tutorial/gfx/android-app-project-package.png b/tutorial/gfx/android-app-project-package.png new file mode 100644 index 000000000..c22b7fd11 Binary files /dev/null and b/tutorial/gfx/android-app-project-package.png differ diff --git a/tutorial/gfx/android-emulator.png b/tutorial/gfx/android-emulator.png new file mode 100644 index 000000000..31220a680 Binary files /dev/null and b/tutorial/gfx/android-emulator.png differ diff --git a/tutorial/gfx/backbone-usage.png b/tutorial/gfx/backbone-usage.png new file mode 100644 index 000000000..fa2783bc8 Binary files /dev/null and b/tutorial/gfx/backbone-usage.png differ diff --git a/tutorial/gfx/completed-deployment-openshift.png b/tutorial/gfx/completed-deployment-openshift.png new file mode 100644 index 000000000..7d65f8581 Binary files /dev/null and b/tutorial/gfx/completed-deployment-openshift.png differ diff --git a/tutorial/gfx/completed-fields-application-wizard-step2.png b/tutorial/gfx/completed-fields-application-wizard-step2.png new file mode 100644 index 000000000..a5a2189f2 Binary files /dev/null and b/tutorial/gfx/completed-fields-application-wizard-step2.png differ diff --git a/tutorial/gfx/completed-fields-application-wizard-step3.png b/tutorial/gfx/completed-fields-application-wizard-step3.png new file mode 100644 index 000000000..94c68fcc1 Binary files /dev/null and b/tutorial/gfx/completed-fields-application-wizard-step3.png differ diff --git a/tutorial/gfx/completed-fields-application-wizard.png b/tutorial/gfx/completed-fields-application-wizard.png new file mode 100644 index 000000000..ec35de692 Binary files /dev/null and b/tutorial/gfx/completed-fields-application-wizard.png differ diff --git a/tutorial/gfx/completed-username-password-fields.png b/tutorial/gfx/completed-username-password-fields.png new file mode 100644 index 000000000..63f0110af Binary files /dev/null and b/tutorial/gfx/completed-username-password-fields.png differ diff --git a/tutorial/gfx/cordova_add_device_plugin.png b/tutorial/gfx/cordova_add_device_plugin.png new file mode 100644 index 000000000..8aadff8d7 Binary files /dev/null and b/tutorial/gfx/cordova_add_device_plugin.png differ diff --git a/tutorial/gfx/cordova_add_notifications_plugin.png b/tutorial/gfx/cordova_add_notifications_plugin.png new file mode 100644 index 000000000..d9f514fc3 Binary files /dev/null and b/tutorial/gfx/cordova_add_notifications_plugin.png differ diff --git a/tutorial/gfx/cordova_add_statusbar_plugin.png b/tutorial/gfx/cordova_add_statusbar_plugin.png new file mode 100644 index 000000000..1b6607751 Binary files /dev/null and b/tutorial/gfx/cordova_add_statusbar_plugin.png differ diff --git a/tutorial/gfx/cordova_choose_to_add_plugins.png b/tutorial/gfx/cordova_choose_to_add_plugins.png new file mode 100644 index 000000000..9a6e73ea3 Binary files /dev/null and b/tutorial/gfx/cordova_choose_to_add_plugins.png differ diff --git a/tutorial/gfx/cordova_confirm_plugin_versions.png b/tutorial/gfx/cordova_confirm_plugin_versions.png new file mode 100644 index 000000000..e40355cfb Binary files /dev/null and b/tutorial/gfx/cordova_confirm_plugin_versions.png differ diff --git a/tutorial/gfx/cordovasim.png b/tutorial/gfx/cordovasim.png new file mode 100644 index 000000000..a540b3c6e Binary files /dev/null and b/tutorial/gfx/cordovasim.png differ diff --git a/tutorial/gfx/create-new-hybrid-mobile-application-project.png b/tutorial/gfx/create-new-hybrid-mobile-application-project.png new file mode 100644 index 000000000..5861526fc Binary files /dev/null and b/tutorial/gfx/create-new-hybrid-mobile-application-project.png differ diff --git a/tutorial/gfx/create_new_folder.png b/tutorial/gfx/create_new_folder.png new file mode 100644 index 000000000..baad9092b Binary files /dev/null and b/tutorial/gfx/create_new_folder.png differ diff --git a/tutorial/gfx/create_www_folder.png b/tutorial/gfx/create_www_folder.png new file mode 100644 index 000000000..c0a36dd51 Binary files /dev/null and b/tutorial/gfx/create_www_folder.png differ diff --git a/tutorial/gfx/database-design.png b/tutorial/gfx/database-design.png new file mode 100644 index 000000000..a745f16ae Binary files /dev/null and b/tutorial/gfx/database-design.png differ diff --git a/tutorial/gfx/eclipse-generate-hashcode-equals-2.png b/tutorial/gfx/eclipse-generate-hashcode-equals-2.png new file mode 100755 index 000000000..7a204c100 Binary files /dev/null and b/tutorial/gfx/eclipse-generate-hashcode-equals-2.png differ diff --git a/tutorial/gfx/eclipse-generate-hashcode-equals.png b/tutorial/gfx/eclipse-generate-hashcode-equals.png new file mode 100755 index 000000000..ce0c45ced Binary files /dev/null and b/tutorial/gfx/eclipse-generate-hashcode-equals.png differ diff --git a/tutorial/gfx/eclipse-green-bar.png b/tutorial/gfx/eclipse-green-bar.png new file mode 100644 index 000000000..1056ab5bb Binary files /dev/null and b/tutorial/gfx/eclipse-green-bar.png differ diff --git a/tutorial/gfx/eclipse-maven-profile-update.png b/tutorial/gfx/eclipse-maven-profile-update.png new file mode 100755 index 000000000..c893c9df0 Binary files /dev/null and b/tutorial/gfx/eclipse-maven-profile-update.png differ diff --git a/tutorial/gfx/eclipse-project-maven-profiles.png b/tutorial/gfx/eclipse-project-maven-profiles.png new file mode 100755 index 000000000..215684d7b Binary files /dev/null and b/tutorial/gfx/eclipse-project-maven-profiles.png differ diff --git a/tutorial/gfx/edit-embedded-cartridge-openshift.png b/tutorial/gfx/edit-embedded-cartridge-openshift.png new file mode 100644 index 000000000..7c3996040 Binary files /dev/null and b/tutorial/gfx/edit-embedded-cartridge-openshift.png differ diff --git a/tutorial/gfx/find-hybrid-mobile-tools-cordovasim.png b/tutorial/gfx/find-hybrid-mobile-tools-cordovasim.png new file mode 100644 index 000000000..866b4a071 Binary files /dev/null and b/tutorial/gfx/find-hybrid-mobile-tools-cordovasim.png differ diff --git a/tutorial/gfx/forge-project-list-facets.png b/tutorial/gfx/forge-project-list-facets.png new file mode 100644 index 000000000..36f5c29b7 Binary files /dev/null and b/tutorial/gfx/forge-project-list-facets.png differ diff --git a/tutorial/gfx/forge-scaffold-setup.png b/tutorial/gfx/forge-scaffold-setup.png new file mode 100644 index 000000000..0dd698a3e Binary files /dev/null and b/tutorial/gfx/forge-scaffold-setup.png differ diff --git a/tutorial/gfx/forge_scaffold_generate.png b/tutorial/gfx/forge_scaffold_generate.png new file mode 100644 index 000000000..12ce8912b Binary files /dev/null and b/tutorial/gfx/forge_scaffold_generate.png differ diff --git a/tutorial/gfx/forge_scaffold_generate_action_menu.png b/tutorial/gfx/forge_scaffold_generate_action_menu.png new file mode 100755 index 000000000..706841c57 Binary files /dev/null and b/tutorial/gfx/forge_scaffold_generate_action_menu.png differ diff --git a/tutorial/gfx/forge_scaffold_generate_choose_rest_strategy.png b/tutorial/gfx/forge_scaffold_generate_choose_rest_strategy.png new file mode 100755 index 000000000..70db8f724 Binary files /dev/null and b/tutorial/gfx/forge_scaffold_generate_choose_rest_strategy.png differ diff --git a/tutorial/gfx/forge_scaffold_generate_input_webroot.png b/tutorial/gfx/forge_scaffold_generate_input_webroot.png new file mode 100755 index 000000000..78452e75f Binary files /dev/null and b/tutorial/gfx/forge_scaffold_generate_input_webroot.png differ diff --git a/tutorial/gfx/forge_scaffold_generate_select_entities.png b/tutorial/gfx/forge_scaffold_generate_select_entities.png new file mode 100644 index 000000000..b213dcde4 Binary files /dev/null and b/tutorial/gfx/forge_scaffold_generate_select_entities.png differ diff --git a/tutorial/gfx/git-remote-connection-timeout.png b/tutorial/gfx/git-remote-connection-timeout.png new file mode 100644 index 000000000..e30c026e7 Binary files /dev/null and b/tutorial/gfx/git-remote-connection-timeout.png differ diff --git a/tutorial/gfx/h2console_settings.png b/tutorial/gfx/h2console_settings.png new file mode 100644 index 000000000..f3d68ea2e Binary files /dev/null and b/tutorial/gfx/h2console_settings.png differ diff --git a/tutorial/gfx/hybrid-mobile-pane-preferences-window.png b/tutorial/gfx/hybrid-mobile-pane-preferences-window.png new file mode 100644 index 000000000..cfb928f2d Binary files /dev/null and b/tutorial/gfx/hybrid-mobile-pane-preferences-window.png differ diff --git a/tutorial/gfx/import-openShift-application-prompt.png b/tutorial/gfx/import-openShift-application-prompt.png new file mode 100644 index 000000000..21ece4775 Binary files /dev/null and b/tutorial/gfx/import-openShift-application-prompt.png differ diff --git a/tutorial/gfx/introduction/as_eap_found.png b/tutorial/gfx/introduction/as_eap_found.png new file mode 100755 index 000000000..c06905367 Binary files /dev/null and b/tutorial/gfx/introduction/as_eap_found.png differ diff --git a/tutorial/gfx/introduction/as_eap_selected.png b/tutorial/gfx/introduction/as_eap_selected.png new file mode 100755 index 000000000..d2fd7f4c9 Binary files /dev/null and b/tutorial/gfx/introduction/as_eap_selected.png differ diff --git a/tutorial/gfx/introduction/event_service_copy_paste.png b/tutorial/gfx/introduction/event_service_copy_paste.png new file mode 100755 index 000000000..27b0405cf Binary files /dev/null and b/tutorial/gfx/introduction/event_service_copy_paste.png differ diff --git a/tutorial/gfx/introduction/forge_action_menu.png b/tutorial/gfx/introduction/forge_action_menu.png new file mode 100755 index 000000000..5e438fc1f Binary files /dev/null and b/tutorial/gfx/introduction/forge_action_menu.png differ diff --git a/tutorial/gfx/introduction/forge_add_constraint_on_event.png b/tutorial/gfx/introduction/forge_add_constraint_on_event.png new file mode 100755 index 000000000..8ca021c75 Binary files /dev/null and b/tutorial/gfx/introduction/forge_add_constraint_on_event.png differ diff --git a/tutorial/gfx/introduction/forge_added_name.png b/tutorial/gfx/introduction/forge_added_name.png new file mode 100755 index 000000000..31576dd47 Binary files /dev/null and b/tutorial/gfx/introduction/forge_added_name.png differ diff --git a/tutorial/gfx/introduction/forge_console_tab.png b/tutorial/gfx/introduction/forge_console_tab.png new file mode 100755 index 000000000..8b090e7c2 Binary files /dev/null and b/tutorial/gfx/introduction/forge_console_tab.png differ diff --git a/tutorial/gfx/introduction/forge_constraint_add_notnull_on_name.png b/tutorial/gfx/introduction/forge_constraint_add_notnull_on_name.png new file mode 100755 index 000000000..2a5d78063 Binary files /dev/null and b/tutorial/gfx/introduction/forge_constraint_add_notnull_on_name.png differ diff --git a/tutorial/gfx/introduction/forge_constraint_add_set_size_attributes_on_description.png b/tutorial/gfx/introduction/forge_constraint_add_set_size_attributes_on_description.png new file mode 100755 index 000000000..fff139e77 Binary files /dev/null and b/tutorial/gfx/introduction/forge_constraint_add_set_size_attributes_on_description.png differ diff --git a/tutorial/gfx/introduction/forge_constraint_add_set_size_attributes_on_name.png b/tutorial/gfx/introduction/forge_constraint_add_set_size_attributes_on_name.png new file mode 100755 index 000000000..806a0f7db Binary files /dev/null and b/tutorial/gfx/introduction/forge_constraint_add_set_size_attributes_on_name.png differ diff --git a/tutorial/gfx/introduction/forge_constraint_add_size_on_description.png b/tutorial/gfx/introduction/forge_constraint_add_size_on_description.png new file mode 100755 index 000000000..c6c66e95e Binary files /dev/null and b/tutorial/gfx/introduction/forge_constraint_add_size_on_description.png differ diff --git a/tutorial/gfx/introduction/forge_constraint_add_size_on_name.png b/tutorial/gfx/introduction/forge_constraint_add_size_on_name.png new file mode 100755 index 000000000..643995fa5 Binary files /dev/null and b/tutorial/gfx/introduction/forge_constraint_add_size_on_name.png differ diff --git a/tutorial/gfx/introduction/forge_event_entity_source.png b/tutorial/gfx/introduction/forge_event_entity_source.png new file mode 100755 index 000000000..f099b8910 Binary files /dev/null and b/tutorial/gfx/introduction/forge_event_entity_source.png differ diff --git a/tutorial/gfx/introduction/forge_is_starting.png b/tutorial/gfx/introduction/forge_is_starting.png new file mode 100755 index 000000000..ce9e47620 Binary files /dev/null and b/tutorial/gfx/introduction/forge_is_starting.png differ diff --git a/tutorial/gfx/introduction/forge_jpa_new_entity.png b/tutorial/gfx/introduction/forge_jpa_new_entity.png new file mode 100755 index 000000000..3b43fdbbc Binary files /dev/null and b/tutorial/gfx/introduction/forge_jpa_new_entity.png differ diff --git a/tutorial/gfx/introduction/forge_jpa_new_entity_created.png b/tutorial/gfx/introduction/forge_jpa_new_entity_created.png new file mode 100755 index 000000000..a3248a794 Binary files /dev/null and b/tutorial/gfx/introduction/forge_jpa_new_entity_created.png differ diff --git a/tutorial/gfx/introduction/forge_jpa_new_entity_event.png b/tutorial/gfx/introduction/forge_jpa_new_entity_event.png new file mode 100755 index 000000000..93d085135 Binary files /dev/null and b/tutorial/gfx/introduction/forge_jpa_new_entity_event.png differ diff --git a/tutorial/gfx/introduction/forge_jpa_new_field_description.png b/tutorial/gfx/introduction/forge_jpa_new_field_description.png new file mode 100755 index 000000000..35bdce83c Binary files /dev/null and b/tutorial/gfx/introduction/forge_jpa_new_field_description.png differ diff --git a/tutorial/gfx/introduction/forge_jpa_new_field_major.png b/tutorial/gfx/introduction/forge_jpa_new_field_major.png new file mode 100755 index 000000000..69264af18 Binary files /dev/null and b/tutorial/gfx/introduction/forge_jpa_new_field_major.png differ diff --git a/tutorial/gfx/introduction/forge_jpa_new_field_name.png b/tutorial/gfx/introduction/forge_jpa_new_field_name.png new file mode 100755 index 000000000..0157a92ea Binary files /dev/null and b/tutorial/gfx/introduction/forge_jpa_new_field_name.png differ diff --git a/tutorial/gfx/introduction/forge_jpa_new_field_picture.png b/tutorial/gfx/introduction/forge_jpa_new_field_picture.png new file mode 100755 index 000000000..b98a4506f Binary files /dev/null and b/tutorial/gfx/introduction/forge_jpa_new_field_picture.png differ diff --git a/tutorial/gfx/introduction/forge_quick_action_menu_filter_constraint.png b/tutorial/gfx/introduction/forge_quick_action_menu_filter_constraint.png new file mode 100755 index 000000000..117d2ff44 Binary files /dev/null and b/tutorial/gfx/introduction/forge_quick_action_menu_filter_constraint.png differ diff --git a/tutorial/gfx/introduction/forge_quick_action_menu_filter_jpa.png b/tutorial/gfx/introduction/forge_quick_action_menu_filter_jpa.png new file mode 100755 index 000000000..d7a9c6be6 Binary files /dev/null and b/tutorial/gfx/introduction/forge_quick_action_menu_filter_jpa.png differ diff --git a/tutorial/gfx/introduction/forge_select_event_entity_for_constraint.png b/tutorial/gfx/introduction/forge_select_event_entity_for_constraint.png new file mode 100755 index 000000000..6c77fb875 Binary files /dev/null and b/tutorial/gfx/introduction/forge_select_event_entity_for_constraint.png differ diff --git a/tutorial/gfx/introduction/forge_setup_constraint_wizard.png b/tutorial/gfx/introduction/forge_setup_constraint_wizard.png new file mode 100755 index 000000000..6706db2be Binary files /dev/null and b/tutorial/gfx/introduction/forge_setup_constraint_wizard.png differ diff --git a/tutorial/gfx/introduction/full_publish.png b/tutorial/gfx/introduction/full_publish.png new file mode 100755 index 000000000..6b174e759 Binary files /dev/null and b/tutorial/gfx/introduction/full_publish.png differ diff --git a/tutorial/gfx/introduction/generate_getters_setters.png b/tutorial/gfx/introduction/generate_getters_setters.png new file mode 100755 index 000000000..8ad94866d Binary files /dev/null and b/tutorial/gfx/introduction/generate_getters_setters.png differ diff --git a/tutorial/gfx/introduction/getter_setter_dialog.png b/tutorial/gfx/introduction/getter_setter_dialog.png new file mode 100755 index 000000000..dfb7267e1 Binary files /dev/null and b/tutorial/gfx/introduction/getter_setter_dialog.png differ diff --git a/tutorial/gfx/introduction/h2console_deployments.png b/tutorial/gfx/introduction/h2console_deployments.png new file mode 100644 index 000000000..7c2557a35 Binary files /dev/null and b/tutorial/gfx/introduction/h2console_deployments.png differ diff --git a/tutorial/gfx/introduction/h2console_in_browser.png b/tutorial/gfx/introduction/h2console_in_browser.png new file mode 100644 index 000000000..fa9df10d8 Binary files /dev/null and b/tutorial/gfx/introduction/h2console_in_browser.png differ diff --git a/tutorial/gfx/introduction/h2console_select_from_event.png b/tutorial/gfx/introduction/h2console_select_from_event.png new file mode 100644 index 000000000..6bedcc406 Binary files /dev/null and b/tutorial/gfx/introduction/h2console_select_from_event.png differ diff --git a/tutorial/gfx/introduction/hibernate_add_jpa_annotations.png b/tutorial/gfx/introduction/hibernate_add_jpa_annotations.png new file mode 100755 index 000000000..6e245c540 Binary files /dev/null and b/tutorial/gfx/introduction/hibernate_add_jpa_annotations.png differ diff --git a/tutorial/gfx/introduction/hibernate_add_jpa_annotations_step2.png b/tutorial/gfx/introduction/hibernate_add_jpa_annotations_step2.png new file mode 100755 index 000000000..ea2b5be01 Binary files /dev/null and b/tutorial/gfx/introduction/hibernate_add_jpa_annotations_step2.png differ diff --git a/tutorial/gfx/introduction/installer_wizard_page1.png b/tutorial/gfx/introduction/installer_wizard_page1.png new file mode 100755 index 000000000..22860e4d6 Binary files /dev/null and b/tutorial/gfx/introduction/installer_wizard_page1.png differ diff --git a/tutorial/gfx/introduction/jbds8_mobile_browsersim.png b/tutorial/gfx/introduction/jbds8_mobile_browsersim.png new file mode 100644 index 000000000..c7357b9a4 Binary files /dev/null and b/tutorial/gfx/introduction/jbds8_mobile_browsersim.png differ diff --git a/tutorial/gfx/introduction/jboss_dev_studio_jboss_central.png b/tutorial/gfx/introduction/jboss_dev_studio_jboss_central.png new file mode 100755 index 000000000..0b9119eeb Binary files /dev/null and b/tutorial/gfx/introduction/jboss_dev_studio_jboss_central.png differ diff --git a/tutorial/gfx/introduction/jboss_maven_repo_settings_xml.png b/tutorial/gfx/introduction/jboss_maven_repo_settings_xml.png new file mode 100755 index 000000000..7ebd67bd5 Binary files /dev/null and b/tutorial/gfx/introduction/jboss_maven_repo_settings_xml.png differ diff --git a/tutorial/gfx/introduction/jboss_maven_repository.png b/tutorial/gfx/introduction/jboss_maven_repository.png new file mode 100755 index 000000000..d078cede6 Binary files /dev/null and b/tutorial/gfx/introduction/jboss_maven_repository.png differ diff --git a/tutorial/gfx/introduction/jboss_tools_runtime_detection.png b/tutorial/gfx/introduction/jboss_tools_runtime_detection.png new file mode 100755 index 000000000..5a669d831 Binary files /dev/null and b/tutorial/gfx/introduction/jboss_tools_runtime_detection.png differ diff --git a/tutorial/gfx/introduction/jboss_tools_runtime_detection_after.png b/tutorial/gfx/introduction/jboss_tools_runtime_detection_after.png new file mode 100755 index 000000000..799ba4195 Binary files /dev/null and b/tutorial/gfx/introduction/jboss_tools_runtime_detection_after.png differ diff --git a/tutorial/gfx/introduction/jquery_mobile_listview.png b/tutorial/gfx/introduction/jquery_mobile_listview.png new file mode 100755 index 000000000..50468cffa Binary files /dev/null and b/tutorial/gfx/introduction/jquery_mobile_listview.png differ diff --git a/tutorial/gfx/introduction/jquery_mobile_listview_widget.png b/tutorial/gfx/introduction/jquery_mobile_listview_widget.png new file mode 100755 index 000000000..ddf752c61 Binary files /dev/null and b/tutorial/gfx/introduction/jquery_mobile_listview_widget.png differ diff --git a/tutorial/gfx/introduction/jquery_mobile_page.png b/tutorial/gfx/introduction/jquery_mobile_page.png new file mode 100755 index 000000000..181bd3832 Binary files /dev/null and b/tutorial/gfx/introduction/jquery_mobile_page.png differ diff --git a/tutorial/gfx/introduction/jquery_mobile_page_widget.png b/tutorial/gfx/introduction/jquery_mobile_page_widget.png new file mode 100755 index 000000000..da7a6d62b Binary files /dev/null and b/tutorial/gfx/introduction/jquery_mobile_page_widget.png differ diff --git a/tutorial/gfx/introduction/jquery_mobile_palette.png b/tutorial/gfx/introduction/jquery_mobile_palette.png new file mode 100755 index 000000000..40d6835cf Binary files /dev/null and b/tutorial/gfx/introduction/jquery_mobile_palette.png differ diff --git a/tutorial/gfx/introduction/jquery_mobile_results.png b/tutorial/gfx/introduction/jquery_mobile_results.png new file mode 100644 index 000000000..260df06da Binary files /dev/null and b/tutorial/gfx/introduction/jquery_mobile_results.png differ diff --git a/tutorial/gfx/introduction/jquery_mobile_template.png b/tutorial/gfx/introduction/jquery_mobile_template.png new file mode 100644 index 000000000..366f4c6c8 Binary files /dev/null and b/tutorial/gfx/introduction/jquery_mobile_template.png differ diff --git a/tutorial/gfx/introduction/js_css_widget.png b/tutorial/gfx/introduction/js_css_widget.png new file mode 100755 index 000000000..a501ea7c4 Binary files /dev/null and b/tutorial/gfx/introduction/js_css_widget.png differ diff --git a/tutorial/gfx/introduction/js_css_widget_library_versions.png b/tutorial/gfx/introduction/js_css_widget_library_versions.png new file mode 100755 index 000000000..e8957396e Binary files /dev/null and b/tutorial/gfx/introduction/js_css_widget_library_versions.png differ diff --git a/tutorial/gfx/introduction/json_event_results.png b/tutorial/gfx/introduction/json_event_results.png new file mode 100644 index 000000000..d333ba13b Binary files /dev/null and b/tutorial/gfx/introduction/json_event_results.png differ diff --git a/tutorial/gfx/introduction/mobile_browsersim.png b/tutorial/gfx/introduction/mobile_browsersim.png new file mode 100644 index 000000000..2323e70ee Binary files /dev/null and b/tutorial/gfx/introduction/mobile_browsersim.png differ diff --git a/tutorial/gfx/introduction/mobile_browsersim_bofa_source.png b/tutorial/gfx/introduction/mobile_browsersim_bofa_source.png new file mode 100644 index 000000000..df23c86b9 Binary files /dev/null and b/tutorial/gfx/introduction/mobile_browsersim_bofa_source.png differ diff --git a/tutorial/gfx/introduction/mobile_browsersim_custom_devices.png b/tutorial/gfx/introduction/mobile_browsersim_custom_devices.png new file mode 100644 index 000000000..72588762d Binary files /dev/null and b/tutorial/gfx/introduction/mobile_browsersim_custom_devices.png differ diff --git a/tutorial/gfx/introduction/mobile_browsersim_devices_menu.png b/tutorial/gfx/introduction/mobile_browsersim_devices_menu.png new file mode 100644 index 000000000..5e768c9e5 Binary files /dev/null and b/tutorial/gfx/introduction/mobile_browsersim_devices_menu.png differ diff --git a/tutorial/gfx/introduction/mobile_browsersim_in_toolbar.png b/tutorial/gfx/introduction/mobile_browsersim_in_toolbar.png new file mode 100644 index 000000000..db709a691 Binary files /dev/null and b/tutorial/gfx/introduction/mobile_browsersim_in_toolbar.png differ diff --git a/tutorial/gfx/introduction/mobile_browsersim_windows_menu.png b/tutorial/gfx/introduction/mobile_browsersim_windows_menu.png new file mode 100644 index 000000000..24ed6a5bf Binary files /dev/null and b/tutorial/gfx/introduction/mobile_browsersim_windows_menu.png differ diff --git a/tutorial/gfx/introduction/new_class_eventservice.png b/tutorial/gfx/introduction/new_class_eventservice.png new file mode 100755 index 000000000..035a4c42d Binary files /dev/null and b/tutorial/gfx/introduction/new_class_eventservice.png differ diff --git a/tutorial/gfx/introduction/new_html_file.png b/tutorial/gfx/introduction/new_html_file.png new file mode 100755 index 000000000..617414bf1 Binary files /dev/null and b/tutorial/gfx/introduction/new_html_file.png differ diff --git a/tutorial/gfx/introduction/new_html_file_correct_location.png b/tutorial/gfx/introduction/new_html_file_correct_location.png new file mode 100755 index 000000000..62dba6220 Binary files /dev/null and b/tutorial/gfx/introduction/new_html_file_correct_location.png differ diff --git a/tutorial/gfx/introduction/new_html_file_m2e_wtp.png b/tutorial/gfx/introduction/new_html_file_m2e_wtp.png new file mode 100644 index 000000000..250734c8e Binary files /dev/null and b/tutorial/gfx/introduction/new_html_file_m2e_wtp.png differ diff --git a/tutorial/gfx/introduction/new_project_example_step_2.png b/tutorial/gfx/introduction/new_project_example_step_2.png new file mode 100755 index 000000000..2af696410 Binary files /dev/null and b/tutorial/gfx/introduction/new_project_example_step_2.png differ diff --git a/tutorial/gfx/introduction/new_project_wizard.png b/tutorial/gfx/introduction/new_project_wizard.png new file mode 100755 index 000000000..67ea99174 Binary files /dev/null and b/tutorial/gfx/introduction/new_project_wizard.png differ diff --git a/tutorial/gfx/introduction/newly_generated_project_explorer.png b/tutorial/gfx/introduction/newly_generated_project_explorer.png new file mode 100755 index 000000000..315ceb029 Binary files /dev/null and b/tutorial/gfx/introduction/newly_generated_project_explorer.png differ diff --git a/tutorial/gfx/introduction/organize_imports_1.png b/tutorial/gfx/introduction/organize_imports_1.png new file mode 100644 index 000000000..522413c64 Binary files /dev/null and b/tutorial/gfx/introduction/organize_imports_1.png differ diff --git a/tutorial/gfx/introduction/organize_imports_2.png b/tutorial/gfx/introduction/organize_imports_2.png new file mode 100644 index 000000000..0d8830659 Binary files /dev/null and b/tutorial/gfx/introduction/organize_imports_2.png differ diff --git a/tutorial/gfx/introduction/organize_imports_3.png b/tutorial/gfx/introduction/organize_imports_3.png new file mode 100644 index 000000000..0bf4ba6a3 Binary files /dev/null and b/tutorial/gfx/introduction/organize_imports_3.png differ diff --git a/tutorial/gfx/introduction/organize_imports_4.png b/tutorial/gfx/introduction/organize_imports_4.png new file mode 100644 index 000000000..efca420b4 Binary files /dev/null and b/tutorial/gfx/introduction/organize_imports_4.png differ diff --git a/tutorial/gfx/introduction/organize_imports_5.png b/tutorial/gfx/introduction/organize_imports_5.png new file mode 100644 index 000000000..c80b9426a Binary files /dev/null and b/tutorial/gfx/introduction/organize_imports_5.png differ diff --git a/tutorial/gfx/introduction/organize_imports_6.png b/tutorial/gfx/introduction/organize_imports_6.png new file mode 100644 index 000000000..9ccb0c8c6 Binary files /dev/null and b/tutorial/gfx/introduction/organize_imports_6.png differ diff --git a/tutorial/gfx/introduction/outline_of_event.png b/tutorial/gfx/introduction/outline_of_event.png new file mode 100755 index 000000000..2d75865b6 Binary files /dev/null and b/tutorial/gfx/introduction/outline_of_event.png differ diff --git a/tutorial/gfx/introduction/pom_xml_tabs.png b/tutorial/gfx/introduction/pom_xml_tabs.png new file mode 100755 index 000000000..6b48f771b Binary files /dev/null and b/tutorial/gfx/introduction/pom_xml_tabs.png differ diff --git a/tutorial/gfx/introduction/project_explorer_java_packages.png b/tutorial/gfx/introduction/project_explorer_java_packages.png new file mode 100755 index 000000000..62b6094ce Binary files /dev/null and b/tutorial/gfx/introduction/project_explorer_java_packages.png differ diff --git a/tutorial/gfx/introduction/project_explorer_jax_rs_services.png b/tutorial/gfx/introduction/project_explorer_jax_rs_services.png new file mode 100755 index 000000000..8ee513b02 Binary files /dev/null and b/tutorial/gfx/introduction/project_explorer_jax_rs_services.png differ diff --git a/tutorial/gfx/introduction/project_explorer_resources.png b/tutorial/gfx/introduction/project_explorer_resources.png new file mode 100755 index 000000000..4778884c9 Binary files /dev/null and b/tutorial/gfx/introduction/project_explorer_resources.png differ diff --git a/tutorial/gfx/introduction/prompt_for_cheatsheet.png b/tutorial/gfx/introduction/prompt_for_cheatsheet.png new file mode 100755 index 000000000..c38d98049 Binary files /dev/null and b/tutorial/gfx/introduction/prompt_for_cheatsheet.png differ diff --git a/tutorial/gfx/introduction/prompt_update_settings_xml.png b/tutorial/gfx/introduction/prompt_update_settings_xml.png new file mode 100755 index 000000000..06f3b5e3b Binary files /dev/null and b/tutorial/gfx/introduction/prompt_update_settings_xml.png differ diff --git a/tutorial/gfx/introduction/quickstarts_directory_layout.png b/tutorial/gfx/introduction/quickstarts_directory_layout.png new file mode 100644 index 000000000..35ab788cf Binary files /dev/null and b/tutorial/gfx/introduction/quickstarts_directory_layout.png differ diff --git a/tutorial/gfx/introduction/result_run_on_server.png b/tutorial/gfx/introduction/result_run_on_server.png new file mode 100755 index 000000000..847d62c68 Binary files /dev/null and b/tutorial/gfx/introduction/result_run_on_server.png differ diff --git a/tutorial/gfx/introduction/run_as_run_on_server.png b/tutorial/gfx/introduction/run_as_run_on_server.png new file mode 100755 index 000000000..b5830b872 Binary files /dev/null and b/tutorial/gfx/introduction/run_as_run_on_server.png differ diff --git a/tutorial/gfx/introduction/runtime_open_dialog.png b/tutorial/gfx/introduction/runtime_open_dialog.png new file mode 100755 index 000000000..487d36447 Binary files /dev/null and b/tutorial/gfx/introduction/runtime_open_dialog.png differ diff --git a/tutorial/gfx/introduction/save_modified_resources.png b/tutorial/gfx/introduction/save_modified_resources.png new file mode 100755 index 000000000..4b31fb644 Binary files /dev/null and b/tutorial/gfx/introduction/save_modified_resources.png differ diff --git a/tutorial/gfx/introduction/searching_for_runtimes_dialog.png b/tutorial/gfx/introduction/searching_for_runtimes_dialog.png new file mode 100755 index 000000000..c6db56fd3 Binary files /dev/null and b/tutorial/gfx/introduction/searching_for_runtimes_dialog.png differ diff --git a/tutorial/gfx/introduction/select_forge_view.png b/tutorial/gfx/introduction/select_forge_view.png new file mode 100644 index 000000000..afb897fbd Binary files /dev/null and b/tutorial/gfx/introduction/select_forge_view.png differ diff --git a/tutorial/gfx/introduction/select_html_template.png b/tutorial/gfx/introduction/select_html_template.png new file mode 100755 index 000000000..54a03bd7f Binary files /dev/null and b/tutorial/gfx/introduction/select_html_template.png differ diff --git a/tutorial/gfx/introduction/show_forge_view.png b/tutorial/gfx/introduction/show_forge_view.png new file mode 100644 index 000000000..0eb8ecfac Binary files /dev/null and b/tutorial/gfx/introduction/show_forge_view.png differ diff --git a/tutorial/gfx/introduction/show_in_forge_console.png b/tutorial/gfx/introduction/show_in_forge_console.png new file mode 100644 index 000000000..175397048 Binary files /dev/null and b/tutorial/gfx/introduction/show_in_forge_console.png differ diff --git a/tutorial/gfx/introduction/source_organize_imports.png b/tutorial/gfx/introduction/source_organize_imports.png new file mode 100755 index 000000000..bc2cfc800 Binary files /dev/null and b/tutorial/gfx/introduction/source_organize_imports.png differ diff --git a/tutorial/gfx/introduction/venue_after_getters_setters.png b/tutorial/gfx/introduction/venue_after_getters_setters.png new file mode 100755 index 000000000..c9c3f6b01 Binary files /dev/null and b/tutorial/gfx/introduction/venue_after_getters_setters.png differ diff --git a/tutorial/gfx/introduction/web.xml_editor.png b/tutorial/gfx/introduction/web.xml_editor.png new file mode 100644 index 000000000..9fe8054b3 Binary files /dev/null and b/tutorial/gfx/introduction/web.xml_editor.png differ diff --git a/tutorial/gfx/ios-simulator.png b/tutorial/gfx/ios-simulator.png new file mode 100644 index 000000000..f86e3824c Binary files /dev/null and b/tutorial/gfx/ios-simulator.png differ diff --git a/tutorial/gfx/jboss-eap-selected-deployment.png b/tutorial/gfx/jboss-eap-selected-deployment.png new file mode 100644 index 000000000..6c98efdcc Binary files /dev/null and b/tutorial/gfx/jboss-eap-selected-deployment.png differ diff --git a/tutorial/gfx/link-www-directory-to-webapp.png b/tutorial/gfx/link-www-directory-to-webapp.png new file mode 100644 index 000000000..d3cadce2d Binary files /dev/null and b/tutorial/gfx/link-www-directory-to-webapp.png differ diff --git a/tutorial/gfx/new-openshift-application-wizard.png b/tutorial/gfx/new-openshift-application-wizard.png new file mode 100644 index 000000000..a21a5d9f4 Binary files /dev/null and b/tutorial/gfx/new-openshift-application-wizard.png differ diff --git a/tutorial/gfx/octocat_social.png b/tutorial/gfx/octocat_social.png new file mode 100644 index 000000000..6d49b20fd Binary files /dev/null and b/tutorial/gfx/octocat_social.png differ diff --git a/tutorial/gfx/pom-file-projects-pane.png b/tutorial/gfx/pom-file-projects-pane.png new file mode 100644 index 000000000..f65b25f05 Binary files /dev/null and b/tutorial/gfx/pom-file-projects-pane.png differ diff --git a/tutorial/gfx/run-on-android-emulator.png b/tutorial/gfx/run-on-android-emulator.png new file mode 100644 index 000000000..6b30cac12 Binary files /dev/null and b/tutorial/gfx/run-on-android-emulator.png differ diff --git a/tutorial/gfx/run-on-ios-simulator.png b/tutorial/gfx/run-on-ios-simulator.png new file mode 100644 index 000000000..f25130262 Binary files /dev/null and b/tutorial/gfx/run-on-ios-simulator.png differ diff --git a/tutorial/gfx/select_hybrid_mobile_engine_for_project.png b/tutorial/gfx/select_hybrid_mobile_engine_for_project.png new file mode 100644 index 000000000..e94d14c88 Binary files /dev/null and b/tutorial/gfx/select_hybrid_mobile_engine_for_project.png differ diff --git a/tutorial/gfx/setup_hybrid_mobile_engine_340.png b/tutorial/gfx/setup_hybrid_mobile_engine_340.png new file mode 100644 index 000000000..de1f48d7f Binary files /dev/null and b/tutorial/gfx/setup_hybrid_mobile_engine_340.png differ diff --git a/tutorial/gfx/setup_hybrid_mobile_engine_from_scratch.png b/tutorial/gfx/setup_hybrid_mobile_engine_from_scratch.png new file mode 100644 index 000000000..565638e40 Binary files /dev/null and b/tutorial/gfx/setup_hybrid_mobile_engine_from_scratch.png differ diff --git a/tutorial/gfx/setup_hybrid_mobile_engine_version.png b/tutorial/gfx/setup_hybrid_mobile_engine_version.png new file mode 100644 index 000000000..f3a09fb12 Binary files /dev/null and b/tutorial/gfx/setup_hybrid_mobile_engine_version.png differ diff --git a/tutorial/gfx/setup_linked_folder_to_webapp.png b/tutorial/gfx/setup_linked_folder_to_webapp.png new file mode 100644 index 000000000..fce490d56 Binary files /dev/null and b/tutorial/gfx/setup_linked_folder_to_webapp.png differ diff --git a/tutorial/gfx/single-page-app.png b/tutorial/gfx/single-page-app.png new file mode 100644 index 000000000..e66b7b363 Binary files /dev/null and b/tutorial/gfx/single-page-app.png differ diff --git a/tutorial/gfx/start-hybrid-mobile-tools-cordovasim-installation-with-link.png b/tutorial/gfx/start-hybrid-mobile-tools-cordovasim-installation-with-link.png new file mode 100755 index 000000000..3ac5bedab Binary files /dev/null and b/tutorial/gfx/start-hybrid-mobile-tools-cordovasim-installation-with-link.png differ diff --git a/tutorial/gfx/start-new-hybrid-mobile-application-project.png b/tutorial/gfx/start-new-hybrid-mobile-application-project.png new file mode 100644 index 000000000..b992e66d5 Binary files /dev/null and b/tutorial/gfx/start-new-hybrid-mobile-application-project.png differ diff --git a/tutorial/gfx/ticket-monster-administration-use-cases.png b/tutorial/gfx/ticket-monster-administration-use-cases.png new file mode 100644 index 000000000..0add41e47 Binary files /dev/null and b/tutorial/gfx/ticket-monster-administration-use-cases.png differ diff --git a/tutorial/gfx/ticket-monster-architecture.png b/tutorial/gfx/ticket-monster-architecture.png new file mode 100755 index 000000000..84d3c424d Binary files /dev/null and b/tutorial/gfx/ticket-monster-architecture.png differ diff --git a/tutorial/gfx/ticket-monster-user-use-cases.png b/tutorial/gfx/ticket-monster-user-use-cases.png new file mode 100644 index 000000000..6a90cfb77 Binary files /dev/null and b/tutorial/gfx/ticket-monster-user-use-cases.png differ diff --git a/tutorial/gfx/ticket_monster_hybrid.png b/tutorial/gfx/ticket_monster_hybrid.png new file mode 100644 index 000000000..0e10c7a6c Binary files /dev/null and b/tutorial/gfx/ticket_monster_hybrid.png differ diff --git a/tutorial/gfx/ticketmonster-configured-jboss-eap.png b/tutorial/gfx/ticketmonster-configured-jboss-eap.png new file mode 100644 index 000000000..c31871237 Binary files /dev/null and b/tutorial/gfx/ticketmonster-configured-jboss-eap.png differ diff --git a/tutorial/gfx/ui-create-booking.png b/tutorial/gfx/ui-create-booking.png new file mode 100644 index 000000000..7ec257db0 Binary files /dev/null and b/tutorial/gfx/ui-create-booking.png differ diff --git a/tutorial/gfx/ui-event-details.png b/tutorial/gfx/ui-event-details.png new file mode 100644 index 000000000..7ec257db0 Binary files /dev/null and b/tutorial/gfx/ui-event-details.png differ diff --git a/tutorial/gfx/ui-file-structure.png b/tutorial/gfx/ui-file-structure.png new file mode 100644 index 000000000..f4dd08724 Binary files /dev/null and b/tutorial/gfx/ui-file-structure.png differ diff --git a/tutorial/gfx/warning-prompt-unsigned-content.png b/tutorial/gfx/warning-prompt-unsigned-content.png new file mode 100644 index 000000000..a869c54a6 Binary files /dev/null and b/tutorial/gfx/warning-prompt-unsigned-content.png differ diff --git a/tutorial/ticket-monster.asciidoc b/tutorial/ticket-monster.asciidoc new file mode 100644 index 000000000..12d01fac7 --- /dev/null +++ b/tutorial/ticket-monster.asciidoc @@ -0,0 +1,28 @@ += Ticket Monster Tutorial +Marius Bogoevici, Pete Muir, Burr Sutter +:doctype: book +:thumbnail: http://jboss.org/jdf/images/ticket-monster-splash-2.png + +ifdef::asciidoctor[] +// put Asciidoctor-specific settings here +endif::asciidoctor[] + +include::WhatIsTicketMonster.asciidoc[] + +include::Introduction.asciidoc[] + +include::DataPersistence.asciidoc[] + +include::BusinessLogic.asciidoc[] + +include::UserFrontEnd.asciidoc[] + +include::AdminHTML5.asciidoc[] + +include::DashboardHTML5.asciidoc[] + +include::HybridUI.asciidoc[] + +include::JBossDeployment.asciidoc[] + +include::OpenShiftDeployment.asciidoc[] \ No newline at end of file