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
-
6. Architektur
Ein kurzer Überblick über die Architektur der Webanwendung totask2.
6.1. Datenmodell
Das Datenmodell der Projektzeiterfassung.
6.2. Code Modell
Überblick über die internen Module von totask2.
Schematischer Ablauf eines Requests (Anfrage einer Seite durch den Browser):
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.
/** 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.
-->
<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:
im 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:
$(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:
/** 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:
Der Htlm/JavaScript Code hierzu
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;
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.
@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:
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:
7.6. Selenium Test
Neben Unit Tests mit spring-tests ist auch ein reiner Blackbox test mit Selenium vorbereitet:
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 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:
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.
== Ü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:
Und eigener Verwaltungstabellen erkennt Flyway den Stand der Datenbank:
: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.
\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.
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"
..