1. Überblick

Das Dokument beschreibt aus Entwicklersicht das Aufsetzen und Weiterentwickeln der Anwendung totask2. Die Anwendung ist eigentlich nur eine Entschuldigung diverse Bibliotheken und Tools einzusetzen um die Ideen dahinter näher beurteilen zu können.

2. Notwendige Umgebung

Vorbedingung hierzu:

  • Java JDK 8 (oracle) installieren

  • Gradle download als Build System www.gradle.org

  • git installieren zur Versionierung git-scm.com

zudem eine IDE (z.B akuelles eclipse) eclipse.org

3. Download

Der komplette Quellcode des Projekts lässt sich mit "git clone" aus dem Master Source Repository von github holen.

%> git clone https://github.com/man-at-home/totask2.git

4. Build & Run

Compile und Test des Projekts erfolgt mit ⇒ %> gradlew test.

Dokumentation erstellen (einschließlich diesem Dokument hier) ⇒ %> gradlew allDocs.

Run mit InMemory Datenbank h2 ⇒ %> gradlew bootRun

Run mit persistenter Datenbank (die Daten bleiben zwischen den Runs erhalten) ⇒ %> gradlew bootRun -Dspring.profiles.active=qa

QA Analyse incl. Tests und CodeCoverage: %> gradlew sonarRunners (parallel muss StartSonar.bat gestartet sein als localhost:9000/)

5. Deployment

5.1. Für lokalen App Server WildFly

  • gradle deployLocal

  • Wildfly starten mit standalone Script.

Hint: tomcat dependency muss entfernt sein im gradle build. Hint: newrelic Monitoring ist derzeit installiert (Agent wie auf der Homepage beschrieben für Wildlfy installiert).

5.2. Für OpenShift (cloud)

  • generell wird ganz normal .\build.gradle wird verwendet

  • hooks für OpenShift sind folgende shell Scripte:

    • .openshift\action_hooks\pre_build (cardridge vorbereiten für build)

    • .openshift\action_hooks\build (gradle assemble Aufruf)

    • .openshift\action_hooks\deploy (gradle deployOpenShift Aufruf)

  • update der Anwendung in der cloud mit git:

    • git push cloud

    • remote: ssh://5*4@h**e.rhcloud.com/~/git/host.git/

  • Zugriff/Monitoring

    • web: www.openshift.com

    • ssh: wie auf openShift Seite zur Anwendung angegeben

    • sftp: hostname.rhcloud.com

    • commandline: rhc Kommandos

screenshot.sftp
Figure 1. sftp to openShift gear
screenshot.sftp
Figure 2. rhc command line openShift

6. Architektur

Ein kurzer Überblick über die Architektur der Webanwendung totask2.

6.1. Datenmodell

Das Datenmodell der Projektzeiterfassung.

screenshot.erd
Figure 3. Datenmodell

6.2. Code Modell

Überblick über die internen Module von totask2.

screenshot.modules
Figure 4. Modulübersicht

Schematischer Ablauf eines Requests (Anfrage einer Seite durch den Browser):

screenshot.sequence
Figure 5. Sequenzdiagramm (Beispielhaft)

7. Code-Beispiele

Die Code Beispiele werden "on the fly" bei der Dokumentengenerierung hier in das Dokument übernommen, sollten also immer aktuell sein.

7.1. Data Model (JPA)

Die Datenzugriffsschicht mapped Java Domain Objekte mit JPA Annotations (insbl. @Entity) auf relationale Datenbanktabellen.

/**
 * a working task of a {@link Project} that needs to be worked on.
 *
 * @author man-at-home
 */
@Entity                                                     (1)
@Audited
@Table(name = "TT_TASK")
@AuditTable("TT_TASK_HISTORY")
public class Task {

    @Id                                                     (2)
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private long     id;

    @Size(min = 2, max = 250)
    @NotNull
    @Column(name = "NAME", nullable = false, length = 250)  (3)
    private String  name;
1 Task Klasse wird in die Tabelle tt_task gespeichert
2 Id Attribut entspricht in der Datenbank der Spalte id (primary key)
3 Name Attribut mapped zur Spalte name

7.2. Controller (SpringMVC)

Diese SpringMVC Controller Klasse nimmt Browseranfragen auf bestimmten Urls: (hier project/xx/tasks) entgehen und schickt Antworten an den Browser zurück.

Task.java
/** show all tasks for given project.
 *
 * @param id project.id to show tasks for.
 * */
@RequestMapping(value = "/project/{id}/tasks", method = RequestMethod.GET)          (1)
public String tasksForProject(@PathVariable final long id, final Model model) {

    LOG.trace("tasks for project " + id);

    Project project  = projectRepository.findOne(id);
    List<Task> tasks = taskRepository.findByProjectId(id);                          (2)

    model.addAttribute("tasks", tasks);                                             (3)
    model.addAttribute("projectId", id);
    model.addAttribute("isEditAllowed", project.isEditAllowed(getUser()));


    LOG.debug("serving " + tasks.size() + " tasks for project " + id);
    return "tasks";
}
1 Zuständig für Browser Anfragen (GET request) mit URL /project/xx/tasks
2 Laden der Tasks zum Projekt aus der Datenbankload
3 Bereitstellen der Tasks für Rendern der HTML Seite im View Template

7.3. View Templates (thymeleaf)

Markup um HTML Antwort (Übersicht von Tasks) bereitzustellen.

tasks.html
-->
<tr th:each="task : ${tasks}">    <!-- (1)
                                  -->
    <td th:text="${task.id}">17</td>
    <td th:text="${task.name}">task name</td>        <!-- (2)
                                                     -->
    <td class="dt-left">

        <a th:href="@{/task/{id}(id=${task.id})}" class="btn btn-default" th:if="${isEditAllowed}">
            <span class="glyphicon glyphicon-pencil"></span>
            <span th:text="#{totask2.task.action.edit}">edit..</span>            <!-- (3)
                                                                                  -->
        </a>
        <button class="btn btn-default" name="id" th:value="${task.id}" th:if="${isEditAllowed}">
            <span class="glyphicon glyphicon-remove"></span>
            <span th:text="#{totask2.task.action.delete}">delete</span>
        </button>

    </td>
</tr>
<!--
1 Geht über eine List von Tasks (List<Task>)
2 Zeige für einen Task das Attribut "from"
3 Konstanter i18n Text (aus der Datei message.properties)

7.4. Ajax / Javascript

7.4.1. autocomplete

Ajax Funktion bei der Auswahl von Usern (Suchfunktion) bei der Zuordnung von Tasks:

screenshot8
Figure 6. Zeiterfassung Ajax Autocompletion

im HTML:

editTaskAssignment.html (autocomplete jquery html)
                -->
		<input type="hidden" th:field="*{task.id}"/>
		<input type="hidden" class="userIdRef" th:field="*{user.id}"/>	<!-- (1)
		-->

		 <div class="form-group">
			<label th:for="user" th:text="#{totask2.taskAssignment.user.label}" class="col-sm-2 control-label">user</label>
			<div class="col-sm-10">
				<input
				    type="text"
				    id="user"
					class="form-control"
					placeholder="user"
					th:title="#{totask2.taskAssignment.user.help}"
					maxlength="250"
					th:field="*{user.displayName}"
					th:errorclass="fieldError"
					data-bv-notempty="true"
					data-bv-notempty-message="required field"
				/>																												<!-- (2)
				-->

				<span th:if="${#fields.hasErrors('user')}" class="fieldError" th:errors="*{user}">user is invalid</span>
			</div>
		</div>

		<div class="form-group">
			<label th:text="#{totask2.taskAssignment.user.name.label}" class="col-sm-2 control-label">user</label>
			<div class="col-sm-10">
				<input readonly="readonly" type="text" th:field="*{user.username}" class="readonly form-control userNameRef"/>	<!-- (3)
				-->
			</div>
		</div>

<!--
1 versteckte Referenz des ausgewählten Users, wird beim Speichern (POST) zum Server gesendet
2 das eigentliche autocomplete Control (jquery)
3 Detailanzeige gerade gewählter User

JavaScript Client Code für autocomplete Funktion:

editTaskAssignment.html (autocomplete jquery plugin)
$(function() {
       $( "#user" ).autocomplete({
         source: "/users",                                            (1)
         minLength: 2,
         select: function(event, ui) {
             if( ui.item ) {
                 // ( ) ? "hier: " + ui.item.username : "-" );
                 $(".userIdRef").val( ui.item.id );                (2)
                 $(".userNameRef").val( ui.item.username );
             }
         }
       });
     });
1 URL mit REST/JSON Datenquelle für User (konkret: Klasse UserController url /users)
2 JSON Daten der Rückmeldung in Anzeige übernehmen

Hier die REST Datenquelle in der UserController Klasse:

UserController.java
/** REST API. get all users the fit the given "term". */
@Secured(Authorisation.ROLE_ADMIN)
@ApiIgnore
@RequestMapping(value = "/users", method = RequestMethod.GET)
public List<User> getUsers(@RequestParam(value = "term", defaultValue = "") final String term) {
    LOG.debug("/users, term=" + term);
    List<User> foundUsers = userCachingService.getCachedUsers(term);
    gaugeService.submit("TOTASK2XX.controller.user.REST.ajaxui.result.count", foundUsers.size());
    return foundUsers;
}

7.4.2. chart

Auf der Erfassungsseite für Zeiten wird direkt bei der Eingabe clientseitig ein Chart der Stunden erzeugt:

screenshotChart
Figure 7. weekEntryChart

Der Htlm/JavaScript Code hierzu

weekEntry.html (chart data)
var dailyTotals = new Array(6);
var weekSum = 0;
for (i = 0; i <= 6; i++) {
    var daySum    = sumOneTaskRow('.day_' + i);
    weekSum      += daySum;
    dailyTotals[i]= daySum;

    $('#day_' + i + '_total').html(daySum.toFixed(1));
}
$('#week_total').html(weekSum.toFixed(1));

return dailyTotals;
weekEntry.html (chart ui)
var chartData = {
        labels: [ /*[[#{totask2.weekEntry.monday.label}]]*/    "Mon",
                  /*[[#{totask2.weekEntry.tuesday.label}]]*/   "Tue",
                  /*[[#{totask2.weekEntry.wednesday.label}]]*/ "Wed",
                  /*[[#{totask2.weekEntry.thursday.label}]]*/  "Thu",
                  /*[[#{totask2.weekEntry.friday.label}]]*/    "Fri",
                  /*[[#{totask2.weekEntry.saturday.label}]]*/  "Sat",
                  /*[[#{totask2.weekEntry.sunday.label}]]*/    "Sun"
                ],
        datasets: [
            {
                label: "weekly work",
                fillColor: "rgba(220,220,220,0.5)",
                strokeColor: "rgba(220,220,220,0.8)",
                highlightFill: "rgba(220,220,220,0.75)",
                highlightStroke: "rgba(220,220,220,1)",
                data: dailyTotals
            }
        ]
    };

var ctx = $("#weekEntryChart").get(0).getContext("2d");
weekChart = new Chart(ctx).Bar(
        chartData,
        {animationSteps: 20} /* options. */
        );

7.5. Versionierung von Daten

Einige Stammdaten der Anwendung sind versioniert (sprich: alte Stände werden aufgehoben, historisiert). Dazu verwende ich hibernate-envers.

Project.java
@Table(name = "TT_PROJECT")
@Audited                                    (1)
@AuditTable("TT_PROJECT_HISTORY")           (2)
@ApiModel(value = "Project", description = "project to log work on tasks, containing *task*s and being administered by project leads")
public class Project {

    private static final Logger LOG = LoggerFactory.getLogger(Project.class);

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private long     id;
1 Annotation um das Objekt zu versionieren.
2 Datenbanktabelle für historische Werte.

Auf alte Werte kann dann mit der envers API zugegriffen werden:

ProjectHistoryTest.java
LOG.debug("flash back query for project" + projectId);

AuditReader ar = AuditReaderFactory.get(entityManager);
AuditQuery query = ar.createQuery().forRevisionsOfEntity(
        Project.class, true, true);
query.add(new IdentifierEqAuditExpression(projectId, true));

@SuppressWarnings("unchecked")
List<Project> result = query.getResultList();

result.stream().forEach(
        p -> LOG.debug("historic project entry: " + p));

in der UI:

screenshot.projectHistory
Figure 8. ProjectHistory

7.6. Selenium Test

Neben Unit Tests mit spring-tests ist auch ein reiner Blackbox test mit Selenium vorbereitet:

testscript.html
Figure 9. totask2.test.LoginProjectsLogout.selenium.html
screnshot.ide
Figure 10. test-ide

7.7. plantUML Diagramm

Die hier gezeigten Diagramme sind mit plantUML erzeugt. Der Charm dabei:

a) auch das Diagramm ist in Textformat und kann daher gemerged/versioniert werden

b) es ist direkt im JavaDoc Kommentar enthalten und dementsprechend auch in der daraus generierten HTML Dokumentation.

Diagramm im Quellcode:

package-info.java (plantuml documenentation for JavaDoc)
package org.manathome.totask2.model;

/*
*
@startuml doc-files/totask2.design.datamodel.png

Project         "1" *-- "n" Task : contains
Project         "n"  -- "n" User            : leads
Task            "1" *-- "n" TaskAssignment
TaskAssignment  "1" -- "n"  User : assigned to
WorkEntry       "n" -- "1"  User
WorkEntry       "n" -- "1"  Task : worked on

Project   : name
Task      : name

User      : username
User      : displayName
User      : password
User      : isAdmin : boolean

WorkEntry  : at       : Date
WorkEntry  : duration : Hours

TaskAssignment : from  : Date
TaskAssignment : until : Date


@enduml
*/

aufbereitet wird daraus:

screenshot2.erd
Figure 11. Datenmodell2

7.8. asciidoctor Dokumentation

Die Dokumentation zum Projekt (auch dieses Dokument) ist in "asciidoc" erstellt. Eine Markupsprache spezifisch für einfache Dokumentenerstellung. Sie kann in diverse Formate aufbereitet werden (u.a. html, pdf, latex) ist gut les- und versionierbar.

task2.developer-manual.asciidoc (this documentation)
    == Überblick                                                           (1)

    Das Dokument beschreibt *aus Entwicklersicht* das...                   (2)
    ...
	include::{resourcedir}/templates/weekEnty.html[tags=developer-manual-chart]
1 Überschrift
2 Text mit teilweise (*) hervorgehobenen Bereichen

7.9. flyway Datenbank Migration

Die Datenbankstruktur wird im Entwicklungsbetrieb automatisch generiert, für die Test/Produktionsumgebung wird die Struktur aber mit Flyway verwaltet. Dieses kleine Tool verwaltet je Datenbank deren aktuellen Releasestand und führt bei Bedarf beim Start der Anwendung notwendige Migrationen am Datenmodell (Upgrade) durch.

Aus den Scripten:

flywayscripts.dir
Figure 12. flyway script directory

Und eigener Verwaltungstabellen erkennt Flyway den Stand der Datenbank:

console with flyway output/commands
	:testClasses
	:flywayInfo
	+----------------+----------------------------+---------------------+---------+
	| Version        | Description                | Installed on        | State   |
	+----------------+----------------------------+---------------------+---------+
	| 1              | init totask2 db            | 2014-10-26 02:17:37 | Success |    (1)
	| 2              | createTestUsersProject.db  | 2014-10-26 02:17:37 | Success |
	| 3              | alterTableUsers.db         | 2014-10-26 02:17:37 | Success |
	| 4              | alterTablesVarious.db      |                     | Pending |    (2)
	| 5              | createHistoryTables.db     |                     | Pending |
	+----------------+----------------------------+---------------------+---------+

	BUILD SUCCESSFUL

    >gradle flywayMigrate                                                              (3)
1 bereits installierte Datenbankscripts
2 noch fehlende Datenbankscripts
3 Kommandozeilenaufruf um Datenbank auf neuesten Stand zu bringen

7.10. gradle Build

gradle ist ein Groovy basierendes Build System, als Alternative zu den bekannteren Build-Tools maven oder ant zu empfehlen.

gradle.build.output
\data\projects\toTask2>gradle --daemon allDocs

	:asciidoctor
	Converting C:\data\projects\toTask2\src\docs\totask2.article.asciidoc
	Converting C:\data\projects\toTask2\src\docs\totask2.developer-manual.asciidoc
	Converting C:\data\projects\toTask2\src\docs\totask2.slides.asciidoc
	:compileJava UP-TO-DATE
	:processResources UP-TO-DATE
	:classes UP-TO-DATE
	:plantuml
	:javadoc UP-TO-DATE
	:copyPlantUMLImages UP-TO-DATE
	:allDocs

	BUILD SUCCESSFUL

	Total time: 31.077 secs
\data\projects\toTask2>

7.11. newRelic Monitoring

bei Start des augmented wildFly werden Monitoring Informationen an die newelic Webseite gesendet.

console with wildfly start output
C:\dev\wildfly\bin>standalone
	Calling "C:\dev\wildfly\bin\standalone.conf.bat"
	..
	Feb 21, 2015 21:33:43 +0100 [6592 1] com.newrelic INFO: Agent is using Logback
	Feb 21, 2015 21:33:43 +0100 [6592 1] com.newrelic INFO: Loading configuration file "C:\dev\wildfly\newrelic\.\newrelic.yml"
	..
screenshot.newrelic1
Figure 13. monitoringdaten newrelic