The content of the underlying directories is as follows:
+
+
+
demo - the sources of TicketMonster application - you can build and run it! Follow the instructions. Or you can see it at work here.
+
cordova - the sources of the TicketMonster Hybrid Mobile (Cordova) application. Follow the instructions to build and run it.
+
+
diff --git a/README.md b/README.md
index 3d5e13308..0283f2f50 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,10 @@
# What is this?
-This is the Git repository for all things TicketMonster, a showcase application for [JBoss Developer Framework](http://jboss.org/jdf).
+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.
-* `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
+* `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/cordova/README.html b/cordova/README.html
new file mode 100644
index 000000000..26d2530b1
--- /dev/null
+++ b/cordova/README.html
@@ -0,0 +1,57 @@
+README
+
What is this?
+
+
This is the Hybrid Mobile project for TicketMonster.
+
+
Importing and running the project
+
+
Prerequisites
+
+
+
JBoss Developer Studio (JBDS) 8.0.0 GA
+
The JBoss Hybrid Mobile Tools + CordovaSim feature is installed in JBDS.
+
+
+
For running on an Android emulator:
+
+
+
The Android Developer Tools plug-in must be installed in JBDS.
+
An Android Virtual Device (AVD) having a minimum API level 10 must be available. The recommended API level is 19 (KitKat).
+
+
+
For running on an iOS simulator:
+
+
+
Mac OS X 10.7 (Lion) or higher.
+
XCode 6.0 or higher, with iOS 6 SDK or higher.
+
An iOS 5.x or higher simulator for the iPhone or iPad.
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.
+
+
+
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.
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.
+
+
+
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.
+
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.
+
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.
+
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
+
+
+
+
Building TicketMonster
+
+
TicketMonster can be built from Maven, by runnning the following Maven command:
+
mvncleanpackage
+
+
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.
+
mvncleanpackage-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.
+
mvncleanpackage-Parq-jbossas-managed
+
+
Building TicketMonster with Postgresql (for OpenShift)
+
+
If you intend to deploy into OpenShift, you can use the postgresql-openshift profile
+
mvncleanpackage-Ppostgresql-openshift
+
+
Building TicketMonster with MySQL (for OpenShift)
+
+
If you intend to deploy into OpenShift, you can use the mysql-openshift profile
+
mvncleanpackage-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
+
+
+
Open a command line and navigate to the root of the JBoss server directory.
+
The following shows the command line to start the server with the web profile:
Make sure you have started the JBoss Server as described above.
+
Type this command to build and deploy the archive into a running server instance.
+
mvncleanpackagejboss-as:deploy
+
+
(You can use the arq-jbossas-remote profile for running tests as well)
+
This will deploy target/ticket-monster.war to the running instance of the server.
+
Now you can see the application running at http://localhost:8080/ticket-monster
+
+
+
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.
+
+
+
+
Open a shell command prompt and change to a directory of your choice. Enter the following command to create a JBoss EAP 6 application:
+
rhcappcreate-aAPP_NAME-tjbosseap-6
+
+
+
+
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:
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:
+
cdAPP_NAME
+ gitrm-rsrcpom.xml
+
+
+
Copy the TicketMonster application sources into this new git repository:
Add the MySQL 5.5 cartridge to the ticketmonster application:
+
rhccartridgeaddmysql-5.5-aticketmonster
+
+
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:
Add the PostgreSQL 9.2 cartridge to the ticketmonster application:
+
rhccartridgeaddpostgresql-9.2-aticketmonster
+
+
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:
You can now deploy the changes to your OpenShift application using git as follows:
+
gitadd-A
+gitcommit-m"TicketMonster on OpenShift"
+gitpush
+
+
+
+
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.
+
diff --git a/demo/pom.xml b/demo/pom.xml
index e66e3d22f..2aab5b2c0 100644
--- a/demo/pom.xml
+++ b/demo/pom.xml
@@ -3,7 +3,7 @@
4.0.0org.jboss.examplesticket-monster
- 2.7.0-SNAPSHOT
+ 2.7.0.ER2warticket-monsterA starter HTML5 + REST webapp project for use on JBoss EAP.
diff --git a/dist/README.dist.md b/dist/README.dist.md
deleted file mode 100644
index 0283f2f50..000000000
--- a/dist/README.dist.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# 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
deleted file mode 100755
index fc5404f22..000000000
--- a/dist/github-flavored-markdown.rb
+++ /dev/null
@@ -1,153 +0,0 @@
-#!/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 = "
"
- 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
deleted file mode 100755
index e0db5d70f..000000000
--- a/dist/release-utils.sh
+++ /dev/null
@@ -1,155 +0,0 @@
-#!/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
deleted file mode 100755
index 822a5dbad..000000000
--- a/dist/release.sh
+++ /dev/null
@@ -1,151 +0,0 @@
-#!/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
deleted file mode 100644
index c777e3de8..000000000
--- a/tutorial/AdminHTML5.asciidoc
+++ /dev/null
@@ -1,534 +0,0 @@
-= 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]
-------------------------------------------------------------------------------------------
- ...
-
- ...
-------------------------------------------------------------------------------------------
-
-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]
-------------------------------------------------------------------------------------------
-
-
-------------------------------------------------------------------------------------------
-
-
-=== 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
deleted file mode 100644
index 2071dc03b..000000000
--- a/tutorial/BusinessLogic.asciidoc
+++ /dev/null
@@ -1,1686 +0,0 @@
-[[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:
- *
- *
- * 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
- *
- * 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
- *
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
deleted file mode 100644
index fdd744bfd..000000000
--- a/tutorial/CONTRIBUTING.md
+++ /dev/null
@@ -1,10 +0,0 @@
-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
--------------------------------------------------------------------------------------------------------
-
-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]
--------------------------------------------------------------------------------------------------------
-
--------------------------------------------------------------------------------------------------------
-
-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]
--------------------------------------------------------------------------------------------------------
-
--------------------------------------------------------------------------------------------------------
-
-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) { %>
-
-<% } %>
--------------------------------------------------------------------------------------------------------
-
-Before submitting the request to the server, the order is confirmed:
-
-.src/main/webapp/resources/templates/mobile/confirm-booking.html
-[source,html]
--------------------------------------------------------------------------------------------------------
-
--------------------------------------------------------------------------------------------------------
-
-Finally, we create the page that displays the booking confirmation:
-
-.src/main/webapp/resources/templates/mobile/booking-details.html
-[source,html]
--------------------------------------------------------------------------------------------------------
-