In diesem Beitrag möchte ich gerne das kleine Webprojekt „Timeline“ vorstellen. Mit diesem Projekt möchte ich erläutern wie man ein Webprojekt mit einer Datenbankanbindung und JavaServerFaces realisiert.
Ziel des Webprojektes
Ziel dieses Webprojektes soll es sein, Beiträge für eine Zeitlinie zu erstellen.
Ein solcher Eintrag enthält:
- Zeitstempel,
- Titel,
- Text,
- Bild,
- Video,
- Hyperlinks
- usw.
Einrichten des Webprojektes
Als erstes wollen wir das JSF Projekt einrichten, dazu habe ich bereits den Beitrag Erstellen eines JavaServerFaces 2.2 Projektes mit Eclipse & Apache Maven erstellt.
Anpassungen an der Projektstruktur
Anders als im zuvor genannten „HelloWorld“ Projekt möchte ich eine etwas andere Projektstruktur verwenden, dieses liegt viel mehr daran das Apache Maven eine bestimmte Struktur für Webprojekte bzw. Java Projekte verlangt. Im einfachen „HelloWorld“ Projekt war dieses nicht so kritisch da keine externen Ressourcen wie CSS, Bilder oder JavaScript Dateien benötigt wurden, jedoch nun wollen wir etwas tiefer in JSF eintauchen.
Das Projekt „Timeline“ gibt es hier zum Download.
Texte und Übersetzungen
Damit wir unsere Anwendung später in mehreren Sprachen pflegen können benötigen wir im Hintergrund ein java.util.ResourceBundle. Für jede Sprache wird ein eigenes ResourceBundle im Format <Anwendungsbezeichnung>_<language_code> angelegt. Eine Liste von Language Codes findest du an der Klasse java.util.Locale bzw. über die ISO 639 (jedoch haben sich dort einige Codes geändert).
ResourceBundle Editor
Zum leichteren bearbeiten von mehreren ResourceBundle Dateien (*.properties) nutze ich ein zusätzliches Plugin.
Dieses Plugin kann über den Marketplace installiert werden. Den Marketplace findest du unter dem Menüpfad „Help“ > „Eclipse Marketplace…“.
Als erstes muss nach einem Begriff eingegrenzt werden ich nutze dazu das Wort „resource“ (1), danach wird auch schon das Plugin angezeigt, nun kann dieses über die Schaltfläche „Install“ (2)installiert werden.
Nachdem das Plugin erfolgreich installiert wurde, kann die Propertiesdatei über das Kontextmenü mit dem Plugin geöffnet werden.
Anlegen der Dateien
Wollen wir nun 3 Propertiedateien im Ordner „src“ > „main“ > „resources“ anlegen. Für eine bessere Projektorganisation packe ich diese beiden Dateien in ein Unterverzeichniss „i18n“.
Einmal für die deutschen und einmal für die englischen Texte sowie einmal eine Datei (ohne Language Code) für den Fallback, falls der Browser keine Sprache liefert bzw. wenn eine Sprache geliefert wird, welche noch nicht unterstützt wird.
Einbinden in der faces-config.xml
Als Nächstes müssen wir nun in der Datei „faces-config.xml“ diese Dateien „einbinden“. Dazu müssen wir den Pfad zu den Resources definieren und eine Bezeichnung „var“ für diese vergeben.
<application> <resource-bundle> <base-name>i18n.timeline</base-name> <var>text</var> </resource-bundle> </application>
Verwenden der ResourceBundle in XHTML Dateien
Wir haben nun das ResourceBundle in der Datei „faces-config.xml“ definiert und den Variablennamen „text“ definiert.
Um auf die Werte in der Propertiesdatei zugreifen zu können gibt es zwei Möglichkeiten:
#{text.test} <br/> #{text['test']}
Formular zum eintragen von Datensätze
Wollen wir im nächsten Schritt ein Formular erzeugen in welchem wir einen ersten einfachen Datensatz (Zeitstempel, Titel und Text) erzeugen wollen.
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:c="http://java.sun.com/jsp/jstl/core" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html"> <h:head> <meta charset="ISO-8859-1"></meta> <h:outputStylesheet library="css" name="styles.css" /> <title>Timeline by Draeger-IT.blog</title> </h:head> <h:body> <fieldset class="inputFieldset"> <legend>#{text.eingabe}</legend> <h:form> <label for="titel">#{text.titel} </label> <h:inputText id="titel"></h:inputText> <br /> <br /> <h:inputTextarea id="text"></h:inputTextarea> <br /> <br /> <h:commandButton styleClass="btn" value="#{text.zuruecksetzen}" ></h:commandButton> <h:commandButton value="#{text.speichern}"></h:commandButton> </h:form> </fieldset> </h:body> </html>
Damit wir diese Daten speichern können, benötigen wir eine Bean und zusätzlich ein Objekt, in welchem wir diese Daten speichern und abrufen können.
Aufbau einer Java Bean
Bevor wir unsere erste Java Bean erzeugen, wollen wir auf den Aufbau und die Lebensdauer einer Java Bean eingehen.
Eine Java Bean (im nachfolgenden nur noch als Bean bezeichnet) wird mit der Annotation @ManagedBean über dem Definieren des Klassennamens eingeleitet.
@ManagedBean public class TimelineFormular {...}
Lebensdauer & Reichweite einer Bean
Des Weiteren muss noch der Scope angegeben werden, in welchem sich die Bean befindet. Der Scope definiert zeitgleich die Lebensdauer und Reichweite der Bean.
@ApplicationScoped
Diese Bean lebt so lange wie die Anwendung gestartet ist und alle Daten in dieser Bean stehen allen Benutzern zur Verfügung. Wenn sich Daten in dieser Bean ändern, werden alle Benutzer davon betroffen sein. Daher sollte so eine Bean immer mit bedacht angelegt werden.
@SessionScoped
Diese Bean wird für den Benutzer beim ersten Aufruf einer Seite / Aktion angelegt und existiert so lange wie die Session des Benutzers nicht abgelaufen ist. Die Dauer der Session ist so lange diese nicht in der Datei „web.xml“ angepasst wurde 30min.
Möchte man diesen Wert ändern so kann man in der Datei „web.xml“ mit folgenden 3 Zeilen den Timeout ändern.
<session-config> <session-timeout>10</session-timeout> </session-config>
Der Sessiontimeout wird bei jeder Kommunikation mit dem Server (Seitenwechsel, Speichern, Aktualisieren) zurückgesetzt.
@RequestScoped
Eine Bean im RequestScope existiert nur solange bis eine neue Seite aufgerufen oder eine Aktion (wie Speichern, Aktualisieren) ausgeführt wird.
Erzeugen der Bean
Erzeugen wir also nun eine Bean im SessionScope mit den Feldern „title“ & „text“. Des Weiteren benötigen wir eine Methode zum Speichern dieser Daten.
package de.draegerit.timeline; import javax.faces.bean.ManagedBean; import javax.faces.bean.SessionScoped; @ManagedBean @SessionScoped public class TimelineFormular { private String title; private String text; public void save() { } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getText() { return text; } public void setText(String text) { this.text = text; } }
Value Binding
Im nächsten Schritt muss nun eine Verbindung von den Eingabefeldern aus der XHTML Datei zur Java Bean hergestellt werden. Dieses nennt man Value Binding.
<h:inputText id="titel" value="#{timelineFormular.title}"></h:inputText>
Der Bezeichner „timelineFormular“ ist unser Klassenname der Bean und „title“ ist ein Feld aus genau dieser Klasse.
Wichtig ist das zu den Feldern welche man benötigt / verwenden möchte getter & setter nach der Java Beans Konvention existieren. Am einfachsten ist es hier sich diese von der IDE erzeugen zu lassen.
Actions
Zum Speichern und ablegen der Daten benötigen wir an den CommandButtons den Aufruf einer Aktion auf dem Server, dieses Attribut nennt sich „action“.
<h:commandButton value="#{text.speichern}" action="#{timelineFormular.save}"></h:commandButton>
Die aufrufende Methode in der Bean wird ohne zusätzliche Parameter erstellt.
private TimelineStorage storage = new TimelineStorage(); public void save() { TimelineEntry entry = new TimelineEntry(); entry.setText(text); entry.setTitle(title); entry.setTimestamp(System.currentTimeMillis()); getStorage().store(entry); }
Im ersten Schritt dieses Webprojektes speichern wir die Daten in einer java.util.List im flüchtigen Speicher, d.h. wenn der Applicationserver heruntergefahren, neu gestartet wird oder der Browser vom Benutzer geschlossen wird, werden alle Daten gelöscht. Damit wir die Daten in einer Liste ablegen können, benötigen wir eine Klasse. Als ersten Schritt wollen wir den Titel, den Text sowie einen Zeitstempel speichern.
package de.draegerit.timeline.entity; public class TimelineEntry { private long timestamp; private String title; private String text; public long getTimestamp() { return timestamp; } public void setTimestamp(long timestamp) { this.timestamp = timestamp; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getText() { return text; } public void setText(String text) { this.text = text; } }
Speichern der Daten
java.util.List
Das einfachste ist die Daten in einer java.util.List zu speichern. Dazu nutze ich eine Klasse welche diese Liste beinhaltet.
package de.draegerit.timeline; import java.util.ArrayList; import java.util.List; import de.draegerit.timeline.entity.TimelineEntry; public class TimelineStorage { private List<TimelineEntry> entries = new ArrayList<>(); public void store(TimelineEntry entry) { getEntries().add(entry); } public List<TimelineEntry> getEntries() { return entries; } public void setEntries(List<TimelineEntry> entries) { this.entries = entries } }
Der Vorteil daran ist das bei einer späteren Erweiterung (Bsp. zu einer Datenbank) nicht viel geändert werden muss.
Ausgeben der gespeicherten Daten
Nachdem wir die Daten aufgenommen haben, wollen wir diese auch wieder ausgeben.
Hier nutze ich die „forEach“ Schleife:
<c:forEach items="#{timelineFormular.storage.entries}" var="entrie"> <div class="timelineEntry"> <h4>#{entrie.title}</h4> <br /> #{entrie.text} <br /> <h:outputText value="#{entrie.timestamp}" styleClass="timestamp"> <f:convertDateTime pattern="dd.MM.yyyy HH:mm:ss" /> </h:outputText> </div> </c:forEach>
Den gespeicherten Zeitstempel formatiere ich in mithilfe von „convertDateTime“ in das deutsche Format „01.12.20018 12:58:34“.
Das Pattern kann man fast beliebig aufbauen, eine Liste mit den gültigen Patternzeichen findest du unter Oracle Java API – SimpleDateFormat.
Wie man erkennen kann wird der Zeilenumbruch nicht angezeigt. Dieses liegt vielmehr daran das ein Zeilenumbruch in einem HTML Dokument mit „<br/>“ dargestellt wird.
Wir müssen nun also alle „\n“ (Sonderzeichen für einen Zeilenumbruch) durch ein „<br/>“ ersetzen.
Das ersetzen kann man „einfach“ lösen, indem man den getter am Text wie folgt modifiziert:
public String getText() { return text.replaceAll("\n", "<br/>"); }
Der Code ersetzt alle Zeilenumbrüche durch einen HTML Zeilenumbruch. Dieses ist jedoch nur eine „Quick and Dirty Lösung“.
TextConverter
Es gibt in JSF verschiedene Konverter, da es für unseren Fall keinen passenden Konverter gibt, schreiben wir uns diesen kurzerhand selbst.
Dazu erzeuge ich eine Klasse „TextConverter“ und implementiere das Interface javax.faces.convert.Converter.
package de.draegerit.timeline.util; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.convert.Converter; import javax.faces.convert.FacesConverter; @FacesConverter("de.draegerit.timeline.util.TextConverter") public class TextConverter implements Converter { @Override public Object getAsObject(FacesContext context, UIComponent component, String value) { return replaceLineBreaks(value); } @Override public String getAsString(FacesContext context, UIComponent component, Object value) { return replaceLineBreaks(value); } private String replaceLineBreaks(Object value) { return value.toString().replaceAll("\n", "<br/>"); } }
Die Methode „replaceLineBreaks“ hat als Parameter den Wert als Object. Auf dieses Objekt wird die Methode „toString()“ aufgeführt, diese Methode liefert dann einen String. Wenn das Objekt ein String ist (wie in unserem Fall) dann wird der Wert dahinter geliefert, ansonsten muss vorher einen Cast auf ein Datentyp erfolgen.
Verwenden des TextConverters
Nachdem wir den TextConverter erzeugt haben, können wir diesen wie folgt in unserem JSF Tag „outputText“ verwenden:
<h:outputText value="#{entrie.text}" escape="false"> <f:converter converterId = "de.draegerit.timeline.util.TextConverter" /> </h:outputText>
Der Konverter muss dabei zwischen dem geöffneten sowie dem geschlossenen Tag „h:outputText“ befinden. Zusätzlich muss man noch das Attribut „escape“ auf „false“ setzen, da sonst der HTML LineBreak umgewandelt und dann als Text angezeigt wird.
Das Ergebnis bleibt gleich jedoch haben wir nun einen TextConverter welcher an mehreren Stellen verwendet werden kann und wir unsere DAO’s nicht anpassen müssen.
Upload von Bilder
Nachdem wir nun mit den ersten Schritten zu dem Webprojekt fertig sind, können wir einen einfachen Beitrag erstellen und nachdem speichern darstellen.
Als Nächstes wollen wir nun ein Bild zu dem Beitrag hochladen.
Allgemeines zum Upload von Daten
Wenn ein Benutzer eine Datei auf einen Server hochlädt, sollte diese nicht unverändert abgelegt werden. Denn es gibt nicht nur gute Benutzer, jede Datei kann Schadsoftware enthalten, welche von außen benutzt werden kann. Am einfachsten ist es den Dateinamen der Datei zu modifizieren (bei einem späteren Download kann dieser Name wieder angepasst werden). Zusätzlich sollte man prüfen, ob der Dateityp der Datei, welche hochgeladen wird, dem erwarteten Dateityp entspricht. In unserem Fall würden wir prüfen, ob es sich um Bilder im jpg, jpeg, png handelt.
JSF Tag
Wie in HTML gibt es einen speziellen Tag auch für JSF, dieser ist „h:inputFile“.
<h:inputFile value="#{timelineFormular.file}"></h:inputFile>
Als Value wird ein Objekt vom Typ javax.servlet.http.Part erwartet. Des Weiteren müssen wir dem umgebenen Formular ein zusätzliches Attribut hinzufügen:
<h:form enctype="multipart/form-data">
Dieses sorgt dafür das wir mehrere Datentypen zum Server senden können.
Damit wir später den Dateinamen in die originale Bezeichnung wieder zurückändern müssen wir den neuen sowie den „alten“ Dateinamen speichern. Erzeugen wir also nun ein Objekt um diese Daten zu speichern.
package de.draegerit.timeline.entity; public class UploadFile { private String filename; private String originalFilename; public String getFilename() { return filename; } public void setFilename(String filename) { this.filename = filename; } public String getOriginalFilename() { return originalFilename; } public void setOriginalFilename(String originalFilename) { this.originalFilename = originalFilename; } }
Extrahieren des Dateinamens
Den Dateinamen kann man mit reinem JavaServerFaces nicht so einfach. Jedoch findet man Header Informationen an dem Part Objekt.
String contentDisp = part.getHeader("content-disposition");
Die Header Informationen sind mit einem Semikolon separiert.
form-data; name="j_idt8:j_idt12"; filename="C:\Downloads\affe.png"
Somit müssen wir diese Zeichenkette erstmal splitten.
String[] items = contentDisp.split(";");
Da wir den Dateinamen haben wollen, müssen wir mit einer Schleife über dieses Array iterieren. Wenn der Wert mit der Zeichenkette „filename“ beginnt, so wird der Wert, nachdem Gleichheitszeichen als Dateiname extrahiert und sofort zurückgeliefert.
for (String s : items) { if (s.trim().startsWith("filename")) { return s.substring(s.indexOf("=") + 2, s.length() - 1); } }
Die ganze Methode:
private String extractFileNameFromPart(Part part) { String contentDisp = part.getHeader("content-disposition"); String[] items = contentDisp.split(";"); for (String s : items) { if (s.trim().startsWith("filename")) { return s.substring(s.indexOf("=") + 2, s.length() - 1); } } return ""; }
Für den Datentyp javax.servlet.http.Part müssen wir zusätzlich eine Java EE Abhängigkeit in der Datei „pom.xml“ einfügen.
<dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>8.0</version> <scope>provided</scope> </dependency>
Nun können wir unsere Beans’s sowie das DAO „TimelineEntry“ um dieses Feld erweitern. (Dabei sollten die getter & setter Methoden nicht vergessen werden.)
Anpassen der Tomcat server.xml
Damit wir die Dateien in einem bestimmten Ordner ablegen können müssen wir den Context der Anwendung anpassen.
<Context docBase="Timeline" path="/Timeline" reloadable="true" source="org.eclipse.jst.jee.server:Timeline" aliases="/images=/C:/temp/images" />
Hier wird zusätzlich das Attribut „aliases“ eingefügt, der Wert zeigt auf einen absoluten Pfad auf der Festplatte wo der Apache Tomcat Server läuft.
Methode zum speichern der Datei
An dem Objekt „Part“ gibt es die Methode „write“, diese Methode erwartet einen Dateinamen. Da wir einen bestimmten Ordner für die Ablage der Bilder nutzen möchten, müssen wir uns hier über den ExternenContext den Pfad holen und vor dem Dateinamen setzen.
FacesContext fC = FacesContext.getCurrentInstance(); String relativeWebPath = "/images"; String absoluteDiskPath = fC.getExternalContext().getRealPath(relativeWebPath); file.write(absoluteDiskPath + "\\" + filename);
In meinem Fall wird nun jedes Bild in den Ordner „C:\temp\images“ gespeichert.
Anzeigen der Bilder
Als Nächstes wollen wir nun die Bilder auch anzeigen.
Bevor wir jedoch ein Bild anzeigen lassen, prüfen wir, ob zu diesem Datensatz überhaupt ein Bild existiert.
<c:if test="#{not empty entrie.file}"> <h:graphicImage value="/images/#{entrie.file.filename}"></h:graphicImage> <br /> </c:if>
Wenn dieses Bild existiert so soll dieses aus dem Ordner welchen wir in der Datei server.xml definiert haben geladen werden.
Download
Hier nun das gesamte Projekt zum bequemen Download.