Questions & Answers
datasource-config.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

  <bean id="dataSource"
        class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close"
        p:driverClassName="${jdbc.driverClassName}"
        p:url="${jdbc.url}"
        p:username="${jdbc.username}"
        p:password="${jdbc.password}"
        p:maxActive="${dbcp.maxActive}"
        p:maxIdle="${dbcp.maxIdle}"
        p:maxWait="${dbcp.maxWait}"/>
  
</beans>
jdbc-live.properties
jdbc.driverClassName=oracle.jdbc.OracleDriver
jdbc.url=jdbc:oracle:thin:@my_server:1521:my_db
jdbc.username=my_user
jdbc.password=my_password

dbcp.maxActive=100
dbcp.maxIdle=30
dbcp.maxWait=20000

hibernate.generate_statistics=true
hibernate.show_sql=false
hibernate.format_sql=false
hibernate.dialect=org.hibernate.dialect.Oracle10gDialect
jdbc-test.properties
jdbc.driverClassName=oracle.jdbc.OracleDriver
jdbc.url=jdbc:oracle:thin:@my_server:1521:my_db
jdbc.username=my_user
jdbc.password=my_password

dbcp.maxActive=100
dbcp.maxIdle=30
dbcp.maxWait=20000

hibernate.generate_statistics=true
hibernate.show_sql=false
hibernate.format_sql=false
hibernate.dialect=org.hibernate.dialect.Oracle10gDialect
jdbc.properties
jdbc.driverClassName=oracle.jdbc.OracleDriver
jdbc.url=jdbc:oracle:thin:@my_server:1521:my_db
jdbc.username=my_user
jdbc.password=my_password

dbcp.maxActive=100
dbcp.maxIdle=30
dbcp.maxWait=20000

hibernate.generate_statistics=true
hibernate.show_sql=true
hibernate.format_sql=false
hibernate.dialect=org.hibernate.dialect.Oracle10gDialect
jpa-config.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

  <context:component-scan base-package="com.lishman.qa.data.jpa"/>

  <!-- Entity Manager -->
  <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
        p:persistenceUnitName="QA"
        p:dataSource-ref="dataSource">
    <property name="jpaVendorAdapter">
      <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
            p:databasePlatform="${hibernate.dialect}"
            p:showSql="${hibernate.show_sql}"/>
    </property>
  </bean>

  <!--  Transaction Manager -->
  <tx:annotation-driven transaction-manager="transactionManager"/>

  <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
  </bean>
  
  <!-- JpaTemplate must be used if declarative transactions are required (is this true?) -->
  <bean id="jpaTemplate" class="org.springframework.orm.jpa.JpaTemplate">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
  </bean>
  
</beans>
messages.properties
application.name=Questions & Answers

theme.plain=plain
theme.silver=silver
theme.blue=blue

navigation.back=back
navigation.cancel=cancel
navigation.menu.main=menu

button.create=Create
button.update=Update
button.delete=Delete

test.take=Take a Test
test.nextQuestion=next question

question.label=Question
question.plural=Questions
question.details=Question Details
question.maintain=Question Maintenance
question.revealAnswer=Reveal Answer

answer.label=Answer

tag.label=Tag
tag.plural=Tags
tag.details=Tag Details
tag.maintain=Tag Maintenance
tag.field.name=Name
tag.field.description=Description

validation.exists=already exists
validation.hasQuestions=tag is being used by {0} question(s)

typeMismatch.java.util.Date=must be a valid date
typeMismatch.java.lang.Integer=must be a valid number
typeMismatch.java.lang.Long=must be a valid number

NotEmpty=must not be empty
NotBlank=must not be empty
messages_de.properties
application.name=Fragen und Antworten

theme.plain=plain
theme.silver=silber
theme.blue=blau

navigation.back=startseite
navigation.cancel=stornieren
navigation.menu.main=menü

button.create=Schaffen
button.update=Unter
button.delete=Löschen

test.take=Werfen Sie einen Test
test.nextQuestion=nächste frage

question.label=Frage
question.plural=Fragen
question.details=Frage Details
question.maintain=Frage Wartung
question.revealAnswer=Reveal Antwort

answer.label=Antwort

tag.label=Tag
tag.plural=Tags
tag.details=Tag Details
tag.maintain=Tag Wartung
tag.field.name=Name
tag.field.description=Beschreibung

validation.exists=bereits vorhanden
validation.hasQuestions=begriffe in gebrauch

typeMismatch.java.util.Date=ungültiges Datum
typeMismatch.java.lang.Integer=ungültige Zahl
typeMismatch.java.lang.Long=ungültige Zahl

NotEmpty=darf nicht leer sein
NotBlank=darf nicht leer sein
messages_fr.properties
application.name=Questions et réponses

theme.plain=plaine
theme.silver=d'argent
theme.blue=bleu

navigation.back=retournez
navigation.cancel=annuler
navigation.menu.main=menu

button.create=Créer
button.update=Changement
button.delete=Supprimer

test.take=Passer un Test
test.nextQuestion=question suivante

question.label=Question
question.plural=Questions
question.details=Détails Question
question.maintain=Maintenance Question
question.revealAnswer=Révèlent Réponse

answer.label=Réponse

tag.label=Tag
tag.plural=Tags
tag.details=Détails de Tag
tag.maintain=Tag Maintenance
tag.field.name=Nom
tag.field.description=Description

validation.exists=Existe déjà
validation.hasQuestions=Tag en cours d'utilisation

typeMismatch.java.util.Date=date non valide
typeMismatch.java.lang.Integer=nombre invalide
typeMismatch.java.lang.Long=nombre invalide

NotEmpty=Ne doit pas être vide
NotBlank=Ne doit pas être vide
plain.properties
styleSheet=/qa1/css/plain.css
silver.properties
styleSheet=/qa1/css/silver.css
domain-config.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

  <context:component-scan base-package="com.lishman.qa.domain"/>
  
  <!--  Note: <mvc:annotation-driven/> includes the validator bean by default 
        
        This means that 2 matching validator beans could be found if both are included.
        DomainValidator uses @Qualifier("validator") to avoid this problem..
  
         No unique bean of type [org.springframework.validation.beanvalidation.SpringValidatorAdapter] is defined: 
         expected single matching bean but found 2: [org.springframework.validation.beanvalidation.LocalValidatorFactoryBean#0, validator]
  -->
  <bean id="validator"
        class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

</beans>
log4j.properties
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

### set log levels - for more verbose logging change 'info' to 'debug' ###

#------------------------------------------------------------------------------
# Root

log4j.rootLogger=INFO, stdout


#------------------------------------------------------------------------------
# Application

com.lishman=INFO

#------------------------------------------------------------------------------
# Spring

log4j.logger.org.springframework=INFO

#------------------------------------------------------------------------------
# Hibernate

log4j.logger.org.hibernate=INFO

### log HQL query parser activity
#log4j.logger.org.hibernate.hql.ast.AST=debug

### log just the SQL
log4j.logger.org.hibernate.SQL=INFO

### log JDBC bind parameters ###
l#og4j.logger.org.hibernate.type=DEBUG

### log schema export/update ###
#log4j.logger.org.hibernate.tool.hbm2ddl=info

### log HQL parse trees
#log4j.logger.org.hibernate.hql=INFO

### log cache activity ###
#log4j.logger.org.hibernate.cache=info

### log transaction activity
#log4j.logger.org.hibernate.transaction=debug

### log JDBC resource acquisition
#log4j.logger.org.hibernate.jdbc=debug

### enable the following line if you want to track down connection ###
### leakages when using DriverManagerConnectionProvider ###
#log4j.logger.org.hibernate.connection.DriverManagerConnectionProvider=trace
mvc-config.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
           http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

    <mvc:annotation-driven/>
    
    <context:component-scan base-package="com.lishman.qa.web"/>
  
    <!-- 
    <mvc:annotation-driven/> 
  
      replaces the need for..
  
    <bean id="validator"
          class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
  
    -->

  <mvc:view-controller path="/" view-name="main-menu"/>
  <mvc:view-controller path="/main-menu.html" view-name="main-menu"/>
  
  <!-- Views -->
  <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver"
        p:prefix="/WEB-INF/jsp/"
        p:suffix=".jsp"/>
    
  <!-- Locale -->        
  <bean id="messageSource"
        class="org.springframework.context.support.ReloadableResourceBundleMessageSource"
        p:basename="classpath:messages/messages"/>

  <mvc:interceptors>
     <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
  </mvc:interceptors>

  <bean id="localeResolver"
        class="org.springframework.web.servlet.i18n.CookieLocaleResolver"
        p:cookieName="MyLocale"/>  
        
  <!-- Themes -->  
  <bean id="themeSource"
        class="org.springframework.ui.context.support.ResourceBundleThemeSource"
        p:basenamePrefix="/themes/"/>

  <mvc:interceptors>
    <bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"
          p:paramName="theme-name"/>
  </mvc:interceptors>
      
  <bean id="themeResolver"
        class="org.springframework.web.servlet.theme.CookieThemeResolver"
        p:defaultThemeName="plain"/>
        

</beans>
service-config-with-stubs.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">

    <context:component-scan base-package="com.lishman.qa.service">
      <context:exclude-filter type="regex" expression="com.lishman.qa.service.*Impl"/>
    </context:component-scan>

</beans>
service-config.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

    <context:component-scan base-package="com.lishman.qa.service">
      <context:exclude-filter type="regex" expression="com.lishman.qa.service.*Stub"/>
    </context:component-scan>

</beans>
Testing.txt
Testing the stack


    Browser
     |
    Web
     |
    Service
     |
    Data
     |
    Database
  

Layers are tested by mocking (eg using Mockito) the layer 
below, except for the data layer which is tested against the 
real database and is considered an integration test.

We are talking real TDD tests here to ensure that the tests 
drive the design. See http://blog.stevensanderson.com/2009/08/24/writing-great-unit-tests-best-and-worst-practises/
for a great blog called 'Writing Great Unit Tests: Best and Worst Practices'.

The browser / web interaction is also tested using TDD.
Tests are performed using selenium 2 using real browser
with a the application running in web server.

I believe that this is the only way to really test the web
layer and minimises the chance of subtle difference 
introduced by 'pretend' implementations such as HtmlUnit (?)

Browser testing mocks the layer below in the same way as the 
other layers do. In this case the service layer is mocked using
a stub. A stub is a bespoke implementation of the service which can 
save and return data, as opposed to a mock object using a mocking 
framework like Mockito. See http://martinfowler.com/articles/mocksArentStubs.html
by Martin Fowler for a convenient definition of the differences 
between mocks, stubs, fakes and dummies.

This stub is then accessed and controlled by the unit test using 
a separate controller which provides a REST interface. 

  

Validation.txt

schema.sql
-- drop tables
DROP TABLE question_tag;
DROP TABLE question;
DROP TABLE tag;

-- tag
DROP SEQUENCE tag_seq1;

CREATE TABLE tag (
  tag_id    NUMBER,
  tag_name  VARCHAR2(20)      NOT NULL,
  tag_desc  VARCHAR2(1000),
  CONSTRAINT tag_pk PRIMARY KEY (tag_id),
  CONSTRAINT tag_uk1 UNIQUE (tag_name),
  CONSTRAINT tag_cc1 CHECK (trim(tag_name) IS NOT NULL)
);

CREATE SEQUENCE tag_seq1;

CREATE OR REPLACE TRIGGER tag_trg1
  BEFORE INSERT ON tag FOR EACH ROW
BEGIN 
    IF :NEW.tag_id IS NULL THEN
      SELECT tag_seq1.NEXTVAL INTO :NEW.tag_id FROM DUAL;
    END IF;
END;
/ 

-- question
DROP SEQUENCE question_seq1;

CREATE TABLE question (
  ques_id     NUMBER,
  question    VARCHAR2(4000)  NOT NULL,
  answer      VARCHAR2(4000)  NOT NULL,
  CONSTRAINT question_pk PRIMARY KEY (ques_id),
  CONSTRAINT question_cc1 CHECK (trim(question) IS NOT NULL),
  CONSTRAINT question_cc2 CHECK (trim(answer) IS NOT NULL)
);

CREATE SEQUENCE question_seq1;

CREATE OR REPLACE TRIGGER question_trg1
  BEFORE INSERT ON question FOR EACH ROW
BEGIN 
    IF :NEW.ques_id IS NULL THEN
      SELECT question_seq1.NEXTVAL INTO :NEW.ques_id FROM DUAL;
    END IF;
END;
/

-- question_tag
CREATE TABLE question_tag (
  ques_id NUMBER,
  tag_id  NUMBER,
  CONSTRAINT question_tag_pk PRIMARY KEY (ques_id, tag_id),
  CONSTRAINT question_tag_fk1 FOREIGN KEY (ques_id) REFERENCES question,
  CONSTRAINT question_tag_fk2 FOREIGN KEY (tag_id) REFERENCES tag
);
unit-test-data.sql
DROP SEQUENCE tag_seq1; 
CREATE SEQUENCE tag_seq1 START WITH 100;
DROP SEQUENCE question_seq1; 
CREATE SEQUENCE question_seq1 START WITH 100;

TRUNCATE TABLE question_tag;
DELETE FROM tag;
DELETE FROM question;
COMMIT;

INSERT INTO tag (tag_id, tag_name, tag_desc) VALUES (1, 'First Tag',   'Description of First Tag');
INSERT INTO tag (tag_id, tag_name, tag_desc) VALUES (2, 'Second Tag',  'Description of Second Tag');
INSERT INTO tag (tag_id, tag_name, tag_desc) VALUES (3, 'Third Tag',   'Description of Third Tag');
INSERT INTO tag (tag_id, tag_name, tag_desc) VALUES (4, 'Fourth Tag',  'Description of Fourth Tag');

INSERT INTO question (ques_id, question, answer) VALUES (1, 'First question?',  'Answer to first question');
INSERT INTO question (ques_id, question, answer) VALUES (2, 'Second question?', 'Answer to second question');
INSERT INTO question (ques_id, question, answer) VALUES (3, 'Third question?',  'Answer to third question');
INSERT INTO question (ques_id, question, answer) VALUES (4, 'Fourth question?', 'Answer to fourth question');
INSERT INTO question (ques_id, question, answer) VALUES (5, 'Fifth question?',  'Answer to fifth question');

INSERT INTO question_tag (ques_id, tag_id) VALUES (1, 1);
INSERT INTO question_tag (ques_id, tag_id) VALUES (2, 1);
INSERT INTO question_tag (ques_id, tag_id) VALUES (2, 2);
INSERT INTO question_tag (ques_id, tag_id) VALUES (3, 1);
INSERT INTO question_tag (ques_id, tag_id) VALUES (3, 2);
INSERT INTO question_tag (ques_id, tag_id) VALUES (3, 3);

COMMIT;
JpaQuestionDao.java
package com.lishman.qa.data.jpa;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.jpa.JpaTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.Errors;

import com.lishman.qa.data.QuestionDao;
import com.lishman.qa.domain.DomainValidator;
import com.lishman.qa.domain.Question;

@Repository
@Transactional
public class JpaQuestionDao implements QuestionDao {

    @Autowired
    JpaTemplate jpaTemplate;
    
    @Autowired
    private DomainValidator questionValidator;

    @Transactional(readOnly = true)
    public Question getById(int questionId) {
        return (Question) jpaTemplate.find(Question.class, questionId);
    }

    @SuppressWarnings("unchecked")
    @Transactional(readOnly = true)
    public List<Question> getAll() {
        return jpaTemplate.findByNamedQuery("Question.findAll"); 
    }

    @Override
    public Question save(Question question, Errors errors) {
        questionValidator.validate(question, errors);
        if (errors.hasErrors()) {
            return null;
        }
        return jpaTemplate.merge(question);
    }

    @Override
    public void delete(Question question) {
        jpaTemplate.remove(getById(question.getId()));
    }
}
JpaTagDao.java
package com.lishman.qa.data.jpa;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.jpa.JpaTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.Errors;

import com.lishman.qa.data.TagDao;
import com.lishman.qa.domain.DomainValidator;
import com.lishman.qa.domain.Tag;

@Repository
@Transactional
public class JpaTagDao implements TagDao {

    @Autowired
    JpaTemplate jpaTemplate;
    
    @Autowired
    private DomainValidator tagValidator;
    
    @SuppressWarnings("unchecked")
    @Transactional(readOnly = true)
    public List<Tag> getAll() {
        return jpaTemplate.findByNamedQuery("Tag.findAll"); 
    }

    @Transactional(readOnly = true)
    public Tag getById(int tagId) {
        return (Tag) jpaTemplate.find(Tag.class, tagId);
    }

    @SuppressWarnings("unchecked")
    @Transactional(readOnly = true)
    public Tag getByName(String name) {
        Map<String, String> params = new HashMap<String, String>();
        params.put("tag_name", name);
        List<Tag> tags = jpaTemplate.findByNamedQueryAndNamedParams("Tag.findByName", params);
        return tags.isEmpty() ? null : tags.get(0);
    }

    public Tag save(Tag tag, Errors errors) {
        tagValidator.validate(tag, errors);
        validateNameNotInUse(tag, errors);
        if (errors.hasErrors()) {
            return null;
        }
        return jpaTemplate.merge(tag);
    }

    public void delete(Tag tag, Errors errors) {
        int questionCount = tag.getQuestions().size();
        if (questionCount > 0) {
            errors.reject("validation.hasQuestions", new Object[] {questionCount}, "has questions");
            return;
        }
        /*
         * jpaTemplate.remove(tag); results in InvalidDataAccessApiUsageException
         */
        jpaTemplate.remove(getById(tag.getId()));
    }
    
    private void validateNameNotInUse(Tag tag, Errors errors) {
        /* Some validation errors depend on the context. 
         * An existing row in the database is an error if 
         * duplicates are not allowed but a pre-requisite 
         * when deleting or updating. 
         */
        // TODO can this be improved?
        if (!errors.hasFieldErrors("name")) {
            Tag existingTag = this.getByName(tag.getName());
            if (existingTag != null
                 && (tag.isNew() || 
                     !tag.getId().equals(existingTag.getId()))) {
                errors.rejectValue("name", "validation.exists", "exists");
            }
        }
    }

}

QuestionDao.java
package com.lishman.qa.data;

import java.util.List;

import org.springframework.validation.Errors;

import com.lishman.qa.domain.Question;


public interface QuestionDao {

    public List<Question> getAll();

    public Question getById(int questionId);

    public Question save(Question question, Errors errors);

    public void delete(Question question);

}

TagDao.java
package com.lishman.qa.data;

import java.util.List;

import org.springframework.validation.Errors;

import com.lishman.qa.domain.Tag;


public interface TagDao {

    public List<Tag> getAll();

    public Tag getById(int tagId);

    public Tag getByName(String tagName);

    public Tag save(Tag tag, Errors errors);

    public void delete(Tag tag, Errors errors);

}

DomainValidator.java
package com.lishman.qa.domain;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;

@Component
public class DomainValidator {

    private SpringValidatorAdapter validatorAdapter; 
    
    /**
     * Qualifier is used in case more than one validator is registered in the app context.
     * For example, as a result of <mvc:annotation-driven/> in the MVC config and
     * the validator bean in the domain config.
     * @param validator the validator bean.
     */
    @Autowired
    @Qualifier("validator")
    public void setValidatorAdapter(SpringValidatorAdapter validator) {
        this.validatorAdapter = validator;
    }
    
    public void validate(Object object, Errors errors) {
        validatorAdapter.validate(object, errors);
    }
}
Question.java
package com.lishman.qa.domain;

import java.util.SortedSet;
import java.util.TreeSet;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

import org.codehaus.jackson.annotate.JsonIgnore;
import org.hibernate.annotations.Sort;
import org.hibernate.annotations.SortType;
import org.hibernate.validator.constraints.NotBlank;

@Entity
@Table(name = "QUESTION")
@NamedQueries ({
    @NamedQuery(name="Question.findAll",    query="select q from Question q order by q.id"),
    @NamedQuery(name="Question.findByName", query="select q from Question q where q.question = :question_name")
})
public class Question {

    @Id
    @GeneratedValue(generator = "QuestionSequence", 
                    strategy = GenerationType.SEQUENCE)
    @SequenceGenerator(name = "QuestionSequence", 
                       sequenceName = "QUESTION_SEQ1")
    @Column(name = "QUES_ID")
    private Integer id;

    @Column(name = "QUESTION")
    @NotBlank
    private String question;

    @Column(name = "ANSWER")
    @NotBlank
    private String answer;
    
    @ManyToMany(fetch=FetchType.EAGER)
    @JoinTable (name="QUESTION_TAG",
                joinColumns = {@JoinColumn(name="QUES_ID")},
                inverseJoinColumns={@JoinColumn(name="TAG_ID")}
    )
    @Sort(type=SortType.NATURAL)
    private SortedSet<Tag> tags = new TreeSet<Tag>();
    
    // private Set<Tag> tags = new HashSet<Tag>();
    
    public Question() {}
    
    public Question(String question, String answer) {
        setQuestion(question);
        setAnswer(answer);
    }

    @JsonIgnore
    public boolean isNew() {
        return id == null;
    }
    
    /*
     * This is required for BeanPropertyRowMapper in DatabaseTest.
     */
    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getId() {
        return id;
    }

    public void setQuestion(String question) {
        this.question = question;
    }

    public String getQuestion() {
        return question;
    }

    public void setAnswer(String answer) {
        this.answer = answer;
    }

    public String getAnswer() {
        return answer;
    }
    
    public void setTags(SortedSet<Tag> tags) {
        this.tags = tags;
    }
    
    public SortedSet<Tag> getTags() {
        return tags;
    }
//    
//    // TODO implement this & unit test
//    @Override
//    public boolean equals (Object o) {
//        return true;
//    }

}

Tag.java
package com.lishman.qa.domain;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

import org.codehaus.jackson.annotate.JsonIgnore;
import org.hibernate.validator.constraints.NotBlank;

@Entity
@Table(name = "TAG")
@NamedQueries ({
    @NamedQuery(name="Tag.findAll",     query="select t from Tag t order by t.name"),
    @NamedQuery(name="Tag.findByName",  query="select t from Tag t where t.name = :tag_name")
})
public class Tag implements Comparable<Tag> {

    @Id
    @GeneratedValue(generator = "TagSequence", 
                    strategy = GenerationType.SEQUENCE)
    @SequenceGenerator(name = "TagSequence", 
                       sequenceName = "TAG_SEQ1")
    @Column(name = "TAG_ID")
    private Integer id;

    @Column(name = "TAG_NAME")
    @NotBlank
    private String name;

    @Column(name = "TAG_DESC")
    private String description;
    
    // TODO make this LAZY
    // Currently get: org.hibernate.LazyInitializationException
    @ManyToMany(fetch=FetchType.EAGER, mappedBy="tags")
    private List<Question> questions = new ArrayList<Question>();

    public Tag() {
    }

    public Tag(String name, String description) {
        setName(name);
        setDescription(description);
    }

    @JsonIgnore
    public boolean isNew() {
        return id == null;
    }
    
    /*
     * This is required for BeanPropertyRowMapper in DatabaseTest.
     */
    // TODO remove this
    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getId() {
        return id;
    }
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public List<Question> getQuestions() {
        return questions;
    }
    
    public int compareTo(Tag otherTag) {
        return this.getName().toLowerCase().compareTo(otherTag.getName().toLowerCase());
  }
    
    // No setQuestion(..) to make it read only
    // Tags are added to questions, not the other way around
    
    // This is required to make the checkboxes element work on question-form.jsp
    @Override
    public String toString() {
        return getName();
    }
}

QaService.java
package com.lishman.qa.service;

import java.util.List;

import org.springframework.validation.Errors;

import com.lishman.qa.domain.Question;
import com.lishman.qa.domain.Tag;


public interface QaService {

    // Questions
    
    public List<Question> getQuestions();
    
    public List<Question> getShuffledQuestions();

    public Question getQuestionById(int questionId);

    public Question saveQuestion(Question question, Errors errors);

    public void deleteQuestion(Question question);

    // Tags
    
    public List<Tag> getTags();

    public Tag getTagById(int tagId);

    // TODO Can we remove this?
    public Tag getTagByName(String name);

    public Tag saveTag(Tag tag, Errors errors);

    public void deleteTag(Tag tag, Errors errors);
    
}

QaServiceImpl.java
package com.lishman.qa.service;

import java.util.Collections;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.validation.Errors;

import com.lishman.qa.data.QuestionDao;
import com.lishman.qa.data.TagDao;
import com.lishman.qa.domain.Question;
import com.lishman.qa.domain.Tag;

@Service
public class QaServiceImpl implements QaService {

    private QuestionDao questionDao;
    private TagDao tagDao;
    
    /*
     * Setters are used for autowiring to allow Mockito's @InjectMocks to be used
     * which currently only supports setter injection.
     */
    @Autowired
    public void setQuestionDao(QuestionDao questionDao) {
        this.questionDao = questionDao;
    }

    @Autowired
    public void setTagDao(TagDao tagDao) {
        this.tagDao = tagDao;
    }

    // Questions

    @Override
    public List<Question> getQuestions() {
        return questionDao.getAll();
    }

    @Override
    public List<Question> getShuffledQuestions() {
        List<Question> questions = getQuestions();
        Collections.shuffle(questions);
        return questions;
    }

    @Override
    public Question getQuestionById(int questionId) {
        return questionDao.getById(questionId);
    }

    @Override
    public Question saveQuestion(Question question, Errors errors) {
        return questionDao.save(question, errors);
    }

    @Override
    public void deleteQuestion(Question question) {
        questionDao.delete(question);
    }

    // Tags
    
    @Override
    public List<Tag> getTags() {
        return tagDao.getAll();
    }

    @Override
    public Tag getTagById(int tagId) {
        return tagDao.getById(tagId);
    }

    @Override
    public Tag getTagByName(String name) {
        return tagDao.getByName(name);
    }

    @Override
    public Tag saveTag(Tag tag, Errors errors) {       
        return tagDao.save(tag, errors); 
    }
    
    @Override
    public void deleteTag(Tag tag, Errors errors) {
        tagDao.delete(tag, errors); 
    }

}

QuestionController.java
package com.lishman.qa.web;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

import com.lishman.qa.domain.Question;
import com.lishman.qa.service.QaService;

@Controller
@SessionAttributes("questions")
public class QuestionController {

    private QaService qaService;
    
    @Autowired
    public void setQaService(QaService qaService) {
        this.qaService = qaService;
    }

    @RequestMapping("/question-list")
    @ModelAttribute("questions")
    public List<Question> getQuestions() {
        return qaService.getQuestions();
    }
    
    @RequestMapping("/question-details/{id}")
    public String askQuestion(@PathVariable("id") int questionId,
                              Model model) {
        Question question = qaService.getQuestionById(questionId);
        model.addAttribute(question);
        return "question-details";
    }
    
    @SuppressWarnings("unchecked")
    @RequestMapping("/ask-question")
    public String askQuestion(Model model, SessionStatus status) {
        
        if (!model.containsAttribute("questions")) {
            model.addAttribute("questions", qaService.getShuffledQuestions());
        }
        List<Question> questions = (List<Question>) model.asMap().get("questions");
        
        if (questions.isEmpty()) {
            model.asMap().remove("questions");
            status.setComplete();
            return "main-menu";
        }
        
        Question question = questions.get(0);
        model.addAttribute(question);
        questions.remove(0);
        
        return "ask-question";
    }

}

QuestionForm.java
package com.lishman.qa.web;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

import com.lishman.qa.domain.Question;
import com.lishman.qa.domain.Tag;
import com.lishman.qa.service.QaService;

@Controller
@RequestMapping("/question-form")
@SessionAttributes("question")
public class QuestionForm {

    @Autowired
    private QaService qaService;

    @InitBinder
    public void initBinder(WebDataBinder dataBinder) {
        dataBinder.setDisallowedFields("id"); 
        dataBinder.registerCustomEditor(String.class, new StringTrimmerEditor(false));
        // this is for binding the list of tag names to tag objects when the form is sumbitted
        dataBinder.registerCustomEditor(Tag.class, new TagPropertyEditor(qaService));
    }
    
    @ModelAttribute("tagList")
    public List<Tag> getTagList () {
        return (List<Tag>) qaService.getTags();
    }

    @RequestMapping(method = RequestMethod.GET)
    public Question setUpForm(
            @RequestParam(value = "id", required = false) Integer questionId) {
        return questionId == null ? new Question() : qaService.getQuestionById(questionId);
    }

    /*
     * Validation is performed manually to check for duplicates.
     * 
     * If bean validation only is required then we could use this..
     * 
     *      public String save(@Valid Question question, 
     * 
     * However, if this annotation is included as well then the error
     * messages are displayed twice.
     */
    @RequestMapping(params = "save", method = RequestMethod.POST)
    public String save(Question question, 
                       BindingResult result,
                       SessionStatus status) {

        qaService.saveQuestion(question, result);
        if (result.hasErrors()) {
            return "question-form";
        }
        status.setComplete();
        return "redirect:question-list";

    }
    
    /*
     * This will retrieve the original question object from the model (not the updated one).
     * If we include 'Question question' in the method signature then Spring
     * will perform type mismatch validation.
     * If the user enters invalid data then deletes the question then an error
     * will be seen on the web page.
     */
    @RequestMapping(params = "delete", method = RequestMethod.POST)
    public String delete(Model model, SessionStatus status) {
        Question question = (Question) model.asMap().get("question");
        qaService.deleteQuestion(question);
        status.setComplete();
        return "redirect:question-list";
    }
        
}

TagController.java
package com.lishman.qa.web;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import com.lishman.qa.domain.Tag;
import com.lishman.qa.service.QaService;

/**
 * We need to find a way to test the interaction between the browsers
 * and the controllers without having to run the entire stack.
 * 
 * We only need to test that values returned in the model are handled
 * by the web page and that values posted by the browser are received
 * correctly by the controller.
 * 
 * This abstract class contains all the mappings for the Tag Controller.
 * It calls abstract methods for the mapped methods which must be 
 * implemented by the subclasses.
 * 
 * The intention is to extend this class with a 'real' class (TagController)
 * and a mock class (TagControllerMock).
 * 
 * This allows us to use the same mappings for the real and the mock classes.
 * 
 * The mocks can then be used to test the browser / controller interaction.
 * 
 * See TagControllerMock for more details.
 * 
 * @author lishy
 *
 */

@Controller
public class TagController {
    
    private QaService qaService;
    
    @Autowired
    public void setQaService(QaService qaService) {
        this.qaService = qaService;
    }

    @RequestMapping("/tag-list")
    @ModelAttribute("tags")
    public final List<Tag> getTags() {
        return qaService.getTags();
    }

    @RequestMapping("/tag-details/{id}")
    public final String getTag(@PathVariable("id") int tagId,
                              Model model) {
        Tag tag = qaService.getTagById(tagId);
        model.addAttribute(tag);
        return "tag-details";
    }

}

TagForm.java
package com.lishman.qa.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

import com.lishman.qa.domain.Tag;
import com.lishman.qa.service.QaService;

@Controller
@RequestMapping("/tag-form")
@SessionAttributes("tag")
public class TagForm {

    @Autowired
    private QaService qaService; 

    @InitBinder
    public void initBinder(WebDataBinder dataBinder) {
        dataBinder.setDisallowedFields("id"); 
        dataBinder.registerCustomEditor(String.class, new StringTrimmerEditor(false));
    }

    @RequestMapping(method = RequestMethod.GET)
    public Tag setUpForm(@RequestParam(value = "id", required = false) Integer tagId) {
        return tagId == null ? new Tag() : qaService.getTagById(tagId);
    }

    @RequestMapping(params = "save", method = RequestMethod.POST)
    public String save(Tag tag, 
                       BindingResult result,
                       SessionStatus status) {

        qaService.saveTag(tag, result);
        if (result.hasErrors()) {
            return "tag-form";
        }
        
        status.setComplete();
        return "redirect:tag-list";
    }
    
    @RequestMapping(params = "delete", method = RequestMethod.POST)
    public String delete(Tag tag, BindingResult result, SessionStatus status) {
        // TODO Improve this method signature
        /* Need to include BindingResult to display errors using form:errors
         * If we include BindingResult, we need to put it next to a model attribute.
         * 
         * Didn't do it this way originally to avoid the issue where changes on the
         * screen are validated before the delete took place.
         * 
         * However, this doesn't seem to be a problem.
         */
        
        qaService.deleteTag(tag, result);
        if (result.hasErrors()) {
            return "tag-form";
        }
        
        status.setComplete();
        return "redirect:tag-list";
    }
        
}

TagPropertyEditor.java
package com.lishman.qa.web;

import java.beans.PropertyEditorSupport;

import com.lishman.qa.service.QaService;

public class TagPropertyEditor  extends PropertyEditorSupport{


    /**
     * A property editor which converts a tag to a string and vice versa.
     * The string represents the name of the tag.
     * 
     * This worked when..
     * 
     * This tutorial does it slightly differently
     *  http://www.theserverlabs.com/blog/2009/04/30/select-lists-with-validation-in-spring-mvc/
     *  
     *  Spring MVC allows you to specify an itemValue attribute for the <form:select> element. 
     *  DO NOT SPECIFY this attribute if you want to use Property Editors (recommended). 
     *  
     */
    
    private QaService qaService;

    public TagPropertyEditor(QaService qaService) {
        this.qaService = qaService;
    }

    /*
     * This converts the list of strings posted from the question-form.jsp
     * into real Tag objects. 
     */
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        setValue(qaService.getTagByName(text));
    }

}
persistence.xml
<?xml version="1.0" encoding="UTF-8"?>

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
             version="1.0">
             
  <persistence-unit name="QA" transaction-type="RESOURCE_LOCAL"/> 
  
</persistence>
ApplicationPage.java
package com.lishman.qa.browser.pageobjects;

import org.openqa.selenium.WebDriver;


/**
 * Represents a web page for the application.
 * 
 * Somewhere between a totally generic page (ie one that can
 * be used in any app) and a specific page object.
 * 
 * @author lishy
 *
 */
// TODO do we really need this?
public class ApplicationPage extends Page{
    
    public static final String BLACK             = "#000000";
    public static final String BLUE              = "#0000ff";
    public static final String DARK_SLATE_GRAY   = "#2f4f4f";
    public static final String DIM_GRAY          = "#696969";
    public static final String SEA_GREEN         = "#2e8b57";
    
    public ApplicationPage (WebDriver driver, String[] pageTitles) {
        super(driver, pageTitles);
    }

}

MainMenuPage.java
package com.lishman.qa.browser.pageobjects;

import static com.lishman.qa.browser.pageobjects.ApplicationPage.BLACK;
import static com.lishman.qa.browser.pageobjects.ApplicationPage.SEA_GREEN;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

public class MainMenuPage extends Page {
    
    /*
     * We cannot use @CacheLookup here because we need to 
     * select the colour from the page and not the cache.
     */

    // header
    
    @FindBy(css = "h1")
    private WebElement header;
    
    // menu items
    
    @FindBy(id = "take-a-test")
    private WebElement takeATest;
    
    @FindBy(id = "quest-maint")
    private WebElement questionMaintenance;
    
    @FindBy(id = "tag-maint")
    private WebElement tagMaintenance;
    
    // themes
    
    @FindBy(id = "theme-plain")
    private WebElement plainTheme;
    
    @FindBy(id = "theme-silver")
    private WebElement silverTheme;
    
    // locales
    @FindBy(id = "locale-en")
    private WebElement englishLocale;
    
    @FindBy(id = "locale-fr")
    private WebElement frenchLocale;
    
    @FindBy(id = "locale-de")
    private WebElement germanLocale;

    /**
     * Constructor which specifies the title of the page.
     * @param driver the WebDriver being used for the test.
     */
    public MainMenuPage (WebDriver driver) {
        super(driver, new String[] {"Questions & Answers", "Questions et réponses", "Fragen und Antworten"});
    }

    // menu items
    
    public TakeATestPage takeATest() {
        takeATest.click();
        return PageFactory.initElements(getDriver(), TakeATestPage.class);
    }
    
    public QuestionListPage questionMaintenance() {
        questionMaintenance.click();
        return PageFactory.initElements(getDriver(), QuestionListPage.class);
    }
    
    public TagListPage tagMainteanance() {
        tagMaintenance.click();
        return PageFactory.initElements(getDriver(), TagListPage.class);
    }

    // elements
    
    public String headerText() {
        return header.getText();
    }

    // themes
    
    /*
     * Select the theme and wait for the colour of
     * the heading to change. A request for a theme change
     * is sent back to the server so it may not happen
     * instantaneously.
     */
    public void selectPlainTheme() {
        plainTheme.click();
        // TODO not sure if we still need this with implicit waits
        waitForElementToBeColour(header, BLACK);
    }
    
    public void selectSilverTheme() {
        silverTheme.click();
        waitForElementToBeColour(header, SEA_GREEN);
    }
    
    // locale
    
    public void selectEnglish() {
        englishLocale.click();
        waitForElementToContainText(header, "Answers");
    }
    
    public void selectFrench() {
        frenchLocale.click();
        waitForElementToContainText(header, "réponses");
    }
    
    public void selectGerman() {
        germanLocale.click();
        waitForElementToContainText(header, "Antworten");
    }
    
    // colours
    
    public String headerColour() {
        return header.getCssValue("color");
    }
    
    public String takeATestColour() {
        return takeATest.getCssValue("color");
    }
    
    public String questionMaintenanceColour() {
        return questionMaintenance.getCssValue("color");
    }
    
    public String tagMaintenanceColour() {
        return tagMaintenance.getCssValue("color");
    }
    
    public String plainThemeColour() {
        return plainTheme.getCssValue("color");
    }
    
    public String silverThemeColour() {
        return silverTheme.getCssValue("color");
    }
    
    public String englishLocaleColour() {
        return englishLocale.getCssValue("color");
    }
    
    public String frenchLocaleColour() {
        return frenchLocale.getCssValue("color");
    }
    
    public String germanLocaleColour() {
        return germanLocale.getCssValue("color");
    }

}

Page.java
package com.lishman.qa.browser.pageobjects;

import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.StringUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Wait;

import com.google.common.base.Function;


/**
 * Represents a web page used for testing.
 * 
 * Provides generic functionality for accessing web elements.
 * 
 * @author lishy
 *
 */
public class Page {
 
    private WebDriver driver;
    
    /**
     * Constructor which takes the web driver and titles for a page.
     * The different titles represent the titles in the different languages.
     * 
     * An exception is thrown if the title is not correct (ie the 
     * correct page has not been displayed).
     * 
     * @param driver the web driver sed to run the tests.
     * @param pageTitles the title of the page.
     */
    public Page (WebDriver driver, String[] pageTitles) {
        this.driver = driver;        
        waitForPageToLoad(pageTitles);
    }
    
    /**
     * Get the web driver implementation.
     * @return the web driver implementation.
     */
    public WebDriver getDriver () {
        return driver;
    }
    
    /**
     * Get the title of this page. 
     * @return
     */
    public String getTitle() {
        return driver.getTitle();
    }
    
    /**
     * Click on a link using the text of the link to identify the element.
     * @param linkText the text of the link to be clicked on.
     */
    public void clickOnLinkText(String linkText) {
        driver.findElement(By.linkText(linkText)).click();
    }
    
    // popups
    
    /**
     * Get the text which is displayed on the current popup.
     */
    public String getPopupText() {
        return driver.switchTo().alert().getText();
    }
    
    /**
     * Cancel the popup.
     */
    public void cancelConfirmation() {
        driver.switchTo().alert().dismiss();
    }
    
    /**
     * Accept the popup.
     */
    public void acceptConfirmation() {
        driver.switchTo().alert().accept();
    }

    // waits
    
    /**
     * Wait for the specified number of seconds.
     * 
     * Note:
     * As a rule this should not be used!
     * Wait for a particular event (eg an element appears) instead.
     * However it can be useful during development.
     * 
     * @param seconds
     */
    public void wait(int seconds) {
        try {
            Thread.sleep(seconds * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * Wait for the current page to load.
     * Several page titles can be checked for, each one
     * representing a different language.
     * @param pageTitles the page titles to check for.
     */
    // TODO test for a specific value depending on the current locale
    private void waitForPageToLoad(final String[] pageTitles) {
        fluentWait().until(new Function<WebDriver, Boolean>() {
            public Boolean apply(WebDriver driver) {
                for (int i = 0; i < pageTitles.length; i++) {
                    if (pageTitles[i].equals(driver.getTitle())) {
                        return true;
                    }
                }
                return false;
            }
        });
    }
    
    /**
     * Wait for an element to change to a particular colour.
     * @param element the element to be checked.
     * @param colour the colour that the element should turn to.
     */
    // TODO change his to waitForElementToHaveCssValue if other css attributes are needed.
    public void waitForElementToBeColour(final WebElement element, final String colour) {
        fluentWait().until(new Function<WebDriver, Boolean>() {
            public Boolean apply(WebDriver driver) {
                return element.getCssValue("color").equals(colour);
            }
        });
    }
    
    /**
     * Wait for an element to contain the specified text.
     * @param element the element to be checked.
     * @param value the value that the element should contain.
     */
    public void waitForElementToContainText(final WebElement element, final String value) {
        fluentWait().until(new Function<WebDriver, Boolean>() {
            public Boolean apply(WebDriver driver) {
                return StringUtils.contains(element.getText(), value);
            }
        });
    }
    
    /**
     * Wait for an event to occur.
     * Check every tenth of a second for 30 seconds.
     * Also, ignore any element not found exceptions.
     */
    @SuppressWarnings("unchecked")
    private Wait<WebDriver> fluentWait() {
        return new FluentWait<WebDriver>(driver)
                        .withTimeout(30, TimeUnit.SECONDS)
                        .pollingEvery(100, TimeUnit.MILLISECONDS)
                        .ignoring(NoSuchElementException.class);

    }

}

QuestionDetailsPage.java
package com.lishman.qa.browser.pageobjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

/**
 * Unit tests for the question details page.
 * 
 * @author lishy
 */

public class QuestionDetailsPage extends Page {
    
    // header
    
    @FindBy(css = "h1")
    private WebElement header;
    
    // question details
    
    @FindBy(id = "question")
    private WebElement question;

    @FindBy(id = "answer")
    private WebElement answer;
    
    // tags
    
    @FindBy(id = "tags")
    private WebElement tags;
    
    // back link
    
    @FindBy(id = "go-back")
    private WebElement backLink;
 
    // standard constructor
    
    public QuestionDetailsPage(WebDriver driver) {
        super(driver, new String[] {"Question Details"});
    }
    
    // header
    
    public String headerText() {
        return header.getText();
    }
    
    // question details
    
    public String getQuestion() {
        return question.getText();
    }
    
    // tags
    
    public String getTags() {
        return tags.getText();
    }
    
    public String getAnswer() {
        return answer.getText();
    }
    
    // back link
    
    public QuestionListPage clickBackLink() {
        backLink.click();
        return PageFactory.initElements(getDriver(), QuestionListPage.class);
    }
    
}

QuestionListPage.java
package com.lishman.qa.browser.pageobjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

/**
 * Page object which represents the question list page.
 * 
 * @author lishy
 *
 */

public class QuestionListPage extends Page {
    
    // header
    
    @FindBy(css = "h1")
    private WebElement header;
    
    // create button
    
    @FindBy(id = "create")
    private WebElement createButton;
    
    // table - first row
    
    @FindBy(css = "table#question tbody tr:nth-child(2) td:nth-child(1) a")
    private WebElement editFirstQuestion;
    
    @FindBy(css = "table#question tbody tr:nth-child(2) td:nth-child(2) a")
    private WebElement firstQuestion;

    @FindBy(css = "table#question tbody tr:nth-child(2) td:nth-child(3)")
    private WebElement firstAnswer;
    
    // table - second row
    
    @FindBy(css = "table#question tbody tr:nth-child(3) td:nth-child(1) a")
    private WebElement editSecondQuestion;
    
    @FindBy(css = "table#question tbody tr:nth-child(3) td:nth-child(2) a")
    private WebElement secondQuestion;
    
    @FindBy(css = "table#question tbody tr:nth-child(3) td:nth-child(3)")
    private WebElement secondAnswer;
    
    // back link
    
    @FindBy(id = "go-back")
    private WebElement backLink;

    // standard constructor

    public QuestionListPage(WebDriver driver) {
        super(driver, new String[] {"Questions"});
    }
    
    
    // header
    
    public String headerText() {
        return header.getText();
    }
    
    // create button
    
    public QuestionMaintenancePage clickCreateButton() {
        createButton.click();
        return PageFactory.initElements(getDriver(), QuestionMaintenancePage.class);
    }
    
    // table - first row
    
    public String getFirstQuestion() {
        return firstQuestion.getText();
    }
    
    public String getFirstAnswer() {
        return firstAnswer.getText();
    }
    
    public QuestionDetailsPage selectFirstQuestion() {
        firstQuestion.click();
        return PageFactory.initElements(getDriver(), QuestionDetailsPage.class);
    }
    
    public QuestionMaintenancePage editFirstQuestion() {
        editFirstQuestion.click();
        return PageFactory.initElements(getDriver(), QuestionMaintenancePage.class);
    }
    
    // table - second row
    
    public String getSecondQuestion() {
        return secondQuestion.getText();
    }
    
    public String getSecondAnswer() {
        return secondAnswer.getText();
    }
    
    public QuestionDetailsPage selectSecondQuestion() {
        secondQuestion.click();
        return PageFactory.initElements(getDriver(), QuestionDetailsPage.class);
    }
    
    public QuestionMaintenancePage editSecondQuestion() {
        editSecondQuestion.click();
        return PageFactory.initElements(getDriver(), QuestionMaintenancePage.class);
    }
    
    // back link
    
    public MainMenuPage clickBackLink() {
        backLink.click();
        return PageFactory.initElements(getDriver(), MainMenuPage.class);
    }
    

}

QuestionMaintenancePage.java
package com.lishman.qa.browser.pageobjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

/**
 * Unit tests for the question maintenance page.
 * 
 * @author lishy
 */

public class QuestionMaintenancePage extends Page {
    
    // header
    
    @FindBy(css = "h1")
    private WebElement header;
    
    // buttons
    
    @FindBy(id = "create")
    private WebElement createButton;
    
    @FindBy(id = "save")
    private WebElement saveButton;
    
    @FindBy(id = "delete")
    private WebElement deleteButton;
    
    // fields
    
    @FindBy(id = "question")
    private WebElement questionField;
    
    @FindBy(id = "question.errors")
    private WebElement questionFieldErrors;
    
    @FindBy(id = "answer")
    private WebElement answerField;
    
    @FindBy(id = "answer.errors")
    private WebElement answerFieldErrors;
    
    @FindBy(id = "tags1")
    private WebElement firstTagCheckBox;
    
    @FindBy(id = "tags2")
    private WebElement secondTagCheckBox;

    // cancel
    
    @FindBy(id = "cancel")
    private WebElement cancel;
 
    // standard constructor
    
    public QuestionMaintenancePage(WebDriver driver) {
        super(driver, new String[] {"Question Maintenance"});
    }
    
    // header
    
    public String headerText() {
        return header.getText();
    }
    
    // question
    
    public String getQuestion() {
        return questionField.getText();
    }
   
    public void enterQuestion(String question) {
        questionField.clear();
        questionField.sendKeys(question);
    }
    
    public String getQuestionErrors () {
        return questionFieldErrors.getText();
    }
    
    // answer
    
    public String getAnswer() {
        return answerField.getText();
    }
    
    public void enterAnswer(String answer) {
        answerField.clear();
        answerField.sendKeys(answer);
    }
    
    public String getAnswerErrors () {
        return answerFieldErrors.getText();
    }
    
    // tags
    
    public void clickFirstTag() {
        firstTagCheckBox.click();
    }
    
    public void clickSecondTag() {
        secondTagCheckBox.click();
    }
    
    // create button
    
    public boolean isCreateButtonDisplayed() {
        return createButton.isDisplayed();
    }
    
    public QuestionListPage clickCreateButton() {
        createButton.click();
        return PageFactory.initElements(getDriver(), QuestionListPage.class);
    }
    
    public void clickCreateButtonWithError() {
        createButton.click();
    }
    
    // save button
    
    public boolean isSaveButtonDisplayed() {
        return saveButton.isDisplayed();
    }
    
    public QuestionListPage clickSaveButton() {
        saveButton.click();
        return PageFactory.initElements(getDriver(), QuestionListPage.class);
    }
    
    public void clickSaveButtonWithError() {
        saveButton.click();
    }
    
    // delete button
    
    public boolean isDeleteButtonDisplayed() {
        return deleteButton.isDisplayed();
    }
    
    public void clickDeleteButton() {
        deleteButton.click();
    }
    
    public void cancelDelete() {
        super.cancelConfirmation();
    }
    
    public QuestionListPage confirmDelete() {
        super.acceptConfirmation();
        return PageFactory.initElements(getDriver(), QuestionListPage.class);
    }
    
    // cancel
    
    public QuestionListPage clickCancel() {
        cancel.click();
        return PageFactory.initElements(getDriver(), QuestionListPage.class);
    }
}

TagDetailsPage.java
package com.lishman.qa.browser.pageobjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

/**
 * Unit tests for the tag details page.
 * 
 * @author lishy
 */

public class TagDetailsPage extends Page {
    
    // header
    
    @FindBy(css = "h1")
    private WebElement header;
    
    // tag details
    
    @FindBy(css = "table#tag tbody tr:nth-child(1) td:nth-child(2)")
    private WebElement tagName;

    @FindBy(css = "table#tag tbody tr:nth-child(2) td:nth-child(2)")
    private WebElement tagDescription;
    
    // back link
    
    @FindBy(id = "go-back")
    private WebElement backLink;
 
    // standard constructor
    
    public TagDetailsPage(WebDriver driver) {
        super(driver, new String[] {"Tag Details"});
    }
    
    // header
    
    public String headerText() {
        return header.getText();
    }
    
    // tag details
    
    public String getTagName() {
        return tagName.getText();
    }
    
    public String getTagDescription() {
        return tagDescription.getText();
    }
    
    // back link
    
    public TagListPage clickBackLink() {
        backLink.click();
        return PageFactory.initElements(getDriver(), TagListPage.class);
    }
    
}

TagListPage.java
package com.lishman.qa.browser.pageobjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

/**
 * Page object which represents the tag list page.
 * 
 * @author lishy
 *
 */

public class TagListPage extends Page {
    
    // header
    
    @FindBy(css = "h1")
    private WebElement header;
    
    // create button
    
    @FindBy(id = "create")
    private WebElement createButton;
    
    // table - first row
    
    @FindBy(css = "table#tag tbody tr:nth-child(2) td:nth-child(1) a")
    private WebElement editFirstTag;
    
    @FindBy(css = "table#tag tbody tr:nth-child(2) td:nth-child(2) a")
    private WebElement firstTagName;

    @FindBy(css = "table#tag tbody tr:nth-child(2) td:nth-child(3)")
    private WebElement firstTagDescription;
    
    // table - second row
    
    @FindBy(css = "table#tag tbody tr:nth-child(3) td:nth-child(1) a")
    private WebElement editSecondTag;
    
    @FindBy(css = "table#tag tbody tr:nth-child(3) td:nth-child(2) a")
    private WebElement secondTagName;
    
    @FindBy(css = "table#tag tbody tr:nth-child(3) td:nth-child(3)")
    private WebElement secondTagDescription;
    
    // back link
    
    @FindBy(id = "go-back")
    private WebElement backLink;

    // standard constructor
    
    public TagListPage(WebDriver driver) {
        super(driver, new String[] {"Tags"});
    }
    
    // header
    
    public String headerText() {
        return header.getText();
    }
    
    // create button
    
    public TagMaintenancePage clickCreateButton() {
        createButton.click();
        return PageFactory.initElements(getDriver(), TagMaintenancePage.class);
    }
    
    // table - first row
    
    public String getFirstTagName() {
        return firstTagName.getText();
    }
    
    public String getFirstTagDescription() {
        return firstTagDescription.getText();
    }
    
    public TagDetailsPage selectFirstTag() {
        firstTagName.click();
        return PageFactory.initElements(getDriver(), TagDetailsPage.class);
    }
    
    public TagMaintenancePage editFirstTag() {
        editFirstTag.click();
        return PageFactory.initElements(getDriver(), TagMaintenancePage.class);
    }
    
    // table - second row
    
    public String getSecondTagName() {
        return secondTagName.getText();
    }
    
    public String getSecondTagDescription() {
        return secondTagDescription.getText();
    }
    
    public TagDetailsPage selectSecondTag() {
        secondTagName.click();
        return PageFactory.initElements(getDriver(), TagDetailsPage.class);
    }
    
    public TagMaintenancePage editSecondTag() {
        editSecondTag.click();
        return PageFactory.initElements(getDriver(), TagMaintenancePage.class);
    }
    
    // back link
    
    public MainMenuPage clickBackLink() {
        backLink.click();
        return PageFactory.initElements(getDriver(), MainMenuPage.class);
    }
    
}

TagMaintenancePage.java
package com.lishman.qa.browser.pageobjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

/**
 * Unit tests for the tag maintenance page.
 * 
 * @author lishy
 */

public class TagMaintenancePage extends Page {
    
    // header
    
    @FindBy(css = "h1")
    private WebElement header;
    
    // buttons
    
    @FindBy(id = "create")
    private WebElement createButton;
    
    @FindBy(id = "save")
    private WebElement saveButton;
    
    @FindBy(id = "delete")
    private WebElement deleteButton;
    
    // fields
    
    @FindBy(id = "name")
    private WebElement nameField;
    
    @FindBy(id = "name.errors")
    private WebElement nameFieldErrors;
    
    @FindBy(id = "description")
    private WebElement descriptionField;

    // cancel
    
    @FindBy(id = "cancel")
    private WebElement cancel;
 
    // standard constructor
    
    public TagMaintenancePage(WebDriver driver) {
        super(driver, new String[] {"Tag Maintenance"});
    }
    
    // header
    
    public String headerText() {
        return header.getText();
    }
    
    // name
    
    public String getName() {
        return nameField.getAttribute("value");
    }
   
    public void enterName(String name) {
        nameField.clear();
        nameField.sendKeys(name);
    }
    
    public String getNameErrors () {
        return nameFieldErrors.getText();
    }
    
    // description
    
    public String getDescription() {
        return descriptionField.getAttribute("value");
    }
    
    public void enterDescription(String description) {
        descriptionField.clear();
        descriptionField.sendKeys(description);
    }
    
    // create button
    
    public boolean isCreateButtonDisplayed() {
        return createButton.isDisplayed();
    }
    
    public TagListPage clickCreateButton() {
        createButton.click();
        return PageFactory.initElements(getDriver(), TagListPage.class);
    }
    
    public void clickCreateButtonWithError() {
        createButton.click();
    }
    
    // save button
    
    public boolean isSaveButtonDisplayed() {
        return saveButton.isDisplayed();
    }
    
    public TagListPage clickSaveButton() {
        saveButton.click();
        return PageFactory.initElements(getDriver(), TagListPage.class);
    }
    
    public void clickSaveButtonWithError() {
        saveButton.click();
    }
    
    // delete button
    
    public boolean isDeleteButtonDisplayed() {
        return deleteButton.isDisplayed();
    }
    
    public void clickDeleteButton() {
        deleteButton.click();
    }
    
    public void cancelDelete() {
        super.cancelConfirmation();
    }
    
    public TagListPage confirmDelete() {
        super.acceptConfirmation();
        return PageFactory.initElements(getDriver(), TagListPage.class);
    }
    
    // cancel
    
    public TagListPage clickCancel() {
        cancel.click();
        return PageFactory.initElements(getDriver(), TagListPage.class);
    }
}

TakeATestPage.java
package com.lishman.qa.browser.pageobjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

/**
 * Unit tests for the question maintenance page.
 * 
 * @author lishy
 */

public class TakeATestPage extends Page {
    
    // tags

    @FindBy(css = "h2")
    private WebElement tags;
    
    // buttons
    
    @FindBy(id = "reveal")
    private WebElement revealButton;
    
    // fields
    
    @FindBy(id = "question")
    private WebElement questionField;
    
    @FindBy(id = "answer")
    private WebElement answerField;

    // links
    
    @FindBy(id = "menu")
    private WebElement menu;
    
    @FindBy(id = "next-quest")
    private WebElement nextQuestion;
 
    // standard constructor
    
    public TakeATestPage(WebDriver driver) {
        super(driver, new String[] {"Take a Test"});
    }
    
    // tags
    
    public String getTags() {
        return tags.getText();
    }
    
    // question
    
    public String getQuestion() {
        return questionField.getText();
    }

    // answer
    
    public boolean isAnswerDisplayed() {
        return answerField.isDisplayed();
    }
    
    public String getAnswer() {
        return answerField.getText();
    }
    
    public void enterAnswer(String answer) {
        answerField.clear();
        answerField.sendKeys(answer);
    }
    
    // buttons
    
    public void revealAnswer() {
        revealButton.click();
    }
    
    public MainMenuPage clickMenu() {
        menu.click();
        return PageFactory.initElements(getDriver(), MainMenuPage.class);
    }
    
    public void clickNextQuestion() {
        nextQuestion.click();
    }
    
    public MainMenuPage clickNextQuestionWithNoMoreQuestions() {
        nextQuestion.click();
        return PageFactory.initElements(getDriver(), MainMenuPage.class);
    }

}

browser-test.properties
app.host=http://localhost:8080
BrowserTest-context.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">

    <!-- identifies the properties referenced by the SpEL in BrowserTest.java -->
    <util:properties id="browserTestProperties"
                   location="classpath:com/lishman/qa/browser/browser-test.properties"/>

</beans>
BrowserTest.java
package com.lishman.qa.browser;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.client.RestTemplate;

import com.lishman.qa.TestExt;
import com.lishman.qa.domain.Question;
import com.lishman.qa.domain.Tag;

/**
 * Super class of all browser tests.
 * 
 * Handles the web driver for the tests.
 * 
 * @author lishy
 */

/*
 * Required to load the details from the context configuration 
 * (ie the name of the properties file).
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class BrowserTest extends TestExt {
    
    private static WebDriver driver;
    private static boolean manageDriverHere = true;
    private String testHost; // = "http://localhost:8080";

    /**
     * Instantiates the firefox web driver if it has not already been 
     * set externally (ie using the setDriver(..) method).
     * 
     * This allows the driver to be created once for each test class or once
     * for a group of classes (eg a test suite). This helps to minimize
     * the number of times the browser needs to start up and shut down.
     * 
     * implicitlyWait specifies the amount of time the driver should 
     * wait when searching for an element if it is not immediately present. 
     */
    @BeforeClass
    public static void classSetUp() {
        if (manageDriverHere) {
            driver = new FirefoxDriver();
        }
        driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
    }
    
    @AfterClass
    public static void classTearDown() {
        if (manageDriverHere) {
            driver.quit();
        }
    }
    
    public static WebDriver getDriver() {
        return driver;
    }
    
    public static void setDriver(WebDriver webDriver) {
        manageDriverHere = false;
        driver = webDriver;
    }
    
    // test host
    
    public String getTestHost() {
        return testHost;
    }
    
    /*
     * This is an example how to access a property using SpEL.
     * 
     * It requires an entry in a context to specify the properties file.
     * See BrowserTest-context.xml in this directory.
     * 
     * Also, because we are relying Spring to inject this value, we must
     * use the Spring ClassRunner. See above.
     * 
     * This seems a bit heavy handed just to be able to set an instance
     * variable (which will rarely change) but it does show how to use
     * @Value an SpEL. 
     */
    @Value("#{browserTestProperties['app.host']}")
    public void setTestHost (String testHost) {
        this.testHost = testHost;
    }
    
    // qa service stub
    
    public void resetQaServiceStub() {
        RestTemplate restTemplate = new RestTemplate();
        String url = getTestHost() + "/qa1/app/reset";
        restTemplate.postForObject(url, null, String.class);
    }
    
    public Tag getTagByNameFromStub(String tagName) {
        RestTemplate restTemplate = new RestTemplate();
        String url = getTestHost() + "/qa1/app/get-tag/" + tagName;
        return restTemplate.getForObject(url, Tag.class);    
    }
    
    @SuppressWarnings("unchecked")
    public List<LinkedHashMap<String, String>> getAllTagsFromStub() {
        RestTemplate restTemplate = new RestTemplate();
        String url = getTestHost() + "/qa1/app/get-tags";
        List<LinkedHashMap<String, String>> tags = restTemplate.getForObject(url, List.class); 
        return tags;
    }
    
    public Question getQuestionByTextFromStub(String questionName) {
        RestTemplate restTemplate = new RestTemplate();
        String url = getTestHost() + "/qa1/app/get-question/" + questionName;
        return restTemplate.getForObject(url, Question.class);    
    }
    
    @SuppressWarnings("unchecked")
    public List<LinkedHashMap<String, String>> getAllQuestionsFromStub() {
        RestTemplate restTemplate = new RestTemplate();
        String url = getTestHost() + "/qa1/app/get-questions";
        List<LinkedHashMap<String, String>> tags = restTemplate.getForObject(url, List.class); 
        return tags;
    }

}

MainMenuTest.java
package com.lishman.qa.browser;

import static com.lishman.qa.browser.pageobjects.ApplicationPage.BLACK;
import static com.lishman.qa.browser.pageobjects.ApplicationPage.BLUE;
import static com.lishman.qa.browser.pageobjects.ApplicationPage.DARK_SLATE_GRAY;
import static com.lishman.qa.browser.pageobjects.ApplicationPage.SEA_GREEN;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.support.PageFactory;

import com.lishman.qa.browser.pageobjects.MainMenuPage;

/**
 * Tests the main menu page.
 * 
 * Tests..
 * 
 *      themes
 *      locales
 *      menu items
 * 
 * @author lishy
 */

public class MainMenuTest extends BrowserTest {
    
    private MainMenuPage mainMenuPage;
    
    @Before
    public void setUp() {
        getDriver().get(getTestHost() + "/qa1/app/");
        mainMenuPage = PageFactory.initElements(getDriver(), MainMenuPage.class);
        mainMenuPage.selectEnglish();
     }
    
    // themes

    @Test
    public void selectingPlainThemeDisplaysPageInPlainMode() {
        mainMenuPage.selectPlainTheme();
        assertThat(mainMenuPage.headerColour(), equalTo(BLACK));
        testMainMenuColours(BLUE);
    }
        
    @Test
    public void selectingSilverThemeDisplaysPageInSilverMode() {
        mainMenuPage.selectSilverTheme();
        assertThat(mainMenuPage.headerColour(), equalTo(SEA_GREEN));
        testMainMenuColours(DARK_SLATE_GRAY);        
    }
    
    // locale
    
    @Test
    public void selectingEnglishLanguageDisplaysPageInEnglish() {
        mainMenuPage.selectEnglish();
        assertThat(mainMenuPage.headerText(), equalTo("Questions & Answers"));
    }
    
    @Test
    public void selectingFrenchLanguageDisplaysPageInFrench() {
        mainMenuPage.selectFrench();
        assertThat(mainMenuPage.headerText(), equalTo("Questions et réponses"));
    }
    
    @Test
    public void selectingGermanLanguageDisplaysPageInGerman() {
        mainMenuPage.selectGerman();
        assertThat(mainMenuPage.headerText(), equalTo("Fragen und Antworten"));
    }
    
    // navigation
    
    @Test
    public void clickingTakeATestOptionNavigatesToTestPage() {
        mainMenuPage.takeATest();
        assertThat(mainMenuPage.getTitle(), equalTo("Take a Test"));
    }
    
    @Test
    public void clickingQuestionMaintenanceOptionNavigatesToQuestionMaintenancePage() {
        mainMenuPage.questionMaintenance();
        assertThat(mainMenuPage.getTitle(), equalTo("Questions"));
    }
    
    @Test
    public void clickingTagMaintenanceOptionNavigatesToTagMaintenancePage() {
        mainMenuPage.tagMainteanance();
        assertThat(mainMenuPage.getTitle(), equalTo("Tags"));
    }

    // helper 
    
    private void testMainMenuColours(String colour) {
        assertThat(mainMenuPage.takeATestColour(),              equalTo(colour));
        assertThat(mainMenuPage.questionMaintenanceColour(),    equalTo(colour));
        assertThat(mainMenuPage.tagMaintenanceColour(),         equalTo(colour));
        assertThat(mainMenuPage.plainThemeColour(),             equalTo(colour));
        assertThat(mainMenuPage.silverThemeColour(),            equalTo(colour));
        assertThat(mainMenuPage.englishLocaleColour(),          equalTo(colour));
        assertThat(mainMenuPage.frenchLocaleColour(),           equalTo(colour));
        assertThat(mainMenuPage.germanLocaleColour(),           equalTo(colour));
    }

}

QuestionDetailsTest.java
package com.lishman.qa.browser;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.support.PageFactory;

import com.lishman.qa.browser.pageobjects.QuestionDetailsPage;
import com.lishman.qa.browser.pageobjects.QuestionListPage;


/**
 * Unit tests for the question details page.
 * 
 * @author lishy
 */

public class QuestionDetailsTest extends BrowserTest {
    
    private QuestionDetailsPage questionDetailsPage;
    
    @Before
    public void setUp() {
        getDriver().get(getTestHost() + "/qa1/app/question-details/1");
        questionDetailsPage = PageFactory.initElements(getDriver(), QuestionDetailsPage.class);
    }
    
    // header
    
    @Test
    public void correctHeaderIsDisplayedWhenPageIsLoaded() {
        assertThat(questionDetailsPage.headerText(), equalTo("Question Details"));
    }
    
    // question details

    @Test
    public void correctQuestionDetailsAreDisplayedForSpecifiedIdentifier() {
        assertThat(questionDetailsPage.getQuestion(), equalTo("question one"));
        assertThat(questionDetailsPage.getAnswer(),   equalTo("answer one"));
        assertThat(questionDetailsPage.getTags(),     equalTo("Tags: one"));
    
        getDriver().get(getTestHost() + "/qa1/app/question-details/2");
        questionDetailsPage = PageFactory.initElements(getDriver(), QuestionDetailsPage.class);
        assertThat(questionDetailsPage.getQuestion(),  equalTo("question two"));
        assertThat(questionDetailsPage.getAnswer(),    equalTo("answer two"));
        assertThat(questionDetailsPage.getTags(),      equalTo("Tags: one, two"));
    }
    
    // back link
    
    @Test
    public void questionListPageIsDisplayedWhenBackLinkIsClicked() {
        QuestionListPage questionListPage = questionDetailsPage.clickBackLink();
        assertThat(questionListPage.getTitle(),   equalTo("Questions"));
    }

    // themes
    // locale
    // tags
    

}

QuestionListTest.java
package com.lishman.qa.browser;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.support.PageFactory;

import com.lishman.qa.browser.pageobjects.MainMenuPage;
import com.lishman.qa.browser.pageobjects.QuestionDetailsPage;
import com.lishman.qa.browser.pageobjects.QuestionListPage;
import com.lishman.qa.browser.pageobjects.QuestionMaintenancePage;

/**
 * Unit tests for the question list page.
 * 
 * @author lishy
 */

public class QuestionListTest extends BrowserTest {
    
    private QuestionListPage questionListPage;
    
    @Before
    public void setUp() {
        getDriver().get(getTestHost() + "/qa1/app/question-list.html");
        questionListPage = PageFactory.initElements(getDriver(), QuestionListPage.class);
     }
    
    // header
    
    @Test
    public void correctHeaderIsDisplayedWhenPageIsLoaded() {
        assertThat(questionListPage.headerText(), equalTo("Questions"));
    }
    
    // table

    @Test
    public void allQuestionsAreDisplayedInTheTable() {
        assertThat(questionListPage.getFirstQuestion(),     equalTo("question one"));
        assertThat(questionListPage.getFirstAnswer(),       equalTo("answer one"));
        assertThat(questionListPage.getSecondQuestion(),    equalTo("question two"));
        assertThat(questionListPage.getSecondAnswer(),      equalTo("answer two"));
    }
    
    // display question details
    
    @Test
    public void firstQuestionDetailsAreDisplayedWhenFirstQuestionNameIsClicked() {
        QuestionDetailsPage questionDetailsPage = questionListPage.selectFirstQuestion();
        assertThat(questionDetailsPage.getTitle(),        equalTo("Question Details"));
        assertThat(questionDetailsPage.getQuestion(),     equalTo("question one"));
    }
    
    @Test
    public void secondQuestionDetailsAreDisplayedWhenSecondQuestionNameIsClicked() {
        QuestionDetailsPage questionDetailsPage = questionListPage.selectSecondQuestion();
        assertThat(questionDetailsPage.getTitle(),        equalTo("Question Details"));
        assertThat(questionDetailsPage.getQuestion(),     equalTo("question two"));
    }
    
    // edit question details
    
    @Test
    public void firstQuestionCanBeEditedWhenFirstEditButtonIsClicked() {
        QuestionMaintenancePage questionMaintenancePage = questionListPage.editFirstQuestion();
        assertThat(questionMaintenancePage.getTitle(),     equalTo("Question Maintenance"));
        assertThat(questionMaintenancePage.getQuestion(),  equalTo("question one"));
    }
 
    @Test
    public void secondQuestionCanBeEditedWhenSecondEditButtonIsClicked() {
        QuestionMaintenancePage questionMaintenancePage = questionListPage.editSecondQuestion();
        assertThat(questionMaintenancePage.getTitle(),     equalTo("Question Maintenance"));
        assertThat(questionMaintenancePage.getQuestion(),  equalTo("question two"));
    }
    
    // create
    
    @Test
    public void createQuestionPageIsDisplayedWhenCreateButtonIsClicked() {
        QuestionMaintenancePage questionMaintenancePage = questionListPage.clickCreateButton();
        assertThat(questionMaintenancePage.getTitle(),    equalTo("Question Maintenance"));
        assertThat(questionMaintenancePage.getQuestion(), equalTo(""));
    }
    
    // back link
    
    @Test
    public void mainMenuPageIsDisplayedWhenBackLinkIsClicked() {
        MainMenuPage mainMenuPage = questionListPage.clickBackLink();
        assertThat(mainMenuPage.getTitle(),   equalTo("Questions & Answers"));
    }

    // themes
    // locale
    

}

QuestionMaintenanceTest.java
package com.lishman.qa.browser;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.support.PageFactory;

import com.lishman.qa.browser.pageobjects.QuestionListPage;
import com.lishman.qa.browser.pageobjects.QuestionMaintenancePage;
import com.lishman.qa.domain.Question;
import com.lishman.qa.domain.Tag;

/**
 * Unit tests for the question maintenance page.
 *
 * @author lishy
 */

public class QuestionMaintenanceTest extends BrowserTest {
    
    QuestionMaintenancePage questionMaintenancePage;
    
    @Before
    public void setUp() {
        getDriver().get(getTestHost() + "/qa1/app/question-form.html");
        questionMaintenancePage = PageFactory.initElements(getDriver(), QuestionMaintenancePage.class);
    }
    
    @After
    public void tearDown() {
        super.resetQaServiceStub();
     }
    
    // header
    
    @Test
    public void correctHeaderIsDisplayedWhenPageIsLoaded() {
        assertThat(questionMaintenancePage.headerText(), equalTo("Question Maintenance"));
    }
    
    // create
    
    @Test
    public void questionIsCreatedWhenValidDetailsAreEnteredAndCreateButtonIsClicked() {

        questionMaintenancePage.enterQuestion("my-question");
        questionMaintenancePage.enterAnswer("my-desc");
        QuestionListPage questionListPage = questionMaintenancePage.clickCreateButton();

        assertThat(questionListPage.getTitle(), equalTo("Questions"));
        Question question = super.getQuestionByTextFromStub("my-question");
        assertThat(question.getQuestion(), equalTo("my-question"));
        assertThat(question.getAnswer(), equalTo("my-desc"));
    }
    
    @Test
    public void questionIsCreatedWithOneTag() {

        questionMaintenancePage.enterQuestion("my-question");
        questionMaintenancePage.enterAnswer("my-desc");
        // select first tag
        questionMaintenancePage.clickFirstTag();
        QuestionListPage questionListPage = questionMaintenancePage.clickCreateButton();

        assertThat(questionListPage.getTitle(), equalTo("Questions"));
        Question question = super.getQuestionByTextFromStub("my-question");
        assertThat(question.getQuestion(), equalTo("my-question"));
        assertThat(question.getAnswer(), equalTo("my-desc"));
        assertThat(question.getTags().iterator().next().getName(), equalTo("one"));
    }
    
    @Test
    public void questionIsCreatedWithTwoTags() {

        questionMaintenancePage.enterQuestion("my-question");
        questionMaintenancePage.enterAnswer("my-desc");
        // select both tags
        questionMaintenancePage.clickFirstTag();
        questionMaintenancePage.clickSecondTag();
        QuestionListPage questionListPage = questionMaintenancePage.clickCreateButton();

        assertThat(questionListPage.getTitle(), equalTo("Questions"));
        Question question = super.getQuestionByTextFromStub("my-question");
        assertThat(question.getQuestion(), equalTo("my-question"));
        assertThat(question.getAnswer(), equalTo("my-desc"));
        Iterator<Tag> iter = question.getTags().iterator();
        assertThat(iter.next().getName(), equalTo("one"));
        assertThat(iter.next().getName(), equalTo("two"));
    }
    
    // no question or answer
    
    @Test
    public void newQuestionWithNoQuestionAndAnswerIsRejected() {

        questionMaintenancePage.clickCreateButtonWithError();

        assertThat(questionMaintenancePage.getQuestionErrors(), equalTo("must not be empty"));
        assertThat(questionMaintenancePage.getAnswerErrors(),   equalTo("must not be empty"));

        List<LinkedHashMap<String, String>> questions = super.getAllQuestionsFromStub();
        assertThat(questions.size(), equalTo(2));
    }
    
    // update
    
    @Test
    public void questionIsUpdatedWhenDetailsAreChangedAndUpdateButtonIsClicked() {
        
        getDriver().get(getTestHost() + "/qa1/app/question-form.html?id=1");
        questionMaintenancePage = PageFactory.initElements(getDriver(), QuestionMaintenancePage.class);

        questionMaintenancePage.enterQuestion("my-question");
        questionMaintenancePage.enterAnswer("my-answer");
        // de-select first tag, select second tag
        questionMaintenancePage.clickFirstTag();
        questionMaintenancePage.clickSecondTag();
        QuestionListPage questionListPage = questionMaintenancePage.clickSaveButton();

        assertThat(questionListPage.getTitle(), equalTo("Questions"));
        Question question = getQuestionByTextFromStub("my-question");
        assertThat(question.getQuestion(), equalTo("my-question"));
        assertThat(question.getAnswer(), equalTo("my-answer"));
        assertThat(question.getTags().iterator().next().getName(), equalTo("two"));
    }
    
    // no question or answer
    
    @Test
    public void existingQuestionWithNoQuestionOrAnswerIsRejected() {
        
        getDriver().get(getTestHost() + "/qa1/app/question-form.html?id=1");
        questionMaintenancePage = PageFactory.initElements(getDriver(), QuestionMaintenancePage.class);

        questionMaintenancePage.enterQuestion("");
        questionMaintenancePage.enterAnswer("");

        questionMaintenancePage.clickSaveButtonWithError();

        assertThat(questionMaintenancePage.getQuestionErrors(), equalTo("must not be empty"));
        assertThat(questionMaintenancePage.getAnswerErrors(),   equalTo("must not be empty"));

        List<LinkedHashMap<String, String>> questions = super.getAllQuestionsFromStub();
        assertThat(questions.size(), equalTo(2));
    }
    
    // delete
    
    @Test
    public void questionIsDeletedWhenDeleteButtonIsClicked() {
        
        getDriver().get(getTestHost() + "/qa1/app/question-form.html?id=1");
        questionMaintenancePage = PageFactory.initElements(getDriver(), QuestionMaintenancePage.class);

        questionMaintenancePage.clickDeleteButton();
        assertThat(questionMaintenancePage.getPopupText(), equalTo("Delete Question?"));
        QuestionListPage questionListPage = questionMaintenancePage.confirmDelete();
        
        assertThat(questionListPage.getTitle(), equalTo("Questions"));
        List<LinkedHashMap<String, String>> questions = super.getAllQuestionsFromStub();
        assertThat(questions.size(), equalTo(1));
        Question question = getQuestionByTextFromStub("question two");
        assertThat(question.getQuestion(), equalTo("question two"));
    }
    
    // cancel
    
    @Test
    public void cancellingQuestionMaintenanceReturnsToListPage() {
        QuestionListPage questionListPage = questionMaintenancePage.clickCancel();
        
        assertThat(questionListPage.getTitle(), equalTo("Questions"));
        
        // Check that no data has been updated
        List<LinkedHashMap<String, String>> questions = super.getAllQuestionsFromStub();
        assertThat(questions.size(), equalTo(2));
        assertThat(questions.get(0).get("question"), equalTo("question one"));
        assertThat(questions.get(1).get("question"), equalTo("question two"));
        
        /*
         * Note that RestTemplate does not convert that contents of the 
         * collection to Questions. Instead it uses a generic map object.
         * 
         * I could try wrapping the collection of questions in another class.
         * 
         * This is a known issue:
         *  https://jira.springsource.org/browse/SPR-7002
         *  https://jira.springsource.org/browse/SPR-7023
         */
        
    }

}

TagDetailsTest.java
package com.lishman.qa.browser;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.support.PageFactory;

import com.lishman.qa.browser.pageobjects.TagDetailsPage;
import com.lishman.qa.browser.pageobjects.TagListPage;

/**
 * Unit tests for the tag details page.
 * 
 * @author lishy
 */

public class TagDetailsTest extends BrowserTest {
    
    private TagDetailsPage tagDetailsPage;
    
    @Before
    public void setUp() {
        getDriver().get(getTestHost() + "/qa1/app/tag-details/1");
        tagDetailsPage = PageFactory.initElements(getDriver(), TagDetailsPage.class);
    }
    
    // header
    
    @Test
    public void correctHeaderIsDisplayedWhenPageIsLoaded() {
        assertThat(tagDetailsPage.headerText(), equalTo("Tag Details"));
    }
    
    // tag details

    @Test
    public void correctTagDetailsAreDisplayedForSpecifiedIdentifier() {
        assertThat(tagDetailsPage.getTagName(),         equalTo("one"));
        assertThat(tagDetailsPage.getTagDescription(),  equalTo("description one"));
    
        getDriver().get(getTestHost() + "/qa1/app/tag-details/2");
        tagDetailsPage = PageFactory.initElements(getDriver(), TagDetailsPage.class);
        assertThat(tagDetailsPage.getTagName(),         equalTo("two"));
        assertThat(tagDetailsPage.getTagDescription(),  equalTo("description two"));
    }
    
    // back link
    
    @Test
    public void tagListPageIsDisplayedWhenBackLinkIsClicked() {
        TagListPage tagListPage = tagDetailsPage.clickBackLink();
        assertThat(tagListPage.getTitle(),   equalTo("Tags"));
    }

    // themes
    // locale
    

}

TagListTest.java
package com.lishman.qa.browser;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.support.PageFactory;

import com.lishman.qa.browser.pageobjects.MainMenuPage;
import com.lishman.qa.browser.pageobjects.TagDetailsPage;
import com.lishman.qa.browser.pageobjects.TagListPage;
import com.lishman.qa.browser.pageobjects.TagMaintenancePage;

/**
 * Unit tests for the tag list page.
 * 
 * @author lishy
 */

public class TagListTest extends BrowserTest {
    
    private TagListPage tagListPage;
    
    @Before
    public void setUp() {
        getDriver().get(getTestHost() + "/qa1/app/tag-list.html");
        tagListPage = PageFactory.initElements(getDriver(), TagListPage.class);
     }
    
    // header
    
    @Test
    public void correctHeaderIsDisplayedWhenPageIsLoaded() {
        assertThat(tagListPage.headerText(), equalTo("Tags"));
    }
    
    // table

    @Test
    public void allTagsAreDisplayedInTheTable() {
        assertThat(tagListPage.getFirstTagName(),         equalTo("one"));
        assertThat(tagListPage.getFirstTagDescription(),  equalTo("description one"));
        assertThat(tagListPage.getSecondTagName(),        equalTo("two"));
        assertThat(tagListPage.getSecondTagDescription(), equalTo("description two"));
    }
    
    // display tag details
    
    @Test
    public void firstTagDetailsAreDisplayedWhenFirstTagNameIsClicked() {
        TagDetailsPage tagDetailsPage = tagListPage.selectFirstTag();
        assertThat(tagDetailsPage.getTitle(),   equalTo("Tag Details"));
        assertThat(tagDetailsPage.getTagName(), equalTo("one"));
    }
    
    @Test
    public void secondTagDetailsAreDisplayedWhenSecondTagNameIsClicked() {
        TagDetailsPage tagDetailsPage = tagListPage.selectSecondTag();
        assertThat(tagDetailsPage.getTitle(),   equalTo("Tag Details"));
        assertThat(tagDetailsPage.getTagName(), equalTo("two"));
    }
    
    // edit tag details
    
    @Test
    public void firstTagCanBeEditedWhenFirstEditButtonIsClicked() {
        TagMaintenancePage tagMaintenancePage = tagListPage.editFirstTag();
        assertThat(tagMaintenancePage.getTitle(),   equalTo("Tag Maintenance"));
        assertThat(tagMaintenancePage.getName(),    equalTo("one"));
    }
 
    @Test
    public void secondTagCanBeEditedWhenSecondEditButtonIsClicked() {
        TagMaintenancePage tagMaintenancePage = tagListPage.editSecondTag();
        assertThat(tagMaintenancePage.getTitle(),   equalTo("Tag Maintenance"));
        assertThat(tagMaintenancePage.getName(),    equalTo("two"));
    }
    
    // create
    
    @Test
    public void createTagPageIsDisplayedWhenCreateButtonIsClicked() {
        TagMaintenancePage tagMaintenancePage = tagListPage.clickCreateButton();
        assertThat(tagMaintenancePage.getTitle(),   equalTo("Tag Maintenance"));
        assertThat(tagMaintenancePage.getName(),    equalTo(""));
    }
    
    // back link
    
    @Test
    public void mainMenuPageIsDisplayedWhenBackLinkIsClicked() {
        MainMenuPage mainMenuPage = tagListPage.clickBackLink();
        assertThat(mainMenuPage.getTitle(),   equalTo("Questions & Answers"));
    }

    // themes
    // locale
    

}

TagMaintenanceTest.java
package com.lishman.qa.browser;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

import java.util.LinkedHashMap;
import java.util.List;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.support.PageFactory;

import com.lishman.qa.browser.pageobjects.TagListPage;
import com.lishman.qa.browser.pageobjects.TagMaintenancePage;
import com.lishman.qa.domain.Tag;

/**
 * Unit tests for the tag maintenance page.
 * 
 * @author lishy
 */

public class TagMaintenanceTest extends BrowserTest {
    
    TagMaintenancePage tagMaintenancePage;
    
    @Before
    public void setUp() {
        getDriver().get(getTestHost() + "/qa1/app/tag-form.html");
        tagMaintenancePage = PageFactory.initElements(getDriver(), TagMaintenancePage.class);
    }
    
    @After
    public void tearDown() {
        super.resetQaServiceStub();
     }
    
    // header
    
    @Test
    public void correctHeaderIsDisplayedWhenPageIsLoaded() {
        assertThat(tagMaintenancePage.headerText(), equalTo("Tag Maintenance"));
    }
    
    // create
    
    @Test
    public void tagIsCreatedWhenValidNameIsEnteredAndCreateButtonIsClicked() {

        tagMaintenancePage.enterName("my-tag");
        tagMaintenancePage.enterDescription("my-desc");
        TagListPage tagListPage = tagMaintenancePage.clickCreateButton();

        assertThat(tagListPage.getTitle(), equalTo("Tags"));
        Tag tag = super.getTagByNameFromStub("my-tag");
        assertThat(tag.getName(), equalTo("my-tag"));
        assertThat(tag.getDescription(), equalTo("my-desc"));
    }
    
    // no name
    
    @Test
    public void newTagWithNoNameIsRejected() {

        tagMaintenancePage.clickCreateButtonWithError();

        assertThat(tagMaintenancePage.getNameErrors(), equalTo("must not be empty"));
        List<LinkedHashMap<String, String>> tags = super.getAllTagsFromStub();
        assertThat(tags.size(), equalTo(2));
    }
    
    // duplicate
    
    @Test
    public void newTagIsRejectedIfTagNameAlreadyExists() {

        tagMaintenancePage.enterName("one");
        tagMaintenancePage.enterDescription("my-desc");
        tagMaintenancePage.clickCreateButtonWithError();

        assertThat(tagMaintenancePage.getNameErrors(), equalTo("already exists"));
        List<LinkedHashMap<String, String>> tags = super.getAllTagsFromStub();
        assertThat(tags.size(), equalTo(2));
    }
    
    // update
    
    @Test
    public void tagIsUpdatedWhenNameIsChangedAndUpdateButtonIsClicked() {
        
        getDriver().get(getTestHost() + "/qa1/app/tag-form.html?id=1");
        tagMaintenancePage = PageFactory.initElements(getDriver(), TagMaintenancePage.class);

        tagMaintenancePage.enterName("my-tag");
        tagMaintenancePage.enterDescription("my-desc");
        TagListPage tagListPage = tagMaintenancePage.clickSaveButton();

        assertThat(tagListPage.getTitle(), equalTo("Tags"));
        Tag tag = getTagByNameFromStub("my-tag");
        assertThat(tag.getName(), equalTo("my-tag"));
        assertThat(tag.getDescription(), equalTo("my-desc"));
    }
    
    // no name
    
    @Test
    public void existingTagWithNoNameIsRejected() {
        
        getDriver().get(getTestHost() + "/qa1/app/tag-form.html?id=1");
        tagMaintenancePage = PageFactory.initElements(getDriver(), TagMaintenancePage.class);

        tagMaintenancePage.enterName("");
        tagMaintenancePage.clickSaveButtonWithError();

        assertThat(tagMaintenancePage.getNameErrors(), equalTo("must not be empty"));
        List<LinkedHashMap<String, String>> tags = super.getAllTagsFromStub();
        assertThat(tags.size(), equalTo(2));
    }
    
    // duplcate
    
    @Test
    public void existingTagIsRejectedIfTagNameAlreadyExists() {

        getDriver().get(getTestHost() + "/qa1/app/tag-form.html?id=1");
        tagMaintenancePage = PageFactory.initElements(getDriver(), TagMaintenancePage.class);

        tagMaintenancePage.enterName("two");
        tagMaintenancePage.clickSaveButtonWithError();

        assertThat(tagMaintenancePage.getNameErrors(), equalTo("already exists"));
        List<LinkedHashMap<String, String>> tags = super.getAllTagsFromStub();
        assertThat(tags.size(), equalTo(2));
    }
    
    // delete

    @Test
    public void tagIsDeletedWhenDeleteButtonIsClicked() {
        
        getDriver().get(getTestHost() + "/qa1/app/tag-form.html?id=1");
        tagMaintenancePage = PageFactory.initElements(getDriver(), TagMaintenancePage.class);

        tagMaintenancePage.clickDeleteButton();
        assertThat(tagMaintenancePage.getPopupText(), equalTo("Delete Tag?"));
        TagListPage tagListPage = tagMaintenancePage.confirmDelete();
        
        assertThat(tagListPage.getTitle(), equalTo("Tags"));
        List<LinkedHashMap<String, String>> tags = super.getAllTagsFromStub();
        assertThat(tags.size(), equalTo(1));
        Tag tag = getTagByNameFromStub("two");
        assertThat(tag.getName(), equalTo("two"));
    }
    
    // cancel
    
    @Test
    public void cancellingTagMaintenanceReturnsToListPage() {
        TagListPage tagListPage = tagMaintenancePage.clickCancel();
        
        assertThat(tagListPage.getTitle(), equalTo("Tags"));
        
        // Check that no data has been updated
        List<LinkedHashMap<String, String>> tags = super.getAllTagsFromStub();
        assertThat(tags.size(), equalTo(2));
        assertThat(tags.get(0).get("name"), equalTo("one"));
        assertThat(tags.get(1).get("name"), equalTo("two"));
        
        /*
         * Note that RestTemplate does not convert that contents of the 
         * collection to Tags. Instead it uses a generic map object.
         * 
         * I could try wrapping the collection of tags in another class.
         * 
         * This is a known issue:
         *  https://jira.springsource.org/browse/SPR-7002
         *  https://jira.springsource.org/browse/SPR-7023
         */
        
    }

}

TakeATestTest.java
package com.lishman.qa.browser;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.support.PageFactory;

import com.lishman.qa.browser.pageobjects.MainMenuPage;
import com.lishman.qa.browser.pageobjects.TakeATestPage;

/**
 * Unit tests for the take a test page.
 * 
 * @author lishy
 */

public class TakeATestTest extends BrowserTest {
    
    TakeATestPage takeATestPage;
    
    @Before
    public void setUp() {
        getDriver().get(getTestHost() + "/qa1/app/ask-question.html");
        takeATestPage = PageFactory.initElements(getDriver(), TakeATestPage.class);
    }
    
    // take a test
    
    @Test
    public void testIsTakenFromStartToFinish() {
        
        // first question
     
        assertThat(takeATestPage.getTags(), equalTo("Tags: one"));
        assertThat(takeATestPage.getQuestion(), equalTo("question one"));
        assertThat(takeATestPage.isAnswerDisplayed(), equalTo(false));
        
        takeATestPage.revealAnswer();
        
        assertThat(takeATestPage.isAnswerDisplayed(), equalTo(true));
        assertThat(takeATestPage.getAnswer(), equalTo("answer one"));
        
        // second question
        
        takeATestPage.clickNextQuestion();
        
        assertThat(takeATestPage.getTags(), equalTo("Tags: one, two"));
        assertThat(takeATestPage.getQuestion(), equalTo("question two"));
        assertThat(takeATestPage.isAnswerDisplayed(), equalTo(false));
        
        takeATestPage.revealAnswer();
        
        assertThat(takeATestPage.isAnswerDisplayed(), equalTo(true));
        assertThat(takeATestPage.getAnswer(), equalTo("answer two"));
        
        // no more questions
        
        MainMenuPage mainMenuPage = takeATestPage.clickNextQuestionWithNoMoreQuestions();
        assertThat(mainMenuPage.getTitle(), equalTo("Questions & Answers"));
        
    }
    
    // cancel
    
    @Test
    public void clickingMenuLinkReturnsToMenuPage() {
        MainMenuPage mainMenuPage = takeATestPage.clickMenu();
        assertThat(mainMenuPage.getTitle(), equalTo("Questions & Answers"));
    }
    
    
    // TODO themes
    // TODO locale
     

}

JpaDaoTest-context.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">

  <import resource="classpath:com/lishman/qa/domain/DomainTest-context.xml"/>  
  <import resource="classpath:data/jpa-config.xml"/>

</beans>
JpaQuestionDaoTest.java
package com.lishman.qa.data.jpa;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;

import java.util.List;

import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.orm.jpa.JpaTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;

import com.lishman.qa.data.DatabaseTest;
import com.lishman.qa.data.QuestionDao;
import com.lishman.qa.domain.Question;
import com.lishman.qa.domain.Tag;

@ContextConfiguration("JpaDaoTest-context.xml")
public class JpaQuestionDaoTest extends DatabaseTest {

    @Autowired
    private QuestionDao questionDao;
   
    @Autowired
    private JpaTemplate jpaTemplate;
    
    @Test
    public void correctNumberOfRowsAreReturned() {        
        assertEquals(super.countRowsInTable("QUESTION"), questionDao.getAll().size());
    }
    
    // getById
    
    @Test
    public void questionIsReturnedForSpecifiedId() {
        Question question = questionDao.getById(1);
        assertEquals((Integer) 1,                question.getId());
        assertEquals("First question?",          question.getQuestion());
        assertEquals("Answer to first question", question.getAnswer());
    }
    
    @Test
    public void tagsAreLinkedToQuestions() {
        assertEquals(1, questionDao.getById(1).getTags().size());
        assertEquals(2, questionDao.getById(2).getTags().size());
        assertEquals(3, questionDao.getById(3).getTags().size());
    }
    
    // save (insert)
    
    @Test
    public void questionIsCreatedWithTags() {
        Question question = new Question("New question", "New answer");
        question.getTags().add(jpaTemplate.find(Tag.class, 2));
        question.getTags().add(jpaTemplate.find(Tag.class, 3));
        Errors errors = new BeanPropertyBindingResult(question, "question");
        
        Question savedQuestion = questionDao.save(question, errors);
        jpaTemplate.flush();
        
        assertEquals(0, errors.getAllErrors().size());
        
        Integer currId = super.getCurrentSequenceValue("QUESTION_SEQ1");
        currId *= 50; // generator has default allocationSize of 50
        assertEquals(currId, savedQuestion.getId());

        Question newQuestion = super.getQuestionById(currId);
        assertEquals(currId,         newQuestion.getId());
        assertEquals("New question", newQuestion.getQuestion());
        assertEquals("New answer" ,  newQuestion.getAnswer());
        
        List<Tag> tags = super.getTagsForQuestion(currId);
        assertEquals(2, tags.size());
        assertEquals("Second Tag", tags.get(0).getName());        
        assertEquals("Third Tag",  tags.get(1).getName());
    }
    
    @Test
    public void invalidQuestionIsRejected() {
        Question question = new Question(null, null);
        Errors errors = new BeanPropertyBindingResult(question, "question");
        
        Question savedQuestion = questionDao.save(question, errors);
        jpaTemplate.flush();
        
        assertNull(savedQuestion);
        assertEquals(2, errors.getAllErrors().size());
        assertEquals("NotBlank", errors.getFieldError("question").getCode());
        assertEquals("NotBlank", errors.getFieldError("answer").getCode());
    }
    
    /*
     * A transient object is an object that has not been persisted yet.
     */
    @Test(expected=InvalidDataAccessApiUsageException.class)
    public void transientTagIsRejected() {
        Question question = new Question("New question", "New answer");
        question.getTags().add(new Tag("Transient", "Transient Object"));
        Errors errors = new BeanPropertyBindingResult(question, "question");
        
        questionDao.save(question, errors);
        jpaTemplate.flush();
    }
    
    // save (update)
    
    @Test
    public void questionIsUpdatedWithNewTags() {
        Question question = questionDao.getById(1);
        assertEquals((Integer) 1,                question.getId());
        assertEquals("First question?",          question.getQuestion());
        assertEquals("Answer to first question", question.getAnswer());
        
        Errors errors = new BeanPropertyBindingResult(question, "question");

        question.setQuestion("Updated Question");
        question.setAnswer("Updated Answer");
        question.getTags().clear();
        question.getTags().add(jpaTemplate.find(Tag.class, 2));
        question.getTags().add(jpaTemplate.find(Tag.class, 3));
        
        questionDao.save(question, errors);
        jpaTemplate.flush();
        
        assertEquals(0, errors.getAllErrors().size());
        
        Question questionFromDb = super.getQuestionById(1);
        assertEquals((Integer) 1,         questionFromDb.getId());
        assertEquals("Updated Question",  questionFromDb.getQuestion());
        assertEquals("Updated Answer",    questionFromDb.getAnswer());
        
        List<Tag> tags = super.getTagsForQuestion(1);
        assertEquals(2, tags.size());
        assertEquals("Second Tag", tags.get(0).getName());        
        assertEquals("Third Tag",  tags.get(1).getName());
    }
    
    // delete
    
    @Test
    public void questionWithoutTagsIsDeleted() {
        Question question = questionDao.getById(4);
        assertNotNull(question);
        
        questionDao.delete(question);
        jpaTemplate.flush();
        
        int rowCount = super.simpleJdbcTemplate.queryForInt(
                "SELECT count(*) FROM question WHERE ques_id = 4");
        assertEquals(0, rowCount);
    }
    
    @Test
    public void questionWithTagsIsDeleted() {
        
        /* In a many-to-many relationship, when the <i>owner</i> entity is deleted any
         * rows referencing it in the association table are deleted also.
         * 
         * The same is not true for the inverse side.
         * Rows in the association table are not automatically removed which may
         * cause a foreign key constraint violation.
         * 
         * Note: this has nothing to do with the directionality of the relationship.
         */
        
        /*
         * This works in the context of tags and questions.
         * 
         * A tag cannot be removed if it is used by questions.
         * However, a question can be removed even if it has tags.
         */
        assertEquals(3, associationTableRowCount(3));
        
        Question question = questionDao.getById(3);
        assertNotNull(question);
        
        questionDao.delete(question);
        jpaTemplate.flush();
        
        // Associated rows on the association table are removed.
        
        assertEquals(0, associationTableRowCount(3));
    }
    
    private int associationTableRowCount(int questionId) {
        int associationTableRows = simpleJdbcTemplate.queryForInt(
                "SELECT count(*) FROM question_tag WHERE ques_id = ?", questionId);
        return associationTableRows;
    }
}

JpaTagDaoTest.java
package com.lishman.qa.data.jpa;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.jpa.JpaTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;

import com.lishman.qa.data.DatabaseTest;
import com.lishman.qa.data.TagDao;
import com.lishman.qa.domain.Tag;

@ContextConfiguration("JpaDaoTest-context.xml")
public class JpaTagDaoTest extends DatabaseTest {

    @Autowired
    private TagDao tagDao;
   
    @Autowired
    private JpaTemplate jpaTemplate;
    
    // getAll
    
    @Test
    public void correctNumberOfRowsAreReturned() {        
        assertEquals(super.countRowsInTable("TAG"), tagDao.getAll().size());
    }
    
    // getById
    
    @Test
    public void tagIsReturnedForSpecifiedId() {
        Tag tag = tagDao.getById(1);
        assertEquals(new Integer(1),             tag.getId());
        assertEquals("First Tag",                tag.getName());
        assertEquals("Description of First Tag", tag.getDescription());
        
        assertEquals(3, tag.getQuestions().size());
    }
    
    // getByName
    
    @Test
    public void tagIsReturnedForSpecifiedName() {
        Tag tag = tagDao.getByName("Second Tag");
        assertEquals(new Integer(2),              tag.getId());
        assertEquals("Second Tag",                tag.getName());
        assertEquals("Description of Second Tag", tag.getDescription());
        
        assertEquals(2, tag.getQuestions().size());
    }
    
    // save (insert)
    
    @Test
    public void newValidTagIsCreated() {
        Tag tag = new Tag("New Tag Name", "New Tag description");
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        Tag savedTag = tagDao.save(tag, errors);
        jpaTemplate.flush();
        
        assertEquals(0, errors.getAllErrors().size());
        
        Integer currId = super.getCurrentSequenceValue("TAG_SEQ1");
        currId *= 50; // generator has default allocationSize of 50
        assertEquals(currId, savedTag.getId());
        
        assertEquals(currId,                 savedTag.getId());
        assertEquals("New Tag Name",         savedTag.getName());
        assertEquals("New Tag description" , savedTag.getDescription());

        Tag newTag = super.getTagById(currId);
        assertEquals(currId,                 newTag.getId());
        assertEquals("New Tag Name",         newTag.getName());
        assertEquals("New Tag description" , newTag.getDescription());
    }
    
    @Test
    public void newTagWithNoNameIsRejected() {
        Tag tag = new Tag(null, "New Tag description");
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        Tag savedTag = tagDao.save(tag, errors);
        jpaTemplate.flush();
        
        assertNull(savedTag);
        assertEquals(1, errors.getAllErrors().size());
        assertEquals("NotBlank", errors.getFieldError("name").getCode());
    }
  
    @Test
    public void newTagWithDuplicateNameIsRejected() {
        Tag tag = new Tag("First Tag", "Description");
        BindingResult result = new BeanPropertyBindingResult(tag, "tag");
        
        Tag savedTag = tagDao.save(tag, result);

        assertNull(savedTag);
        assertEquals(1, result.getAllErrors().size());
        assertTrue(result.hasFieldErrors("name"));
        assertEquals("exists", result.getFieldError("name").getDefaultMessage());
    }
    
    // save (update)

    @Test
    public void existingValidTagIsUpdated() {
        Tag tag = tagDao.getById(1);
        assertEquals((Integer) 1,                tag.getId());
        assertEquals("First Tag",                tag.getName());
        assertEquals("Description of First Tag", tag.getDescription());

        tag.setName("Updated Name");
        tag.setDescription("Updated Description");
        
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        Tag savedTag = tagDao.save(tag, errors);
        jpaTemplate.flush();
        
        assertEquals(0, errors.getAllErrors().size());
        
        assertEquals((Integer) 1,            savedTag.getId());
        assertEquals("Updated Name",         savedTag.getName());
        assertEquals("Updated Description",  savedTag.getDescription());
        
        Tag newTag = super.getTagById(1);
        assertEquals((Integer) 1,           newTag.getId());
        assertEquals("Updated Name",        newTag.getName());
        assertEquals("Updated Description", newTag.getDescription());
    }
    
    @Test
    public void existingTagWithNoNameIsRejected() {
        Tag tag = tagDao.getById(1);
        assertEquals((Integer) 1,                tag.getId());
        assertEquals("First Tag",                tag.getName());
        assertEquals("Description of First Tag", tag.getDescription());

        tag.setName(null);
        
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        Tag savedTag = tagDao.save(tag, errors);
        
        assertNull(savedTag);
        assertEquals(1, errors.getAllErrors().size());
        assertEquals("NotBlank", errors.getFieldError("name").getCode());
    }
    
    @Test
    public void existingTagWithDuplicateNameIsRejected() {
//        Tag tag = new Tag(3, "Second Tag", "Description");
        Tag tag = new Tag("Second Tag", "Description");
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        Tag savedTag = tagDao.save(tag, errors);

        assertNull(savedTag);
        assertEquals(1, errors.getAllErrors().size());
        assertTrue(errors.hasFieldErrors("name"));
        assertEquals("exists", errors.getFieldError("name").getDefaultMessage());
    }

    @Test
    public void tagWithNoQuestionsIsDeleted() {
        Tag tag = tagDao.getById(4);
        assertNotNull(tag);
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        tagDao.delete(tag, errors);
        jpaTemplate.flush();
        
        int rowCount = super.simpleJdbcTemplate.queryForInt(
                "SELECT count(*) FROM tag WHERE tag_id = 4");
        assertEquals(0, rowCount);
    }
  
    @Test
    public void tagWithQuestionsIsNotDeleted() {
        Tag tag = tagDao.getById(3);
        assertNotNull(tag);
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        tagDao.delete(tag, errors);

        assertEquals(1, errors.getAllErrors().size());
        assertTrue(errors.hasGlobalErrors());
        assertEquals("has questions", errors.getGlobalError().getDefaultMessage());

        // Associated rows on the association table are NOT removed.
        // An foreign key violation error is raised.
    }

}

DatabaseConstraintTest.java
package com.lishman.qa.data;

import static org.junit.Assert.assertEquals;

import org.junit.Test;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.UncategorizedSQLException;
import org.springframework.test.context.ContextConfiguration;

import com.lishman.qa.domain.Question;
import com.lishman.qa.domain.Tag;

@ContextConfiguration("DatabaseTest-context.xml")
public class DatabaseConstraintTest extends DatabaseTest {
    
    // TAG
    
    @Test
    public void tagIdIsUsedIfSpecified() {
        simpleJdbcTemplate.update("INSERT INTO tag (tag_id, tag_name) " +
                                  "VALUES (999, 'New Tag Name')");
        Tag tag = super.getTagById(999);
        assertEquals("New Tag Name", tag.getName());
    }
    
    @Test
    public void tagIdIsGeneratedIfNotSpecified() {
        simpleJdbcTemplate.update("INSERT INTO tag (tag_name) VALUES ('New Tag Name')");
        Tag tag = super.getTagById(super.getCurrentSequenceValue("TAG_SEQ1"));
        assertEquals("New Tag Name", tag.getName());
    }
    
    @Test(expected=DuplicateKeyException.class)
    public void duplicateTagIdIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO tag (tag_id, tag_name) " +
                                  "VALUES (999, 'New Tag Name')");
        simpleJdbcTemplate.update("INSERT INTO tag (tag_id, tag_name) " +
                                  "VALUES (999, 'Other Tag Name')");
    }
    
    @Test(expected=DuplicateKeyException.class)
    public void duplicateTagNameIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO tag (tag_name) VALUES ('New Tag Name')");
        simpleJdbcTemplate.update("INSERT INTO tag (tag_name) VALUES ('New Tag Name')");
    }

    @Test(expected=DataIntegrityViolationException.class)
    public void tagWithNoNameIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO tag (tag_name) VALUES (null)");
    }
    
    @Test(expected=DataIntegrityViolationException.class)
    public void tagWithBlankNameIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO tag (tag_name) VALUES (' ')");
    }
    
    @Test(expected=UncategorizedSQLException.class)
    public void tagWithNameTooBigIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO tag (tag_name) VALUES (rpad('x', 21))");
    }
    
    @Test(expected=UncategorizedSQLException.class)
    public void tagWithDescriptionTooBigIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO tag (tag_name, tag_desc) " +
                                  "VALUES ('x', rpad('x', 1001))");
    }
    
    // QUESTION
    
    @Test
    public void questionIdIsUsedIfSpecified() {
        simpleJdbcTemplate.update("INSERT INTO question (ques_id, question, answer) " +
                                  "VALUES (999, 'New Question', 'New Answer')");
        Question question = super.getQuestionById(999);
        assertEquals("New Question", question.getQuestion());
    }
    
    @Test
    public void questionIdIsGeneratedIfNotSpecified() {
        simpleJdbcTemplate.update("INSERT INTO question (question, answer) " +
                                  "VALUES ('New Question', 'New Answer')");
        Question question = super.getQuestionById(super.getCurrentSequenceValue("QUESTION_SEQ1"));
        assertEquals("New Question", question.getQuestion());
    }

    @Test(expected=DuplicateKeyException.class)
    public void duplicateQuestionIdIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO question (ques_id, question, answer) " +
                                  "VALUES (999, 'New Question', 'New Answer')");    
        simpleJdbcTemplate.update("INSERT INTO question (ques_id, question, answer) " +
                                  "VALUES (999, 'New Question', 'New Answer')");    
    }

    @Test(expected=DataIntegrityViolationException.class)
    public void questionWithNoQuestionTextIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO question (answer) " +
                                  "VALUES ('New Answer')"); 
    }
    
    @Test(expected=DataIntegrityViolationException.class)
    public void questionWithBlankQuestionTextIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO question (question, answer) " +
                                  "VALUES (' ', 'New Answer')"); 
    }
    
    @Test(expected=DataIntegrityViolationException.class)
    public void questionWithNoAnswerTextIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO question (question) " +
                                  "VALUES ('New Question')"); 
    }
    
    @Test(expected=DataIntegrityViolationException.class)
    public void questionWithBlankAnswerTextIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO question (question, answer) " +
                                  "VALUES ('New Question', ' ')"); 
    }    
    
    @Test(expected=UncategorizedSQLException.class)
    public void questionWithQuestionTextTooBigIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO question (question, answer) " +
                                  "VALUES (rpad('x', 4000) || 'x', 'New Answer')"); 
    }
    
    @Test(expected=UncategorizedSQLException.class)
    public void questionWithAnswerTextTooBigIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO question (question, answer) " +
                                  "VALUES ('New Question', rpad('x', 4000) || 'x')"); 
    }

    // QUESTION_TAG
    
    @Test(expected=DataIntegrityViolationException.class)
    public void questionTagIsRejectedIfTagIdIsNull() {
        simpleJdbcTemplate.update("INSERT INTO question_tag (ques_id, tag_id) " +
                                  "VALUES (1, null)");
    }
    
    @Test(expected=DataIntegrityViolationException.class)
    public void questionTagIsRejectedIfQuestionIdIsNull() {
        simpleJdbcTemplate.update("INSERT INTO question_tag (ques_id, tag_id) " +
                                  "VALUES (null, 1)");
    }
    
    @Test(expected=DataIntegrityViolationException.class)
    public void questionTagIsRejectedIfTagDoesNotExist() {
        simpleJdbcTemplate.update("INSERT INTO question_tag (ques_id, tag_id) " +
                                  "VALUES (1, 999)");
    }
    
    @Test(expected=DataIntegrityViolationException.class)
    public void questionTagIsRejectedIfQuestionDoesNotExist() {
        simpleJdbcTemplate.update("INSERT INTO question_tag (ques_id, tag_id) " +
                                  "VALUES (999, 1)");
    }

    @Test(expected=DuplicateKeyException.class)
    public void duplicateQuestionTagCombinationIsRejected() {
        simpleJdbcTemplate.update("INSERT INTO question_tag (ques_id, tag_id) " +
                                  "VALUES (3, 3)");
        simpleJdbcTemplate.update("INSERT INTO question_tag (ques_id, tag_id) " +
                                  "VALUES (3, 3)");
    }
    
    @Test(expected=DataIntegrityViolationException.class)
    public void tagWithAssociatedQuestionsCannotBeDeleted() {
        simpleJdbcTemplate.update("DELETE FROM tag WHERE tag_id = 1");
    }
    
    @Test(expected=DataIntegrityViolationException.class)
    public void questionWithAssociatedTagsCannotBeDeleted() {
        simpleJdbcTemplate.update("DELETE FROM question WHERE ques_id = 1");
    }

}

DatabaseTest-context.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

  <bean id="databaseUtils"
        class="com.lishman.qa.data.DatabaseUtils"
        p:dataSource-ref="dataSource"/>

  <context:property-placeholder location="classpath:data/jdbc-test.properties"/>

  <import resource="classpath:data/datasource-config.xml"/>
  
  <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
        p:dataSource-ref="dataSource"/>

</beans>
DatabaseTest.java
package com.lishman.qa.data;

import java.util.List;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;

import com.lishman.qa.domain.Question;
import com.lishman.qa.domain.Tag;

/**
 * Acts as a superclass to other database tests.
 * Application context is inherited by subclasses.
 * 
 * @author lishy
 *
 */

// Same name as context file so it does not need to be named explicitly.
@ContextConfiguration
public class DatabaseTest extends AbstractTransactionalJUnit4SpringContextTests {

    // TODO use reflection to inject ids rather than relying on setId
    
    protected int getCurrentSequenceValue(String sequenceName) {
        return simpleJdbcTemplate.queryForInt("SELECT " + sequenceName + ".currval FROM dual");
    } 
    
    protected Tag getTagById(int tagId) {
        return super.simpleJdbcTemplate.queryForObject(
                "SELECT tag_id AS id, tag_name AS name, tag_desc AS description " +
                "FROM tag " +
                "WHERE tag_id = ?",
                new BeanPropertyRowMapper<Tag>(Tag.class), tagId);
    } 
    
    protected Question getQuestionById(int questionId) {
        return super.simpleJdbcTemplate.queryForObject(
                "SELECT ques_id AS id, question, answer " +
                "FROM question " +
                "WHERE ques_id = ?",
                new BeanPropertyRowMapper<Question>(Question.class), questionId);
    } 
    
    protected List<Tag> getTagsForQuestion(int questionId) {
        List<Tag> tags = super.simpleJdbcTemplate.query(
                "SELECT tag_id AS id, tag_name AS name, tag_desc AS desription " +
                "FROM question" +
                " JOIN question_tag USING (ques_id)" +
                " JOIN tag USING (tag_id) " +
                "WHERE ques_id = ? " +
                "ORDER BY tag_name",
                new BeanPropertyRowMapper<Tag>(Tag.class), questionId);
        return tags;
    } 
}

DatabaseUtils.java
package com.lishman.qa.data;

import java.io.File;
import java.util.List;

import javax.sql.DataSource;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;

public class DatabaseUtils {
    
    private SimpleJdbcTemplate jdbc;
  
    @Autowired
    public void setDataSource(DataSource dataSource) {
        jdbc = new SimpleJdbcTemplate(dataSource);
    }

    public static void runSqlFromFile(String fileName) {
        ApplicationContext context = new FileSystemXmlApplicationContext("classpath:com/lishman/qa/data/DatabaseTest-context.xml");
        DatabaseUtils dbUtil = context.getBean("databaseUtils", DatabaseUtils.class);
        dbUtil.runSqlStatementsFromFile(fileName);
    }
    
    
    public void runSqlStatementsFromFile(String fileName) {
        try {
            List<String> lines = FileUtils.readLines(new File(fileName));
            for (String line : lines) {
                if (StringUtils.isNotBlank(line) && !line.trim().startsWith("--")) {
                    String sql = StringUtils.chomp(line.trim(), ";");
                    jdbc.update(sql);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    } 
 }

DomainTest-context.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
  
  <import resource="classpath:domain-config.xml"/>
          
</beans>
QuestionValidatorTest.java
package com.lishman.qa.domain;

import static org.junit.Assert.assertEquals;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("DomainTest-context.xml")
public class QuestionValidatorTest {

    @Autowired
    DomainValidator validator;
    
    @Test
    public void validQuestionAndAnswerIsAccepted() {
        Question question = new Question("question", "answer");
        Errors errors = new BeanPropertyBindingResult(question, "question");
        
        validator.validate(question, errors);
        
        assertEquals(0, errors.getAllErrors().size());
    }
       
    @Test
    public void questionWithNullValuesIsRejected() {
        Question question = new Question(null, null);
        Errors errors = new BeanPropertyBindingResult(question, "question");
        
        validator.validate(question, errors);
        
        assertEquals(2, errors.getAllErrors().size());
        assertEquals("NotBlank", errors.getFieldError("question").getCode()); 
        assertEquals("NotBlank", errors.getFieldError("answer").getCode());  
    }
    
    @Test
    public void questionWithEmptyValuesIsRejected() {
        Question question = new Question("", "");
        Errors errors = new BeanPropertyBindingResult(question, "question");
        
        validator.validate(question, errors);
        
        assertEquals(2, errors.getAllErrors().size());
        assertEquals("NotBlank", errors.getFieldError("question").getCode());  
        assertEquals("NotBlank", errors.getFieldError("answer").getCode());  
    }
    
    @Test
    public void questionWithBlankValuesIsRejected() {
        Question question = new Question("", "");
        Errors errors = new BeanPropertyBindingResult(question, "question");
        
        validator.validate(question, errors);
        
        assertEquals(2, errors.getAllErrors().size());
        assertEquals("NotBlank", errors.getFieldError("question").getCode());  
        assertEquals("NotBlank", errors.getFieldError("answer").getCode());  
    }

}

TagValidatorTest.java
package com.lishman.qa.domain;

import static org.junit.Assert.assertEquals;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("DomainTest-context.xml")
public class TagValidatorTest {

    @Autowired
    DomainValidator validator;
    
    // Tag Name
    
    @Test
    public void tagWithNameIsAccepted() {
        Tag tag = new Tag("Name", "Description");
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        validator.validate(tag, errors);
        
        assertEquals(0, errors.getAllErrors().size());
    }
    
    @Test
    public void tagWithNullNameIsRejected() {
        Tag tag = new Tag(null, "Description");
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        validator.validate(tag, errors);
        
        assertEquals(1, errors.getAllErrors().size());
        assertEquals("NotBlank", errors.getFieldError("name").getCode());
    }
    
    @Test
    public void tagWithEmptyNameIsRejected() {
        Tag tag = new Tag("", "Description");
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        validator.validate(tag, errors);
        
        assertEquals(1, errors.getAllErrors().size());
        assertEquals("NotBlank", errors.getFieldError("name").getCode());
    }
    
    @Test
    public void tagWithBlankNameIsRejected() {
        Tag tag = new Tag("   ", "Description");
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        validator.validate(tag, errors);
        
        assertEquals(1, errors.getAllErrors().size());
        assertEquals("NotBlank", errors.getFieldError("name").getCode());
    }
    
    // Tag Description
    
    @Test
    public void tagWithDescriptionIsAccepted() {
        Tag tag = new Tag("Name", "Description");
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        validator.validate(tag, errors);
        
        assertEquals(0, errors.getAllErrors().size());
    }
    
    @Test
    public void tagWithNullDescriptionIsAccepted() {
        Tag tag = new Tag("Name", null);
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        validator.validate(tag, errors);
        
        assertEquals(0, errors.getAllErrors().size());
    }
    
    @Test
    public void tagWithEmptyDescriptionIsAccepted() {
        Tag tag = new Tag("Name", "");
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        validator.validate(tag, errors);
        
        assertEquals(0, errors.getAllErrors().size());
    }
    
    @Test
    public void tagWithBlankDescriptionIsAccepted() {
        Tag tag = new Tag("Name", "   ");
        Errors errors = new BeanPropertyBindingResult(tag, "tag");
        
        validator.validate(tag, errors);
        
        assertEquals(0, errors.getAllErrors().size());
    }

}

QaServiceStub.java
package com.lishman.qa.service;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.validation.Errors;

import com.lishman.qa.domain.DomainValidator;
import com.lishman.qa.domain.Question;
import com.lishman.qa.domain.Tag;

/**
 * A QA service stub.
 * 
 * The @Primary annotation ensures that this class will be used
 * when Spring scans the service package and finds the real and
 * the stub versions of QaService.
 * 
 * This allows us to use the real implementations with the 
 * option to override some of these classes just by dropping 
 * stubs into the service package in test.
 * 
 * Obviously, these will not be picked up in the live build
 * because only the src directory is compiled.
 * 
 * Note! This has changed - we either include the stub or include the
 * real implementation. @Primary is no longer needed.
 * 
 * 
 * 
 * Additional methods allow the dummy tag values to be accessed.
 * 
 * @author lishy
 *
 */
// TODO use a custom annotation (eg @Stub) for include / exclude filters
@Service
@Primary
public class QaServiceStub implements QaService {
    
    // TODO tidy this
    
    @Autowired
    private DomainValidator validator;
    
    private static List<Tag> tags = new ArrayList<Tag>();
    private static int tagId = 0;
    
    private static List<Question> questions = new ArrayList<Question>();
    private static int questionId = 0;
    
    static {
        reset();
    }

    // Questions

    @Override
    public List<Question> getQuestions() {
        List<Question> questionsCopy = new ArrayList<Question>();
        for (Question question : questions) {
            questionsCopy.add(copyOf(question));
        }
        return questionsCopy;
    }

    /**
     * The questions are not actually shuffled in the stub.
     * This allows us to guarantee the order during testing.
     */
    @Override
    public List<Question> getShuffledQuestions() {
        return getQuestions();
    }

    @Override
    public Question getQuestionById(int questionId) {
        for (Question question : questions) {
            if (question.getId() == questionId) {
                return copyOf(question);
            }
        }
        return null;
    }

    @Override
    public Question saveQuestion(Question question, Errors errors) {
        validator.validate(question, errors);
        if (errors.hasErrors()) {
            return null;
        }

        Tag existingQuestion = this.getTagByName(question.getQuestion());
        if (existingQuestion != null
             && (question.isNew() || 
                 !question.getId().equals(existingQuestion.getId()))) {
            errors.rejectValue("name", "validation.exists", "exists");
            return null;
        }
        
        if (question.isNew()) {
            question.setId(++questionId);
        } else {
            deleteQuestion(question);
        }
        questions.add(question);
        return question; 
    }

    @Override
    public void deleteQuestion(Question question) {
        Iterator<Question> iter = questions.iterator();
        while (iter.hasNext()) {
            if (iter.next().getId() == question.getId()) {
                iter.remove();
            }
        }
    }

    // Tags
    
    @Override
    public List<Tag> getTags() {
        return tags;
    }

    @Override
    public Tag getTagById(int tagId) {
        for (Tag tag : tags) {
            if (tag.getId() == tagId) {
                return copyOf(tag);
            }
        }
        return null;
    }

    @Override
    public Tag getTagByName(String tagName) {
        for (Tag tag : tags) {
            if (StringUtils.equals(tag.getName(), tagName)) {
                return copyOf(tag);
            }
        }
        return null;
    }

    @Override
    public Tag saveTag(Tag tag, Errors errors) {
        validator.validate(tag, errors);
        if (errors.hasErrors()) {
            return null;
        }

        Tag existingTag = this.getTagByName(tag.getName());
        if (existingTag != null
             && (tag.isNew() || 
                 !tag.getId().equals(existingTag.getId()))) {
            errors.rejectValue("name", "validation.exists", "exists");
            return null;
        }
        
        if (tag.isNew()) {
            tag.setId(++tagId);

        } else {
            deleteTag(tag, errors);
        }
        tags.add(tag);
        return tag; 
    }
    
    @Override
    public void deleteTag(Tag tag, Errors errors) {
        Iterator<Tag> iter = tags.iterator();
        while (iter.hasNext()) {
            if (iter.next().getId() == tag.getId()) {
                iter.remove();
            }
        }
    }
    
    // Extra methods to control the stub data
    
    public static void reset() {
        
        tagId = 0;
        tags.clear();
        Tag tagOne = createTag("one");
        tags.add(tagOne);
        Tag tagTwo = createTag("two");
        tags.add(tagTwo);
        
        questionId = 0;
        questions.clear();
        
        Question questionOne = createQuestion("one");
        SortedSet<Tag> tagsForQuestionOne = new TreeSet<Tag>();
        tagsForQuestionOne.add(tagOne);
        questionOne.setTags(tagsForQuestionOne);
        questions.add(questionOne);

        Question questionTwo = createQuestion("two");
        SortedSet<Tag> tagsForQuestionTwo = new TreeSet<Tag>();
        tagsForQuestionTwo.add(tagOne);
        tagsForQuestionTwo.add(tagTwo);
        questionTwo.setTags(tagsForQuestionTwo);
        questions.add(questionTwo);
    }
    
    public static Question createQuestion (String number) {
        Question question = new Question("question " + number, "answer " + number);
        question.setId(++questionId);
        return question;
    }
    
    public static Tag createTag (String name) {
        Tag tag = new Tag(name, "description " + name);
        tag.setId(++tagId);
        return tag;
    }
    
    public Question getQuestionByText(String questionText) {
        for (Question question : questions) {
            if (StringUtils.equals(question.getQuestion(), questionText)) {
                return question;
            }
        }
        return null;
    }
    
    private Tag copyOf (Tag source) {
        Tag target = new Tag(source.getName(), source.getDescription());
        target.setId(source.getId());
        return target;
    }
    
    private Question copyOf (Question source) {
        Question target = new Question(source.getQuestion(), source.getAnswer());
        target.setId(source.getId());
        if (source.getTags() != null) {
            SortedSet<Tag> targetTags = new TreeSet<Tag>();
            for (Tag tag : source.getTags()) {
                targetTags.add(copyOf(tag));
            }
            target.setTags(targetTags);
        }
        return target;
    }
 
}

QaServiceTest.java
package com.lishman.qa.service;

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.validation.BindingResult;

import com.lishman.qa.data.QuestionDao;
import com.lishman.qa.data.TagDao;
import com.lishman.qa.domain.Question;
import com.lishman.qa.domain.Tag;

@RunWith(MockitoJUnitRunner.class)
public class QaServiceTest {

    @Mock private QuestionDao questionDao;
    @Mock private TagDao tagDao;
    @Mock private BindingResult bindingResult;
    
    @InjectMocks private QaServiceImpl qaService = new QaServiceImpl(); 

    // Questions
    
    @Test
    public void allQuestionsAreReturned() {  
        
        qaService.getQuestions();
        
        verify(questionDao).getAll();
    }

    /**
     * @Test(timeout=5000) works better than @Timed because
     * the former interrupts the executing thread
     * where the later does not.
     */
    @Test(timeout=5000)
    public void questionsAreReturnedInRandomOrder() {  

        List<Question> mockQuestions = new ArrayList<Question>();
        mockQuestions.add(new Question("one", "answer one"));
        mockQuestions.add(new Question("two", "answer two"));

        when(qaService.getQuestions()).thenReturn(mockQuestions);
        
        // Get the first question then run repeatedly until the first question is different.
        String firstRun = qaService.getShuffledQuestions().get(0).getQuestion();
        while (true) {
            String secondRun = qaService.getShuffledQuestions().get(0).getQuestion();
            if (!firstRun.equals(secondRun)) {
                break;
            }
        };

    }
    
    @Test
    public void questionIsReturnedForSpecifiedId() {       
        
        qaService.getQuestionById(123);
        
        verify(questionDao).getById(123);
    }

    @Test
    public void questionDaoIsCalledWhenSaving() {
        Question question = new Question();
        
        qaService.saveQuestion(question, bindingResult);
        
        verify(questionDao).save(question, bindingResult);
    }

    @Test
    public void questionDaoIsCalledWhenDeleting() {
        Question question = new Question("question", "description");
        
        qaService.deleteQuestion(question);
        
        verify(questionDao).delete(question);
    }
    
    // Tags

    @Test
    public void allTagsAreReturned() {  
        
        qaService.getTags();
        
        verify(tagDao).getAll();
    }
    
    @Test
    public void tagIsReturnedForSpecifiedId() {       
        
        qaService.getTagById(123);
        
        verify(tagDao).getById(123);
    }

    @Test
    public void tagDaoIsCalledWhenSaving() {
        Tag tag = new Tag();
        
        qaService.saveTag(tag, bindingResult);
        
        verify(tagDao).save(tag, bindingResult);
    }

    @Test
    public void tagDaoIsCalledWhenDeleting() {
        Tag tag = new Tag("tag", "description");
        
        qaService.deleteTag(tag, bindingResult);
        
        verify(tagDao).delete(tag, bindingResult);
    }
    
    // TODO Test messages from resource bundles
}

BrowserTestSuite.java
package com.lishman.qa.testsuites;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

import com.lishman.qa.browser.BrowserTest;
import com.lishman.qa.browser.MainMenuTest;
import com.lishman.qa.browser.QuestionDetailsTest;
import com.lishman.qa.browser.QuestionListTest;
import com.lishman.qa.browser.QuestionMaintenanceTest;
import com.lishman.qa.browser.TagDetailsTest;
import com.lishman.qa.browser.TagListTest;
import com.lishman.qa.browser.TagMaintenanceTest;
import com.lishman.qa.browser.TakeATestTest;

/**
 * Test the user experience through the browser.
 * This is the 'real' app.
 * 
 * In addition to testing the user experience these tests act
 * as integration tests (eg wiring, themes, i18n etc). They will
 * inevitably test the entire stack from browser to database.
 *
 * @author lishy
 *
 */

/*
 * These are TDD tests, in the same way that all the other unit tests are.
 */

@RunWith(Suite.class)
@SuiteClasses({
    
    MainMenuTest.class,
    
    QuestionListTest.class,
    QuestionDetailsTest.class,
    QuestionMaintenanceTest.class,
    
    TagListTest.class,
    TagDetailsTest.class,
    TagMaintenanceTest.class,
    
    TakeATestTest.class,
    
})

public class BrowserTestSuite {
    
    private static WebDriver driver = new FirefoxDriver();
    
    @BeforeClass
    public static void databaseSetUp() {
        
        // TODO make this conditional
        /*
         * Do we need the option to test against the database?
         * If so then make this conditional.
         */
//        DatabaseUtils.runSqlFromFile("sql/unit-test-data.sql");
        
        BrowserTest.setDriver(driver);
    }
    
    @AfterClass
    public static void tearDown() {
        driver.quit();
    }
}

DataTestSuite.java
package com.lishman.qa.testsuites;

import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

import com.lishman.qa.data.DatabaseConstraintTest;
import com.lishman.qa.data.DatabaseUtils;
import com.lishman.qa.data.jpa.JpaQuestionDaoTest;
import com.lishman.qa.data.jpa.JpaTagDaoTest;

@RunWith(Suite.class)
@SuiteClasses({

    DatabaseConstraintTest.class,
    
    JpaQuestionDaoTest.class,
    JpaTagDaoTest.class,

})

public class DataTestSuite {

    @BeforeClass
    public static void databaseSetUp() {
        DatabaseUtils.runSqlFromFile("sql/unit-test-data.sql");
    }
}

FullTestSuite.java
package com.lishman.qa.testsuites;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;


@RunWith(Suite.class)
@SuiteClasses({

    UnitTestSuite.class,
    IntegrationTestSuite.class,
    
})

public class FullTestSuite {

}
IntegrationTestSuite.java
package com.lishman.qa.testsuites;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(Suite.class)
@SuiteClasses({

    // TODO uncomment this
//    DataTestSuite.class,
    BrowserTestSuite.class,
    
})

public class IntegrationTestSuite {
}

UnitTestSuite.java
package com.lishman.qa.testsuites;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

import com.lishman.qa.domain.QuestionValidatorTest;
import com.lishman.qa.domain.TagValidatorTest;
import com.lishman.qa.service.QaServiceTest;
import com.lishman.qa.web.QuestionControllerTest;
import com.lishman.qa.web.QuestionFormTest;
import com.lishman.qa.web.TagControllerTest;
import com.lishman.qa.web.TagFormTest;
import com.lishman.qa.web.TagPropertyEditorTest;

@RunWith(Suite.class)
@SuiteClasses({
    
    // service
    QaServiceTest.class,
    
    // web
    QuestionControllerTest.class,
    QuestionFormTest.class,
    TagControllerTest.class,
    TagFormTest.class,
    TagPropertyEditorTest.class,
    
    // domain
    QuestionValidatorTest.class,
    TagValidatorTest.class,

})


public class UnitTestSuite {

}

QuestionControllerTest.java
package com.lishman.qa.web;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.bind.support.SimpleSessionStatus;

import com.lishman.qa.domain.Question;
import com.lishman.qa.service.QaService;

@RunWith(MockitoJUnitRunner.class)
public class QuestionControllerTest {

    @Mock private QaService qaService;
    
    @InjectMocks private QuestionController questionController = new QuestionController(); 

    @Test
    public void allQuestionsAreReturned() { 
        List<Question> questions = Collections.singletonList(new Question());
        when(qaService.getQuestions()).thenReturn(questions);
        
        List<Question> returnedQuetions = questionController.getQuestions();

        assertThat(returnedQuetions, sameInstance(questions));
    }
    
    @Test
    public void askQuestionForSpecifiedId() {
        Question question = new Question();
        when(qaService.getQuestionById(123)).thenReturn(question);
        Model model = new ExtendedModelMap();
        
        String viewName = questionController.askQuestion(123, model);
        
        assertThat(viewName, equalTo("question-details"));  
        assertThat((Question) model.asMap().get("question"), sameInstance(question));
    }

    @Test
    public void questionsAreAsked() {
        
        final Question q1 = new Question("question1", "answer1");
        final Question q2 = new Question("question2", "answer2");

        List<Question> mockQuestions = new ArrayList<Question>();
        mockQuestions.add(q1);
        mockQuestions.add(q2);

        when(qaService.getShuffledQuestions()).thenReturn(mockQuestions);

        Model model = new ExtendedModelMap();
        SessionStatus status = new SimpleSessionStatus();
        
        assertThat(questionController.askQuestion(model, status), equalTo("ask-question"));
        Question returnedQ1 = (Question) model.asMap().get("question");
        assertThat(status.isComplete(), equalTo(false));
        
        assertThat(questionController.askQuestion(model, status), equalTo("ask-question"));
        Question returnedQ2 = (Question) model.asMap().get("question");
        assertThat(status.isComplete(), equalTo(false));
        
        assertThat(questionController.askQuestion(model, status), equalTo("main-menu"));
        assertThat(status.isComplete(), equalTo(true));
        
        assertThat(returnedQ1, not(sameInstance(returnedQ2)));
        
        List<Question> questions = new ArrayList<Question>();
        questions.add(q1);
        questions.add(q2);

        assertThat(questions, hasItem(returnedQ1));
        assertThat(questions, hasItem(returnedQ2));
        
        /*
         * Make sure that the Hamcrest jar is above the JUnit jar in Order and Export
         * otherwise we will get NoSuchMethodError.
         */
            
    }
}

QuestionFormTest.java
package com.lishman.qa.web;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.bind.support.SimpleSessionStatus;

import com.lishman.qa.domain.Question;
import com.lishman.qa.service.QaService;

@RunWith(MockitoJUnitRunner.class)
public class QuestionFormTest {

    @Mock private QaService qaService;
    @Mock private BindingResult bindingResult;
    
    @InjectMocks private QuestionForm questionForm = new QuestionForm(); 
    
    
    @Test
    public void emptyQuestionObjectIsReturnedWhenNoIdIsSupplied() {
        
        Question question = questionForm.setUpForm(null);
        
        assertNull(question.getId());
        assertNull(question.getQuestion());
        assertNull(question.getAnswer());
        verify(qaService, never()).getQuestionById(anyInt());

    }

    @Test
    public void populatedQuestionObjectIsReturnedWhenIdIsSupplied() {
        Question question = new Question();
        when(qaService.getQuestionById(123)).thenReturn(question);
         
        Question returnedQuestion = questionForm.setUpForm(123);
         
        assertThat(returnedQuestion, sameInstance(question));
    }

    @Test
    public void questionListIsDisplayedAfterSuccessfulSave() {
        Question question = new Question();
        SessionStatus sessionStatus = new SimpleSessionStatus();
        when(bindingResult.hasErrors()).thenReturn(false);
        
        String viewName = questionForm.save(question, bindingResult, sessionStatus);
        
        assertThat(viewName, equalTo("redirect:question-list"));
        verify(qaService).saveQuestion(question, bindingResult);
        assertThat(sessionStatus.isComplete(), equalTo(true));
    }
    
    @Test
    public void questionFormIsDisplayedAfterFailedSave() {
        Question question = new Question();
        SessionStatus sessionStatus = new SimpleSessionStatus();
        when(bindingResult.hasErrors()).thenReturn(true);
        
        String viewName = questionForm.save(question, bindingResult, sessionStatus);
        
        assertThat(viewName, equalTo("question-form"));
        verify(qaService).saveQuestion(question, bindingResult);
        assertThat(sessionStatus.isComplete(), equalTo(false));
    }
    
    
    @Test
    public void questionListIsDisplayedAfterSuccessfulDelete() {
        Question question = new Question();
        Model model = new ExtendedModelMap();        
        model.addAttribute(question);
        SessionStatus sessionStatus = new SimpleSessionStatus();
        when(bindingResult.hasErrors()).thenReturn(false);
        
        String viewName = questionForm.delete(model, sessionStatus);
        
        assertThat(viewName, equalTo("redirect:question-list"));
        verify(qaService).deleteQuestion(question);
        assertThat(sessionStatus.isComplete(), equalTo(true));
    }

}

StubController.java
package com.lishman.qa.web;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.lishman.qa.domain.Question;
import com.lishman.qa.domain.Tag;
import com.lishman.qa.service.QaServiceStub;

/**
 * This class allows access to the service stub.
 * 
 * It can be used to reset or maintain the internal 
 * state of the stub, as well as examine the data.
 * 
 * @author lishy
 *
 */
@Controller
public class StubController {
    
    @Autowired(required=false)
    private QaServiceStub qaService; 
    
    @RequestMapping(value="/reset", method=RequestMethod.POST)
    @ResponseBody public String reset() {
        QaServiceStub.reset();
        return "ok";
    }
    
    @RequestMapping(value="/get-tag/{name}", method=RequestMethod.GET)
    @ResponseBody public Tag getTag(@PathVariable String name) {
        return qaService.getTagByName(name);
    }
    
    @RequestMapping(value="/get-tags", method=RequestMethod.GET)
    @ResponseBody public List<Tag> getTags() {
        return qaService.getTags();
    }
    
    @RequestMapping(value="/get-question/{question}", method=RequestMethod.GET)
    @ResponseBody public Question getQuestionByText(@PathVariable String question) {
        return qaService.getQuestionByText(question);
    }
    
    @RequestMapping(value="/get-questions", method=RequestMethod.GET)
    @ResponseBody public List<Question> getQuestions() {
        return qaService.getQuestions();
    }

}

TagControllerTest.java
package com.lishman.qa.web;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.when;

import java.util.Collections;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;

import com.lishman.qa.domain.Tag;
import com.lishman.qa.service.QaService;

@RunWith(MockitoJUnitRunner.class)
public class TagControllerTest {
    
    @Mock private QaService qaService;
    
    @InjectMocks private TagController tagController = new TagController(); 
    
    @Test
    public void allTagsAreReturned() {
        List<Tag> tags = Collections.singletonList(new Tag());
        when(qaService.getTags()).thenReturn(tags);
        
        List<Tag> returnedTags = tagController.getTags();

        assertThat(returnedTags, sameInstance(tags));
    }
    
    @Test
    public void tagIsReturnedForSpecifiedId() {
        Tag tag = new Tag();
        when(qaService.getTagById(123)).thenReturn(tag);
        Model model = new ExtendedModelMap();
        
        String viewName = tagController.getTag(123, model);
        
        assertThat(viewName, equalTo("tag-details"));  
        assertThat((Tag) model.asMap().get("tag"), sameInstance(tag));
    }

}

TagFormTest.java
package com.lishman.qa.web;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.bind.support.SimpleSessionStatus;

import com.lishman.qa.domain.Tag;
import com.lishman.qa.service.QaService;

@RunWith(MockitoJUnitRunner.class)
public class TagFormTest {

    @Mock private QaService qaService;
    @Mock private BindingResult bindingResult;
    @Mock private WebDataBinder webDataBinder;
    
    @InjectMocks private TagForm tagForm = new TagForm(); 
    
    @Test
    public void dataBinderIsSetupCorrectly() {
        
        tagForm.initBinder(webDataBinder);

        verify(webDataBinder).setDisallowedFields("id");
        // Couln't get this to work
//        verify(webDataBinder).registerCustomEditor(String.class, any(StringTrimmerEditor.class));
    }
    
    @Test
    public void emptyTagObjectIsReturnedWhenNoIdIsSupplied() {
        
        Tag tag = tagForm.setUpForm(null);
        
        assertThat(tag.getId(), is(nullValue()));
        assertThat(tag.getName(), is(nullValue()));
        assertThat(tag.getDescription(), is(nullValue()));
        verify(qaService, never()).getTagById(anyInt());
    }
    
    /**
     * Methods tend to follow the pattern of..
     * 
     *      - set up of objects, mocks etc
     *      - invocation of method being tested
     *      - assertion & verification
     *      
     * separated by spaces.
     */
    @Test
    public void populatedTagObjectIsReturnedWhenIdIsSupplied() {
        Tag tag = new Tag();
        when(qaService.getTagById(123)).thenReturn(tag);
         
        Tag returnedTag = tagForm.setUpForm(123);
         
        assertThat(returnedTag, sameInstance(tag));
    }
    
    @Test
    public void tagListViewIsDisplayedAfterSuccessfulSave() {
        Tag tag = new Tag();
        SessionStatus sessionStatus = new SimpleSessionStatus();
        when(bindingResult.hasErrors()).thenReturn(false);
        
        String viewName = tagForm.save(tag, bindingResult, sessionStatus);
        
        assertThat(viewName, equalTo("redirect:tag-list"));
        verify(qaService).saveTag(tag, bindingResult);
        assertThat(sessionStatus.isComplete(), equalTo(true));
    }

    @Test
    public void tagFormIsDisplayedAfterFailedSave() {
        Tag tag = new Tag();
        SessionStatus sessionStatus = new SimpleSessionStatus();
        when(bindingResult.hasErrors()).thenReturn(true);
        
        String viewName = tagForm.save(tag, bindingResult, sessionStatus);
        
        assertThat(viewName, equalTo("tag-form"));
        verify(qaService).saveTag(tag, bindingResult);
        assertThat(sessionStatus.isComplete(), equalTo(false));
    }
    
    @Test
    public void tagListViewIsDisplayedAfterSuccessfulDelete() {
        Tag tag = new Tag();
        SessionStatus sessionStatus = new SimpleSessionStatus();
        when(bindingResult.hasErrors()).thenReturn(false);
        
        String viewName = tagForm.delete(tag, bindingResult, sessionStatus);
        
        assertThat(viewName, equalTo("redirect:tag-list"));
        verify(qaService).deleteTag(tag, bindingResult);
        assertThat(sessionStatus.isComplete(), equalTo(true));
    }

    @Test
    public void tagFormIsDisplayedAfterFailedDelete() {
        Tag tag = new Tag();
        SessionStatus sessionStatus = new SimpleSessionStatus();
        when(bindingResult.hasErrors()).thenReturn(true);
        
        String viewName = tagForm.delete(tag, bindingResult, sessionStatus);
        
        assertThat(viewName, equalTo("tag-form"));
        verify(qaService).deleteTag(tag, bindingResult);
        assertThat(sessionStatus.isComplete(), equalTo(false));
    }

}

TagPropertyEditorTest.java
package com.lishman.qa.web;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.when;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import com.lishman.qa.domain.Tag;
import com.lishman.qa.service.QaService;

@RunWith(MockitoJUnitRunner.class)
public class TagPropertyEditorTest {

    @Mock private QaService qaService;
    
    @Test
    public void tagNameIsConvertedToTag() {  
        Tag tag = new Tag("xyz", "xyz description");
        when(qaService.getTagByName(anyString())).thenReturn(tag);
        TagPropertyEditor propEd = new TagPropertyEditor(qaService);        
        
        propEd.setAsText("tagname");
        Tag returnedTag = (Tag) propEd.getValue();
        
        assertThat(returnedTag, equalTo(tag));
    }

}

TestExt.java
package com.lishman.qa;


/**
 * A set of utility methods to help testing with JUnit.
 * Intended to be extended by JUnit tests.
 * 
 * @author lishy
 *
 */

public class TestExt {
    
    /* There are basically four ways to deal with private methods i unit tests
     * 
     *  - Don't test private methods, test the non private methods that use them.
     *  - Give the methods package access.
     *  - Use a nested test class.
     *  - Use reflection.
     * 
     * This class uses reflection.
     * 
     * Reference: http://www.artima.com/suiterunner/private.html
     */

    /**
     * Get the value of a private field.
     * 
     * @param <T> the type of the object to be returned.
     * @param obj the object to get the private field value from.
     * @param fieldName the name of the private field containing the value.
     * @return the value of the private field.
     */
    public <T> T getPrivateFieldValue(Object obj, String fieldName) {
        return TestUtils.getPrivateFieldValue(obj, fieldName);
    }
    
    /**
     * Set the value of a private field.
     * 
     * @param obj the object containing the private field.
     * @param fieldName the name of the private field.
     * @param val the value to be injected.
     */
    public void setPrivateFieldValue(Object obj, String fieldName, Object val) {
        TestUtils.setPrivateFieldValue(obj, fieldName, val);
    }

}

TestUtils.java
package com.lishman.qa;


import java.lang.reflect.Field;

/**
 * A set of useful test utilities.
 * 
 * @author lishy
 *
 */

public class TestUtils {
    
    // TODO can we use the Spring ReflectionTestUtils for this?

    /**
     * Get the value of a private field.
     * 
     * The return type will be inferred from the caller. 
     * However, note the @SuppressWarnings annotation: that tells you that this 
     * code isn't typesafe. You have to verify it yourself, or you could get 
     * ClassCastExceptions at runtime.
     * 
     * @param <T> the type of the object to be returned.
     * @param obj the object to get the private field value from.
     * @param fieldName the name of the private field containing the value.
     * @return the value of the private field.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getPrivateFieldValue(Object obj, String fieldName) {
        try {
            Field field = obj.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            return (T) field.get(obj);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * Set the value of a private field.
     * 
     * @param obj the object containing the private field.
     * @param fieldName the name of the private field.
     * @param val the value to be injected.
     */
    public static void setPrivateFieldValue(Object obj, String fieldName, Object val) {
        try {
            Field field = obj.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(obj, val);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

base.css
/* General */

body {
  font-family: Geneva, Arial, Helvetica, sans-serif;
}

img {
  border-style: none;
}

/* Links */
a {
  font-weight:      normal;
  text-decoration:  none;
}

a:hover {
  font-weight:      normal;
  text-decoration:  none;
}

/* Button */
.button {
  margin-left:       235px;
  padding:           10px;
  border:            1px solid DarkGray;
  text-decoration:   none;
  border-radius:     5px 5px 5px 5px;
  cursor:            pointer;
}

/* Headers */
h1, h2 {
  font-weight:  normal;
  margin:       0;
  padding:      0;
}

h1 {
  font-size:      20px;
  padding-top:    10px;
  padding-bottom: 10px;
}

h2 {
  font-size:      18px;
  padding-top:    10px;
  padding-bottom: 10px;
}

/* Contents */

#contents {
  margin-left:  20;
}

/* Table */
table {
  width:  100%;
}

table {
  border-collapse:  collapse;
  margin:           2px 0 10px 0;
}

table th {
  text-align: left;
}

table th, table td {
  border:   1px Silver solid;
  padding:  5px;
}

table#tag {
  width: 400px; 
}

table#question {
  width: 800px; 
}

/* Horizontal rule */
hr {
  height: 1px;
  border-style: none;
  background-color: lightgray;
}

/* Questions & Answers */
.question-box, .answer-box {
  height:         150px; 
  width:          600px;
  margin:         0px;
  padding:        5px;
  border:         1px LightGray solid;
  
}

.reveal-area {
  margin:    10px 0 10px 0;
  padding:   10px;
}

.footer {
  margin-top:     20px;
}

/* Error */

.error {
  color: Red;
}
plain.css
@import url("base.css");

/* General */
body {
  color:       Black;
}

/* Links */
a {
  color:            Blue;
  text-decoration:  underline;
}

a > button {
  text-decoration:  none;
}

a:hover {
  color:            Blue;
  text-decoration:  underline;
}

/* Headers */
h1, h2 {
  color:        Black;
}

/* Table */
table {
  background:   White;
}

table th {
  color:        Black;
  background:   White;
}
silver.css
@import url("base.css");

/* General */
body {
  color:       DimGray;
}

/* Links */
a {
  color:       DarkSlateGray;
}

a:hover {
  color:       OrangeRed;
}

/* Headers */
h1, h2 {
  color:        SeaGreen;
}

/* Table */
table {
  background:   WhiteSmoke;
}

table th {
  color:        DarkSlateGray;
  background:   Gainsboro;
}
edit.gif
quest.js
$(document).ready(function(){
	
	$(".answer").hide();
	
	$('#reveal').click(function(){
		$(".answer").show(500);
	});
	
	$("#reveal").hover(
	  function () {
	    $(this).css("background", "#F0F0F0");
	  }, 
	  function () {
		$(this).css("background", "White");
	  }
	);

	
});
ask-question.jsp
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

<%@ pageContext.setAttribute("linefeed", "\n"); %>

<html>
 
  <head>
    <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css">
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js"></script>    
    <script type="text/javascript" src="<c:url value='/js/quest.js' />"></script>
    <title><spring:message code="test.take"></title>
  </head>

  <body>
  
    <div id="contents">
   
      <h2><spring:message code="tag.plural">:
        <c:forEach items="${question.tags}" var="tag" varStatus="status">
          ${tag.name}${status.last ? ' ' : ','}
        </c:forEach>
      </h2>

      <div id="question" class="question-box">
        ${fn:replace(question.question, linefeed, "<br/>")}
      </div>
      
      <div class="reveal-area">
        <span id="reveal" class="button"><spring:message code="question.revealAnswer"></span>
      </div>

      <div class="answer-box">
        <div id="answer" class="answer">
          ${fn:replace(question.answer, linefeed, "<br/>")}
        </div>
      </div>
      
      <div class="footer">
        <a id="menu" style="padding-right:10px" href="<c:url value='/app/main-menu.html'/>">
          « <spring:message code="navigation.menu.main">
        </a>|
        <a id="next-quest" style="padding-left:10px" href="<c:url value='/app/ask-question.html'/>">
          <spring:message code="test.nextQuestion"> »
        </a>
      </div>
    
  </div>

  </body>
</html>

main-menu.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>

<html>
  <head>
    <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css">
    <title><spring:message code="application.name"></title>
  </head>
  
  <body>
    <h1><spring:message code="application.name"></h1>

    <div id="contents" class="menu">
      <p>
        <a id="take-a-test" href="<c:url value='/app/ask-question.html' />"><spring:message code="test.take"></a>
      </p>    
      <p>
        <a id="quest-maint" href="<c:url value='/app/question-list.html' />"><spring:message code="question.maintain"></a>
      </p>
      <p>
        <a id="tag-maint" href="<c:url value='/app/tag-list.html' />"><spring:message code="tag.maintain"></a>     
      </p>
    </div>  
    
    <div style="font-size: 80%; margin-top: 40px" class="footer">
      <hr/>
      <p> 
        <a id="theme-plain" href="?theme-name=plain"><spring:message code="theme.plain"></a> | 
        <a id="theme-silver" href="?theme-name=silver"><spring:message code="theme.silver"></a>
      </p>
      <p>
        <a id="locale-en" href="?locale=en">en</a> | 
        <a id="locale-fr" href="?locale=fr">fr</a> | 
        <a id="locale-de" href="?locale=de">de</a>
      </p>
    </div>

  </body>
</html>
question-details.jsp
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

<%@ pageContext.setAttribute("linefeed", "\n"); %>

<html>

  <head>
    <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css">
    <title><spring:message code="question.details"></title>
  </head>

  <body>     
  
    <div id="contents">

      <h1><spring:message code="question.details"></h1>
    
      <div id="question" class="question-box">${fn:replace(question.question, linefeed, "<br/>")}</div>
      
      <br/>
      <h1><spring:message code="answer.label"></h1>

      <div class="answer-box">
        <div id="answer" class="answer">${fn:replace(question.answer, linefeed, "<br/>")}</div>
      </div>

      <br/>
      <h2 id="tags"><spring:message code="tag.plural">:
        <c:forEach items="${question.tags}" var="tag" varStatus="status">
          ${tag.name}${status.last ? ' ' : ','}
        </c:forEach>
      </h2>

    </div>

    <div class="footer">
      <a id="go-back" href="<c:url value='/app/question-list.html' />">
        « <spring:message code="navigation.back">
      </a>
    </div>
    
  </body>
</html>

question-form.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>

  <head>
    <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css">
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js"></script>
    <script type="text/javascript">
      $(function () {
        $('#delete').click(function () {
            
            return confirm("<spring:message code='button.delete'/> <spring:message code='question.label'/>?");
            
          });
      });
    </script>
    <title><spring:message code="question.maintain"></title>
  </head>
  
  <body>
    <h1><spring:message code="question.maintain"></h1>
    
    <div id="contents">
    
      <form:form id="question-form" modelAttribute="question" action="question-form" method="post">
      
        <c:choose>
          <c:when test="${question.new}">
            <button id="create" type="submit" name="save">
              <spring:message code="button.create">
            </button>
          </c:when>
          <c:otherwise>
            <button id="save" type="submit" name="save">
              <spring:message code="button.update">
            </button>
            <button id="delete" type="submit" name="delete" onclick="return confirm('<spring:message code='button.delete'/> 
                                             <spring:message code='question.label'/>?')">
              <spring:message code="button.delete">
            </button>
          </c:otherwise>
        </c:choose>
        
        <p>
          <spring:message code="question.label"><br>
          <form:textarea path="question" rows="5" cols="50"><br/>
          <form:errors path="question" cssClass="error">
        </p>

        <p>
          <spring:message code="answer.label"><br>
          <form:textarea path="answer" rows="5" cols="50"><br/>
          <form:errors path="answer" cssClass="error">
        </p>
        
        <p>
          <spring:message code="tag.plural"><br>
          <form:checkboxes path="tags" items="${tagList}" delimiter="<br/>">
        </p>

      </form:form>
      
    </div>
    
    <div class="footer">
      <a id="cancel" href="<c:url value='/app/question-list.html' />">
        « <spring:message code="navigation.cancel">
      </a>
    </div>
     
  </body>
</html>

question-list.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>

<html>

  <head>
    <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css">
    <title><spring:message code="question.plural"></title>
  </head>
  
  <body>
    <h1><spring:message code="question.plural"></h1>
    
    <div id="contents">
    
      <a href="<c:url value='/app/question-form.html' />">
        <button id="create"><spring:message code="button.create"></button>
      </a>

      <table id="question">
        <tr>
          <th style="width: 20px"> </th>
          <th><spring:message code="question.label"></th>
          <th><spring:message code="answer.label"></th>
        </tr>
        <c:forEach items="${questions}" var="question">
          <tr>
            <td>
              <a href="<c:url value='/app/question-form.html?id=${question.id}'/>">
                <img src="<c:url value='/images/edit.gif' />">
              </a>
            </td>
            <td>
              <a href="<c:url value='/app/question-details/${question.id}'/>">
                ${question.question}
              </a>
            </td>
            <td>           
              ${question.answer}
            </td>          
          </tr>
        </c:forEach>
      </table>
      
    </div>

    <div class="footer">    
      <a id="go-back" href="<c:url value='/app/main-menu.html' />">
        « <spring:message code="navigation.back">
      </a> 
    </div>
    
  </body>
</html>

tag-details.jsp
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<html>

  <head>
    <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css">
    <title><spring:message code="tag.details"></title>
  </head>

  <body>
    <h1><spring:message code="tag.details"></h1>
    
    <div id="contents">
    
      <table id="tag">
        <tr>
          <td><spring:message code="tag.field.name"></td>
          <td><strong>${tag.name}</strong></td>
        </tr>
        <tr>
          <td><spring:message code="tag.field.description"></td>
          <td><strong>${tag.description}</strong></td>
        </tr>
      </table>

    </div>
    
    <div class="footer">
      <a id="go-back" href="<c:url value='/app/tag-list.html' />">
        « <spring:message code="navigation.back">
      </a>
    </div>
    
  </body>
</html>

tag-form.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>

  <head>
    <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css">
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js"></script>
    <script type="text/javascript">
      $(function () {
        $('#delete').click(function () {
            
            return confirm("<spring:message code='button.delete'/> <spring:message code='tag.label'/>?");
            
          });
      });
    </script>
    
    <title><spring:message code="tag.maintain"></title>
  </head>
  
  <body>
    <h1><spring:message code="tag.maintain"></h1>
    
    <div id="contents" class="detail">
    
      <form:form modelAttribute="tag" action="tag-form" method="post">
      
        <c:choose>
          <c:when test="${tag.new}">
            <button id="create" type="submit" name="save">
              <spring:message code="button.create">
            </button>
          </c:when>
          <c:otherwise>
            <button id="save" type="submit" name="save">
              <spring:message code="button.update">
            </button>

            <!-- The confirmation popup does not work -->
            <button id="delete" type="submit" name="delete" onclick="return confirm('<spring:message code='button.delete'/> 
                                             <spring:message code='tag.label'/>?')">
              <spring:message code="button.delete">
            </button>
          </c:otherwise>
        </c:choose>
        
        <form:errors cssClass="error" element="p">
        
        <p>
          <spring:message code="tag.field.name"><br>
          <form:input path="name"><br/>
          <form:errors path="name" cssClass="error">
        </p>

        <p>
          <spring:message code="tag.field.description"><br>
          <form:textarea path="description" cols="50" rows="5"><br/>
          <form:errors path="description" cssClass="error">
        </p>

      </form:form>
      
    </div>

    <div class="footer">    
      <a id="cancel" href="<c:url value='/app/tag-list.html' />">
        « <spring:message code="navigation.cancel">
      </a>
    </div>
    
  </body>
</html>

tag-list.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>

<html>

  <head>
    <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css">
    <title><spring:message code="tag.plural"></title>
  </head>
  
  <body>
    <h1><spring:message code="tag.plural"></h1>
    
    <div id="contents">
    
      <a href="<c:url value='/app/tag-form.html' />">
        <button id="create"><spring:message code="button.create"></button>
      </a>

      <table id="tag">
        <tr>
          <th style="width: 20px"> </th>
          <th><spring:message code="tag.field.name"></th>
          <th><spring:message code="tag.field.description"></th>
        </tr>
        <c:forEach items="${tags}" var="tag">
          <tr>
            <td>
              <a href="<c:url value='/app/tag-form.html?id=${tag.id}'/>">
                <img src="<c:url value='/images/edit.gif' />">
              </a>
            </td>
            <td>
              <a id="back" href="<c:url value='/app/tag-details/${tag.id}'/>">
                ${tag.name}
              </a>
            </td>
            <td>           
              ${tag.description}
            </td>          
          </tr>
        </c:forEach>
      </table>
      
    </div>
    
    <div class="footer">
      <a id="go-back" href="<c:url value='/app/main-menu.html' />">
        « <spring:message code="navigation.back">
      </a> 
    </div>
    
  </body>
</html>

app-config-with-stubs.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

  <import resource="classpath:mvc-config.xml"/>
  <import resource="classpath:service-config-with-stubs.xml"/>
  <import resource="classpath:domain-config.xml"/>

</beans>
app-config.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

  <!-- util:properties ??? -->
  <context:property-placeholder location="classpath:data/jdbc.properties"/>

  <import resource="classpath:mvc-config.xml"/>
  <import resource="classpath:service-config.xml"/>
  <import resource="classpath:data/datasource-config.xml"/>
  <import resource="classpath:data/jpa-config.xml"/>
  <import resource="classpath:domain-config.xml"/>

</beans>
web.xml
<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         version="2.4"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

  <display-name>Quest</display-name>

  <context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>/WEB-INF/classes/log4j.properties</param-value>
  </context-param>
  
  <context-param>
    <param-name>log4jRefreshInterval</param-name>
    <param-value>10000</param-value>
  </context-param>
  
  <listener>
    <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
  </listener>
  
  <context-param>
    <param-name>webAppRootKey</param-name>
    <param-value>qa</param-value>
  </context-param> 

  <servlet>
    <servlet-name>quest</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>
        /WEB-INF/app-config-with-stubs.xml
      </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>quest</servlet-name>
    <url-pattern>/app/*</url-pattern>
  </servlet-mapping>

  <welcome-file-list>
    <welcome-file>app/</welcome-file>
  </welcome-file-list>

</web-app>
build.xml
<?xml version="1.0" encoding="UTF-8"?>

<project name="make-war" basedir="." default="create-war">
   
    <property name="deploy.dir" value="deploy"/>
    <property name="web.dir" value="webapp"/>
    <property name="web-inf.dir" value="${web.dir}/WEB-INF"/>
    <property name="warfile" value="qa1"/>
    <property name="lib.dir" value="${web-inf.dir}/lib"/>
	
    <target name="clean">
      <delete dir="${deploy.dir}"/>
    </target>
	
    <path id="classpath">
      <fileset dir="${lib.dir}" includes="**/*.jar"/>
      <fileset dir="${jee.dir}" includes="**/*.jar"/> <!-- jee.dir passed in as property -->
    </path>

    <target name="compile" depends="clean">
      <mkdir dir="${deploy.dir}/classes"/>
      <javac srcdir="src" destdir="${deploy.dir}/classes" classpathref="classpath"/>
    </target>
	
    <target name="config">
    	
      <copy file="config/data/jdbc-live.properties"
          tofile="${deploy.dir}/classes/data/jdbc.properties"/>
    	
      <copy todir="${deploy.dir}/classes">
        <fileset dir="config">
        	 <exclude name="**/jdbc*"/>
        </fileset>
      </copy> 
    	 	
      <copy todir="${deploy.dir}/classes">
        <fileset dir="src">
        	<include name="META-INF/*"/>
        </fileset>
      </copy>    
    	
    </target>

    <target name="create-war" depends="compile, config">
      <war destfile="${deploy.dir}/${warfile}.war" webxml="${web-inf.dir}/web.xml">
        <classes dir="${deploy.dir}/classes"/>
          <fileset dir="${web.dir}">
            <exclude name="WEB-INF/web.xml"/>
          </fileset>
      </war>
    </target>
   
</project>

Validation

Split the validation and put it in the most appropriate place. Duplicate if necessary.

For exam