ASP.NET MVC: HTML5 Elemente mit jQuery Fallback nutzen
Soo, nachdem ich die Überschrift mit all den hippen Begriffen vollgepackt habe die es in Sachen Web momentan so gibt möchte ich euch kurz erklären worum es in diesem Artikel gehen soll.
Ihr werdet sicher mitbekommen haben, dass HTML5 auf dem Vormarsch ist und auch wenn es immer noch nicht offiziell fertig ist findet man immer häufiger den HTML5-Header <!DOCTYPE html> auf diversen Seiten.
Eins der meiner Meinung nach nützlichsten Features die wir mit HTML5 erhalten sind die neuen Input-Typen. Denn bisher hatten wir keine Wahl und mussten type="text" benutzen!
Die Eingabefelder vom Typ Text konnten natürlich alles enthalten, sind aber nicht gerade Benutzerfreundlich wenn man etwas komplexere Daten eingeben soll wie zum Beispiel ein Datum.
In HTML5 wurden deswegen spezifische Typen für solche immer wieder einkehrende Eingaben eingeführt. Eine von ihnen ist "Date":
Birthday: <input type="date" name="bday" />
Der große Vorteil hier ist, dass die Browser nun eine eigene Implementierung für diesen Typ von Eingabefeldern machen können. Falls der Browser nun also diesen neuen Eingabe Typ unterstützt blendet er automatisch einem (mehr oder weniger) schönen Date-Picker ein in dem man bequem das Datum wählen kann:
Ein weiterer großer Vorteil ist, dass auch die mobilen Browser wie z.B. auf dem iPhone diese neuen Elemente ebenfalls unterstützen und einen passenden Picker einblenden:
Wie ihr in dem ersten Screenshot sehen könnt unterstützt der FireFox (zumindest zum Zeitpunkt des Screenshots) den neuen Typ nicht. In diesem Fall blendet er ein gewöhnliches Textfeld ein wo wir nun gezwungen sind das Datum auf die gewohnte (unbequeme) Weise einzutragen.
Übrigens, ich glaube ich muss euch nicht sagen warum der Internet Explorer hier nicht mit aufgeführt ist oder?
Und genau das ist das Problem um das es in diesem Blogpost geht. Wenn wir die tollen neuen Elemente nutzen gehen wir das Risiko ein, dass Nutzer die einen älteren Browser nutzen nicht in den Genuss einer bequemen Eingabe kommen.
Wir könnten auch komplett auf das HTML5 Element verzichten und direkt alles mit jQuery machen, welches auch einen Date-Picker bietet, aber das ist auch nicht das gelbe vom Ei, denn dann wären die Mobilen Nutzer in Nachteil, da dort die Browser diese Elemente für gewöhnlich unterstützen und eine gewohnte Oberfläche für die Eingabe bieten.
Die Optimale Lösung ist offensichtlich eine Mischung beider Welten. Wenn der Benutzer die Seite mit einem kompatiblen Browser aufruft soll der Date-Picker des Browsers genutzt werden, wenn der Browser aber veraltet ist soll stattdessen der jQuery Fallback greifen und der JavaScript Date-Picker (siehe links) genutzt werden.
Zu unserem Glück macht uns ASP.NET MVC die Umsetzung dieses Plans sehr leicht, da es an den nötigen Stellen sehr einfach erweitert werden kann.
Schauen wir uns mal ein kleines Beispiel an. Wir haben eine Klasse Person, und wollen auch dessen Geburtstag speichern, hier wollen wir die tolle neue Funktionalität nutzen. Implementieren wir das Ganze jedoch erstmal wie gewohnt:
public class Person
{
public int ID { get; set; }
public String Name { get; set; }
public DateTime Geburtstag { get; set; }
}
Wenn wir die Model-Klasse haben können wir das MVC Scaffolding nutzen um für uns eine View zum Erstellen neuer Personen anzulegen:
Schauen wir uns nun doch mal an was da für uns tolles generiert wurde:
<div class="editor-field">
@Html.EditorFor(model => model.Geburtstag)
@Html.ValidationMessageFor(model => model.Geburtstag)
</div>
Auffällig hierbei ist, dass hier @Html.EditorFor genutzt wird und nicht etwa @Html.TextBoxFor! Der Hintergrund ist der, dass MVC bei der Generierung versuchen wird ein passendes Element für den angegebenen Datentyp (DateTime) zu bestimmen, aber da MVC standardmäßig nur den Typ Text kennt wird hier eigentlich immer ein Input-Element vom Typ text generiert.
Hier kommt uns die Erweiterbarkeit von MVC zu gute, denn wir können ganz einfach NuGet nutzen um uns die nötige Funktionalität zu verschaffen. Wir installieren das Packet MvcHtml5Templates.
Nachdem NuGet alles erledigt hat werdet ihr feststellen, dass ihr ein paar neue Dateien in eurem Views/Shared Verzeichnis habt (siehe Bild links). Und wie ihr schon an dem Namen erkennen könnt sind das die neuen Input-Typen aus HTML5.
Diese Templates machen im Grunde nichts anderes als die MVC-Eigenen zu überschreiben und versehen die mit dem passendem Type-Attribut.
Hier sind euch keine Grenzen gesetzt ihr könnt natürlich auch eire eigenen Templates definieren mit euren eigenen Typen. (Auch wenn man das eher selten benötigt)
Das Tolle: mehr müssen wir nicht machen. MVC wird nun zur Laufzeit statt einem Text ein Date Input-Element für uns anlegen. Beachtet, dass das nur geht wenn wir in der View die allgemeine Funktion @Html.EditorFor nutzen und keinen Spezifischen Typ angeben.
Schauen wir mal in den Sourcecode der zur Laufzeit für das Geburtstagsfeld generiert wird:
<input class="text-box single-line" data-val="true" data-val-required="Das Feld 'Geburtstag' ist erforderlich" id="Geburtstag" name="Geburtstag" type="datetime" value=""; />
Wie ihr seht wird nun der Korrekte Typ, nämlich datetime verwendet. Wenn wir die Seite nun also mit einem kompatiblen Browser aufrufen können wir ganz bequem das Datum wählen.
Leider ist der Opera Browser momentan wohl der einzige Desktop Browser der uns hier einen Benutzerfreundlichen Dialog einblendet (siehe erstes Bild oben), somit müssen wir dafür sorgen, dass die Benutzer mit anderen oder alten Browsern nicht benachteiligt werden.
Auch für diese Problematik bringt MVC bereits alles mit was nötig ist sie zu lösen. Werfen wir doch mal einen Blick in unseren Scripts Ordner finden wir alle nötigen jQuery und jQuery UI Skripte, dazu kommt noch die modernizr Bibliothek die uns auch zu Gute kommen wird.
Die jQuery Bibliotheken liefern und die nötige Funktionalität die wir benötigen um diesen hübschen Date-Picker einzublenden.
Die modernizr Bibliothek dagegen hilft uns herauszufinden ob der Browser, den der Benutzer momentan verwendet die gewünschten HTML5 Features unterstützt.
Nun müssen wir also bei unseren Formularen prüfen, ob der Browser die nötigen Funktionen kennt und bei Bedarf die jQuery Klassen einbinden. Das Ganze ist dank der Einfachheit von Modernizr und jQuery ziemlich simpel.
Wechseln wir zu unserer View und fügen die Referenzen auf die nötigen Skripte und CSS Dateien ein:
<script src="@Url.Content("~/Scripts/jquery-1.5.1.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery-ui-1.8.11.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/modernizr-1.7.js")" type="text/javascript"></script>
<link href="@Url.Content("~/Content/themes/base/jquery.ui.all.css")" rel="stylesheet" type="text/css" />
Unten in der View fügen wir nun eine Modernizr Abfrage ein und prüfen, ob der Browser das DateTime-Feld unterstützt, und wenn nicht lassen wir jQuery das Ganze für uns regeln:
<script type="text/javascript">
$(function () {
if (!Modernizr.inputtypes.date) {
$("input[type='datetime']").datepicker();
}
});
</script>
Mit dem Befehl $("input[type='datetime']").datepicker(); werden alle Input-Felder vom Typ DateTime durch den jQuery Picker ersetzt. Coole Sache!
Kleiner Hinweis am Rande: Das ist nicht wirklich best practice da die Skripte auch geladen werden wenn sie gar nicht benötigt werden (der Browser kennt das HTML5 Feld) ich habe das hier der Einfachheit geopfert.
Und das wars auch schon! Alle Browser die das Feld unterstützen blenden nun ihren Picker ein, ansonsten wird der jQuery Fallback genutzt.
Wir müssen uns auch nicht mit irgendwelchen UserAgent-Prüfungen herumschlagen und lassen das alles Modernizr erledigen, der das intern über JavaScript prüft und somit immer aktuell ist. Das heißt dass wenn irgendwann der IE plötzlich die neuen Typen kennt werden diese auch funktionieren!
Genauso könnt ihr auch bei all den anderen neuen HTML5 Typen vorgehen und euren Usern ein bestmögliches Bedienerlebnis bereiten
Konsolenausgaben in einer WindowsForms Applikation
Manchmal will man eine WindowsForms Anwendung auch über die Konsole aufrufen und dann möchte man natürlich auch Ausgaben der Anwendung in dem Konsolenfesnter sehen. Doch leider ist das nicht so einfach, da alle Aufrufe über die Console-Klasse werden ignoriert.
Doch warum ist das so? Das ist ganz einfach und liegt daran, dass das Konsolenfenster, das unser Programm startet zu einem anderem Prozess gehört, nämlich cmd.exe und somit nicht verbunden ist.
Was wir also tun müssen ist, unsere Ausgabe an das Konsolenfenster zu heften, das unsere Anwendung gestartet hat. Dafür kann man die Win32-Methode AttachConsole nutzen.
Diese ist sehr einfach aufgebaut und sollte vor der ersten Ausgabe an die Konsole aufgerufen werden:
Definition des Win32-Aufrufs: (Beispiele in VB.NET)
<DllImport("kernel32.dll")> _
Private Shared Function AttachConsole(dwProcessId As Integer) As Boolean
End Function
Um die Ausgabe an den Prozess zu geben der uns aufgerufen hat (Parent-Process) kann man hier als Wert -1 übergeben:
Private Const ATTACH_PARENT_PROCESS As Integer = -1
Was nun noch bleibt ist es die Methode aufzurufen, zum Beispiel in der Main-Methode der Applikation:
Private Sub frmMain_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
AttachConsole(ATTACH_PARENT_PROCESS)
//[...]
Das wars auch schon, nun werden eure Ausgaben an das Konsolenfester umgeleitet. Um die Konsole wieder von eurem Prozess zu lösen kann die paremeterlose Methode FreeConsole genutzt werden.
Es lohnt sich hier natürlich dann auch eine Unterscheidung zu machen wie die App gestartet wurde, also ob über Konsole oder über Doppelklick wie immer. Wenn sie über die Konsole gestartet wird, sollte man vielleicht die Oberfläche nicht mehr anzeigen. Das kann man dann zB. daran erkennen, dass Environment.GetCommandLineArgs.Length > 1 ist.
Probleme
Leider gibts ein paar Nebeneffekte die nicht unbeachtlich sind.
Ein Problem ist, dass Konsolenfuntionen die die Ausgabe von Programmen umleiten zB. so: "myapp.exe -test >test.txt" nicht funktionieren, die Ausgabe erfolgt weiterhin auf der Konsole. Hierfür konnte ich leider keine Lösung finden, wenn euch was einfällt einfach kommentieren.
Das zweite ist nur eine Schönheitssache, nämlich wenn das Programm beendet wird wird erst nicht die gewohnte Konsoleneingabezeile angezeigt, sondern nur der blinkende Cursor, so dass es aussieht als ob das Programm noch laufen würde, was es aber nicht mehr tut. (Wie gesagt Schönheitssache) Das kann man zB. dadurch umgehen, dass man vor dem Beenden des Programms noch ein [Enter] auf der Konsole ausgibt.
Weitere Infos und auch eine kleine Diskussion gibts hier und hier.
ASP.NET MVC: Zugriff auf Dateien einschraenken
Oft kommt es vor dass man bestimmte Inhalte nicht der breiten Öffentlichkeit präsentieren möchte. So will man beispielsweise bestimmte Inhalte nur autorisierten Benutzern oder nur Admins zur Verfügung stellen, andere User die nicht eingeloggt sind sollen diese Inhalte nicht abrufen können. Ein weiterer Anwendungsfall wäre auch hotlinking zu verhindern.
Dieses Verhalten kann man in ASP.NET MVC über verschiedene Wege erreichen. Eine beliebte Möglichkeit ist es
die Dateien über einen Controller zu verteilen.
Hierbei erstellt man einen Controller der eine Action hat in der die gewünschte Datei geladen und an den Browser zurückgegeben wird. Hier hat man dann die Bequemlichkeit, dass man problemlos prüfen kann ob der User eingeloggt ist und über die nötigen Rechte für das Bild verfügt. Das geht zum Beispiel mit dem Authorized-Attribut für die Actions.
Implementiert würde so eine Action dann wohl ca so aussehen:
public ActionResult Image(string id)
{
var dir = Server.MapPath("/Private/Images");
var path = Path.Combine(dir, id + ".jpg");
return base.File(path, "image/jpeg");
}
Das Bild kann man dann so abrufen:
<img src="@Html.Content("~/Content/Image/" + model.pictureID)" />
Dieser Ansatz funktioniert problemlos hat aber den kleinen Nachteil, dass er nicht so performant ist wie der direkte Zugriff auf die Ressource.
Eine weitere (und auch meine bevorzugte) Möglichkeit eine solche Zugriffskontrolle zu implementieren ist es durch
einen eigenen RouteHandler.
Eine Beispielimplementierung wie man auf diese Weise Hotlinking verhindern kann habe ich in Mike Brinds Blog gefunden.
Hier sind im Grunde drei kleine Schritte nötig um einen eigenen RouteHandler zum Laufen zu bekommen. (Code von Mikes Blog)
Zunächst müssen wir die RouteHandler Klasse anlegen, die das IRouteHandler Interface und dessen einzige Methode GetHttpHandler implementiert:
public class ImageRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new ImageHandler(requestContext);
}
}
Als nächstes erstellen wir die oben genutzte ImageHandler Klasse in der wir das das IHttpHandler Interface implementieren:
public class ImageHandler : IHttpHandler
{
public ImageHandler(RequestContext context)
{
ProcessRequest(context);
}
private static void ProcessRequest(RequestContext requestContext)
{
var response = requestContext.HttpContext.Response;
var request = requestContext.HttpContext.Request;
var server = requestContext.HttpContext.Server;
var validRequestFile = requestContext.RouteData.Values["filename"].ToString();
const string invalidRequestFile = "thief.gif";
var path = server.MapPath("~/graphics/");
response.Clear();
response.ContentType = GetContentType(request.Url.ToString());
if (request.ServerVariables["HTTP_REFERER"] != null &&
request.ServerVariables["HTTP_REFERER"].Contains("yourdomain.com"))
{
response.TransmitFile(path + validRequestFile);
}
else
{
response.TransmitFile(path + invalidRequestFile);
}
response.End();
}
private static string GetContentType(string url)
{
switch (Path.GetExtension(url))
{
case ".gif":
return "Image/gif";
case ".jpg":
return "Image/jpeg";
case ".png":
return "Image/png";
default:
break;
}
return null;
}
public bool IsReusable
{
get { return false; }
}
}
Zu Schluss müssen wir diesen neuen Handler mit einer Route in der Global.asax registrieren:
routes.Add("ImagesRoute",
new Route("images/{filename}", new ImageRouteHandler()));
Hierbei ist noch wichtig, dass ihr die Route an der passenden Stelle registriert, sodass diese nicht ungewollt von einer anderen Route blockiert wird weil diese auch auf dieses Pattern matcht.
Nun werden alle Requests die auf die Route "images/{filename}" gehen und von einer anderen Domain stammen als eurer eigenen auf eine andere Grafik umgeleitet.
Nun bleibt noch ein kleines Problem, denn wenn das Verzeichnis mit den Bildern in eurem Content Verzeichnis liegt kann immer noch darauf verlinkt werden wenn man den vollen Pfad nimmt, also statt dem virtuellem Pfad
http://localhost/images/myimg.jpg
die URL benutzt unter der tatsächlich alle Bilder liegen:
http://localhost/Content/graphics/myimg.jpg
Dies liegt daran, dass der neue RouteHandler in diesem Fall nicht greift und die Default Route genutzt wird. Am einfachsten kann man dies verhindern wenn man den Zugriff für Unbefugte über die web.config sperrt:
<location path="Content/graphics">
<system.web>
<authorization>
<allow roles="admin" />
<deny users="*" />
</authorization>
</system.web>
</location>
Das war dann auch schon, ich nutze gern den zweiten Weg, da dieser mir einfacher und sauberer erscheint. Wenn euch noch andere Möglichkeiten einfallen wie man das anstellen kann dann immer her damit!
SSL Teil 4: Serverseitige Authentifizierung mit Java
Nachdem wir in den letzen Teilen mehr die Theorie durchgenommen haben, möchte ich jetzt zu dem praktischen Teil kommen und euch zeigen wie man diese Theorie in Code umsetzen kann am Beispiel Java. (.NET wird auch noch folgen)
Wir wollen einfach einsteigen und implementieren erst mal nur die eine Serverseitige Authentifizierung, also muss der Server sich uns gegenüber ausweisen und wir sind in der Pflicht sein Zertifikat anzunehmen oder abzulehnen.
Wie in Teil 1 erklärt benötigen wir also zunächst einen Ort wo wir die Zertifikate sichern, denen wir vertrauen. Hier können wir (da wir in Java unterwegs sind) nicht die Windows-eigenen nehmen sondern müssen die so genannten Keystores von Java Nutzen.
Ein Keystore, wie der Name schon sagt ist nichts anderes als ein Speicher für Zertifikate. Wenn ihr Java installiert dann legt Java bereits einen Keystore für euch an, diesen findet ihr dann in eurem Java Verzeichnis Java\jre6\lib\security\cacerts die Datei „cacerts“ enthält hier bereits ähnlich wie bei Windows bereits die CA-Zertifikate von den großen Zertifizierungsstellen.
Es wir desweiterhin in Java unterschieden zwischen Keystore und Truststore, beides sind im Grunde Keystores und unterscheiden sich nur im Namen und in ihrem Inhalt. Eine einzige Datei kann auch beide beinhalten, ist aber eher unüblich, da man die eigenen (privaten) Zertifikate von den CA-Zertifikaten separieren will.
So enthalten die Keystores für gewöhnlich die eigenen Privaten Schlüssel und die eigenen von anderen CAs signierte Zertifikate, die bei Verbindungen als Serverzertifikate (Public Keys) genutzt werden. Der Truststore ist das Gegenstück und enthält die Zertifikate denen man vertraut. So müssen wir die Zertifikate die wir vom Server beim Verbindungsaufbau erhalten gegen einen Truststore prüfen.
Fangen wir mit unserem Code an, wir wollen einen Client erstellen, der eine Socket-basierte Verbindung zu einem Server über SSL aufbaut und das vom Server zurückgegebene Zertifikat prüft.
Java ist auf diesem Gebiet sehr entgegenkommend und bietet für fast alle Anwendungsfälle fertige APIs und Klassen die uns fast die ganze Arbeit abnehmen.
Um den Prozess besser aufzeigen zu können schreiben wir uns eine Eigene SSLSocketFactory die für uns den Aufbau der Verbindung übernimmt und unserem Programm im Prinzip ein fertiges Socket liefert.
Dazu erstellen wir eine neue Klasse und lassen diese von SSLSocketFactory erben. Da das eine abstrakte Klasse ist müssen eir einige Methoden implementieren. Wenn das erledigt ist sieht unsere Klasse schon so aus:
/**
* Stellt eine SSLSocketFactory bereit die einen angegebenen TrustStore nutzt
* @author Sebastian Gross
*/
public class ServerSSLAuthSocketFactory extends SSLSocketFactory{
@Override
public String[] getDefaultCipherSuites() {
}
@Override
public String[] getSupportedCipherSuites() {
}
@Override
public Socket createSocket(Socket socket, String string,
int i, boolean bln) throws IOException {
}
@Override
public Socket createSocket(String destinationAddress,
int destinationPort) throws IOException, UnknownHostException {
}
@Override
public Socket createSocket(String string, int i,
InetAddress ia, int i1) throws IOException, UnknownHostException {
}
@Override
public Socket createSocket(InetAddress ia, int i) throws IOException {
}
@Override
public Socket createSocket(InetAddress ia, int i,
InetAddress ia1, int i1) throws IOException {
}
}
Nun müssen wir diesen Methoden Leben einhauchen, aber keine Angst das ist nicht so schlimm wie es aussieht, denn die einzige Änderung die wir machen müssen ist es der SSLSocktFactory beizubringen mit unserem Truststore zu arbeiten, der Rest der Funktionalität soll unberührt bleiben.
Fügen wir also einen Konstruktor zu unserer Klasse hinzu der das Initialisieren unserer Internen SSLSocketFactory mit unserem Truststore übernimmt.
/** Delegate {@link SSLSocketFactory}. */
private transient SSLSocketFactory factory;
private File caFile;
private KeyStore ks;
private SSLContext context;
private TrustManagerFactory tmf;
private X509TrustManager defTrustManager;
private SavingTrustManager tm;
/**
* Neue Truststore-basierte SSL-SocketFactory
* @param truststoreLocation Vollqualifizierter Pfad zum Truststore oder
leer lassen für Java-Default-Truststore
* @param truststorePass Passwort für den Truststore. Wenn leer wird
"changeit" ala Passwort benutzt.
*/
public ServerSSLAuthSocketFactory(String truststoreLocation,
String truststorePass){
super();
//Wenn ein Pfad zum TrustStore angegeben wurde diesen benutzen
if(!truststoreLocation.isEmpty()){
caFile = new File(truststoreLocation);
}else{
//Pfad zum default Java TrustStore finden
caFile = new File("cacerts");
if (!caFile.exists() || !caFile.isFile()) {
char SEP = File.separatorChar;
File dir = new File(System.getProperty("java.home") + SEP
+ "lib" + SEP + "security");
caFile = new File(dir, "cacerts");
if (!caFile.exists() || !caFile.isFile()) {
caFile = new File(dir, "cacerts");
}
}
}
//Prüfen, ob ein TrustStore Password angegeben wurde
String truststorePassword = truststorePass.isEmpty() ? "changeit" : truststorePass;
//TrustStore öffnen
try {
InputStream in = new FileInputStream(caFile);
ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(in, truststorePassword.toCharArray());
in.close();
} catch (KeyStoreException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
} catch (NoSuchAlgorithmException ex){
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
} catch (CertificateException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
} catch (FileNotFoundException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
}catch (IOException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
}
try {
context = SSLContext.getInstance("TLS");
tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
defTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];
context.init(null, new TrustManager[] {defTrustManager}, null);
factory = context.getSocketFactory();
} catch (NoSuchAlgorithmException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
} catch (KeyStoreException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
} catch (KeyManagementException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
}
}
Das sieht auf den ersten Blick recht komplex aus, ist aber im Grunde recht simpel.
- In den Zeilen 23 - 28 prüfen wir ob der Konstruktor einen Pfad zu einem Truststore übergeben bekommen hat, und wenn dies nicht der Fall ist, wird der Pfad zu dem Standard-Java-Truststore in eurem JRE-Verzeichnis ermittelt.
- Direkt danach in Zeile 41 prüfen wir ob ein Passwort übergeben wurde. Wenn keins übergeben wurde wird "changeit" genutzt. "changeit" ist das Defaultpasswort für den Standard-Java-Truststore.
- In Zeile 45 - 48 öffnen wir den Keystore und laden dessen Inhalt in unsere Kokale KeyStore Variable.
- Das Wichtigste geschieht nun in Zeile 67 - 72 hier holen wir uns einen Verweis auf den SSLContext (67) und erstellen uns mit Hilfe unseres KeyStores einene TrustManagerFactory(68-69). Anschließend lassen wir die TrustManagerFactory für uns einen neuen X509TrustManager "produzieren" der dann den Verweis auf unseren eigenen Keystore trägt. (70)
- Nun sagen wir dem SSLContext, dass es unseren Trustmanager nutzen soll (71) für die darüber laufende Verbindungen und erstellen uns eine SSLSocketFactory (72)
Nun haben wir intern eine SocketFactory, die unseren eigenen (über den Konstruktor angegebenen) Keystore nutzt. Der Rest der Klasse die wir erstellt haben dient hierbei eigentlich nur als Wrapper und Durchreiche, der Wichtigste Teil war der Konstruktor.
Die restlichen Methoden können nun also alle Anfragen direkt an die produzierte SSLSocketFactory weiterleiten. So sieht dann die fertige Klasse aus:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
/**
* Stellt eine SSLSocketFactory bereit die einen angegebenen TrustStore nutzt
* @author Sebastian Gross
*/
public class ServerSSLAuthSocketFactory extends SSLSocketFactory{
/** Delegate {@link SSLSocketFactory}. */
private transient SSLSocketFactory factory;
private File caFile;
private KeyStore ks;
private SSLContext context;
private TrustManagerFactory tmf;
private X509TrustManager defTrustManager;
private SavingTrustManager tm;
/**
* Neue Truststore-basierte SSL-SocketFactory
* @param truststoreLocation Vollqualifizierter Pfad zum
* Truststore oder leer lassen für Java-Default-Truststore
* @param truststorePass Passwort für den Truststore.
* Wenn leer wird "changeit" ala Passwort benutzt.
*/
public ServerSSLAuthSocketFactory(String truststoreLocation,
String truststorePass){
super();
//Wenn ein Pfad zum TrustStore angegeben wurde diesen benutzen
if(!truststoreLocation.isEmpty()){
caFile = new File(truststoreLocation);
}else{
//Pfad zum default Java TrustStore finden
caFile = new File("cacerts");
if (!caFile.exists() || !caFile.isFile()) {
char SEP = File.separatorChar;
File dir = new File(System.getProperty("java.home") + SEP
+ "lib" + SEP + "security");
caFile = new File(dir, "cacerts");
if (!caFile.exists() || !caFile.isFile()) {
caFile = new File(dir, "cacerts");
}
}
}
//Prüfen, ob ein TrustStore Password angegeben wurde
String truststorePassword =
truststorePass.isEmpty() ? "changeit" : truststorePass;
//TrustStore öffnen
try {
InputStream in = new FileInputStream(caFile);
ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(in, truststorePassword.toCharArray());
in.close();
} catch (KeyStoreException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
} catch (NoSuchAlgorithmException ex){
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
} catch (CertificateException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
} catch (FileNotFoundException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
}catch (IOException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
}
try {
context = SSLContext.getInstance("TLS");
tmf = TrustManagerFactory.getInstance(TrustManagerFactory
.getDefaultAlgorithm());
tmf.init(ks);
defTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];
context.init(null, new TrustManager[] {defTrustManager}, null);
factory = context.getSocketFactory();
} catch (NoSuchAlgorithmException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
} catch (KeyStoreException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
} catch (KeyManagementException ex) {
Logger.getLogger(ServerSSLAuthSocketFactory.class.getName())
.log(Level.SEVERE, null, ex);
}
}
@Override
public String[] getDefaultCipherSuites() {
return factory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return factory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket socket, String string,
int i, boolean bln) throws IOException {
return factory.createSocket(socket, string, i, bln);
}
@Override
public Socket createSocket(String destinationAddress,
int destinationPort) throws IOException, UnknownHostException {
return (SSLSocket)factory.createSocket(destinationAddress, destinationPort);
}
@Override
public Socket createSocket(String string, int i,
InetAddress ia, int i1) throws IOException, UnknownHostException {
return factory.createSocket(string, i, ia, i1);
}
@Override
public Socket createSocket(InetAddress ia, int i) throws IOException {
return factory.createSocket(ia, i);
}
@Override
public Socket createSocket(InetAddress ia, int i,
InetAddress ia1, int i1) throws IOException {
return factory.createSocket(ia, i, ia1, i1);
}
}
Da wir nun eine Factory haben die uns Sockets liefert, die auf unseren Truststore zugreift, können wir diese nun nutzen um eine Verbindung zu SSL Servern aufzubauen. Das Schöne hierbei ist, dass Java an sehr vielen Stellen SocketFactories nutzt, so können wir fast in allen Szenarien unsere Klasse einsetzen.
So können wir unsere SSLFactory nun ganz simpel testen:
public static void main (String [] args) throws IOException{
int port = 443; // default https port
String host = "httpsurl";
try {
SSLSocketFactory factory
= (SSLSocketFactory) new ServerSSLAuthSocketFactory("","");
SSLSocket socket = (SSLSocket) factory.createSocket(host, port);
// enable all the suites
String[] supported = socket.getSupportedCipherSuites();
socket.setEnabledCipherSuites(supported);
Writer out = new OutputStreamWriter(socket.getOutputStream());
// https requires the full URL in the GET line
out.write("GET https://" + host + "/ HTTP/1.1\r\n");
out.write("Host: " + host + "\r\n");
out.write("\r\n");
out.flush();
// read response
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
// read the header
String s;
while (!(s = in.readLine()).equals("")) {
System.out.println(s);
}
System.out.println();
// read the length
String contentLength = in.readLine();
int length = Integer.MAX_VALUE;
try {
length = Integer.parseInt(contentLength.trim(), 16);
}
catch (NumberFormatException ex) {
// This server doesn't send the content-length
// in the first line of the response body
}
System.out.println(contentLength);
int c;
int i = 0;
while ((c = in.read()) != -1 && i++ < length) {
System.out.write(c);
}
System.out.println();
out.close();
in.close();
socket.close();
}
catch (IOException ex) {
System.err.println(ex);
}
}
Tragt in Zeile 4 einfach den gewünschten SSL-Host ein wie z.B. Google oder Facebook und startet das kleine Programm. Falls ihr keine Parameter beim Erzeugen der Factory (Zeile 7 und
angegeben habt solltet ihr auch keine Probleme beim Verbinden haben und wenn das Programm durchgelaufen ist seht ihr die Antwort des Servers im HTML Format.
Falls ihr doch einen eigenen Truststore eingetragen habt müsst ihr sicherstellen, dass dieser den Public Key des Servers zu dem ihr euch verbinden wollt beinhaltet, sonst wird die Verbindung nicht erfolgen.
Teil 1: Serverseitige Authentifizierung und Zertifikate
Teil 2: Beidseitige Authentifizierung
Teil 3: Der SSL Handshake
Teil 4: Serverseitige Authentifizierung mit Java
Eine TreeView durchsuchen und den gefundenen Pfad aufklappen
Da ich heute wieder eine Mail mit der Frage bekommen habe wie man ein Item in einer TreeView finden kann und dieses dann anzeigen kann, dachte ich ich poste es gleich hier falls es noch jemand braucht.
Das Ganze funktioniert sehr einfach mit nur einer kleinen rekursiven Funktion:
''' <summary>
''' Diese Funktion durchläuft den ganzen Treenode bis ein Eintrag gefunden wird der dem Suchbegriff
''' gleicht. Danach wird der Pfad der zu diesem TreeNode führt (zur Visualisierung) aufgeklappt
''' </summary>
''' <param name="tn">Der zu durchsuchende Treenode
''' <param name="name">Der Suchbegriff
Public Sub findNode(ByVal tn As TreeNode, ByVal name As String)
For Each tnn As TreeNode In tn.Nodes
If tnn.Text = name Then
Dim t As TreeNode = tnn
For i As Integer = tnn.Level To i = 0 Step -1
Try
t = t.Parent
t.Expand()
Catch ex As Exception
End Try
Next
tnn.Parent.Expand()
Exit Sub
End If
findNode(tnn, name)
Next
End Sub
Hier laufen wir alle Nodes der TreeView durch bis wir auf einen Node stoßen der unserem Suchbegriff entspricht. Ab hier durchlaufen wir den ganzen Weg rückwärts und klappen alle übergeordneten Nodes auf, sodass wir unseren gesuchten Node zu Gesicht bekommen.
Diese Funktion kann man auch ganz gut als Extension Method umschreiben, sodass diese bequemer aufgerufen werden kann, aber das ist dann natürlich jedem selbst überlassen.
Hier noch ein Demoprojekt zum testen: Download [VS 2010]
Anonyme Threaduebergreifende Eventhandler
Jeder von uns uns hatte schon mit Events und den dazugehörigen Handlern zu tun. Jeder Button auf einem Formular hat z.B. einen eigenen Eventhandler, der irgendwo in der Form.Designer.cs definiert und registriert wird. Dieser sieht dann meist ca so aus:
//Registrierung
this.button1.Click += new System.EventHandler(this.button1_Click);
//Verweis / Nutzung
private void button1_Click(object sender, EventArgs e) { }
So wird jedes Mal wenn des Click-Event der Button-Instanz ausgelöst wird die registrierte Methode button1_Click Ausgeführt. Eigentlich ein schönes und einfaches System, aber wenn man eigene Klassen hat und teilweise viele Instanzen davon bildet ist es unschön, da dabei sehr viele Methoden in der Klasse entstehen und somit die Übersicht mit der Zeit verloren geht.
Deswegen gibt es für uns die Möglichkeit so genannte anonyme Methoden als EventHandler zu verwenden. Diese benötigen keine Deklaration und haben keine Referenz, somit gehen diese auch verloren, sobald diese zu Ende gelaufen sind. Benutzen kann man diese so:
this.button1.Click += delegate(object sender, EventArgs e) {
//hier steht mein Code
};
Wie man sieht ist der Code um einiges übersichtlicher geworden und kürzer geworden. Wir übergeben nun eine anonyme Methode als Handler.
Was das ganze noch etwas interessanter macht, ist dass es noch mehr Ersparnis und noch mehr Übersicht bei Threadübergreifenden EventHandlern gibt. Schauen wir uns mal an wie es normalerweise ausschaut:
//Registrierung des EventHandler zum anderen Thread
man.NewInformationUpdate += new NewInformationUpdateArrived(update_received);
//Handlermethode
public void update_received(object sender, TestInformationUpdateEventArgs ev) {
//Aufruf zu dem eigenen Thread deligieren
this.BeginInvoke(new delegateupdate(update), new object[] { ev });
}
//Definition der delegaten Methode
public delegate void delegateupdate(TestInformationUpdateEventArgs ev);
//Eigentliche Methode zum Verarbeiten des Events
private void update(TestInformationUpdateEventArgs msg) {
//...
}
Wir haben nun drei Methoden definiert, nur um ein Event zu behandeln, dies ist von Nöten, da wir hier Events verarbeiten, die aus einem anderen Thread kommen.
Hier können wir ebenfalls viel einsparen wenn wir mit delegates und anonymen Methoden arbeiten:
fd1.CompletionInformation += delegate(object se, CompletionInformationArgs ci) {
this.BeginInvoke(new EventHandler(delegate(object obj, EventArgs ea) {
CompletionInformationArgs cea = (CompletionInformationArgs)ea;
//Unser verabeitungs Code hier
}), new object[] { se, ci });
};
Hier haben wir wieder aus drei Methoden eine gemacht, naja eine richtige zumindest
und zwei anonyme. Ich finde diese Variante deutlich lesbarer und übersichtlicher, da alle wichtigen Information an einer Stelle sind.
Doch Vorsicht, diese Methode hat einen gravierenden Nachteil, da wir hier anonyme Methoden benutzen haben wir keine Referenz darauf und können somit das registrierte Event nie wieder un-registrieren. Mit anderen Worten, man muss abwegen wann man diese Abkürzung gehen kann und wann nicht.
Meiner Erfahrung nach wird der Fall überwiegen wo man diesen Weg leider nicht gehen kann, aber dann kann man versuchen ein Zwischenweg zu finden:
var handler = new EventHandler(delegate(object o, EventArgs e)
{
//handler Logik
};
Subject.NewEvent += handler;
//Diesen können wir später dann auch entfernen
Subject.NewEvent -= handler;
Auf diese Weise haben wir immernoch eine Ersparnis und behalten die volle Kontrolle über unsere Handler.
Als kleine (unschöne) Alternative können wir auch anonyme Handler loswerden, indem wir alle Handler entfernen:
public event EventHandler SomeEvent;
public void ClearEventHandlers() {
Delegate[] delegates = SomeEvent.GetInvocationList();
foreach (Delegate delegate in delegates) {
SomeEvent -= (EventHandler) delegate;
}
}
Java: SubjectDN eines X509Zertifikates zu OpenSSL kompatibel machen
Java stellt uns zum angenehmen Arbeiten mit X509-Zertifikaten die Klasse java.security.cert.X509Certificate zur Verfügung. Hier können wir uns sehr einfach über die methode .getSubjectDN().getName() den Distinguished Name des Zertifikats besorgen.
Wenn man in der Javawelt bleibt ist das auch alles ok und funktioniert Problemlos. Will man aber andere Programme wie zum Beispiel OpenSSL anbinden, bekommt man hier ein Problem, denn OpenSSL liefert einen anderen SubjectDN String als unsere Java Klasse.
Wobei mit anders meine ich, dass die Reihenfolge der ausgegebenen Parameter anders ist, hier ein Beispiel:
Ausgabe von OpenSSL (über die X509_NAME_oneline Methode):
/C=DE/ST=NRW/L=Bielefeld/O=ORG/OU=AB1/CN=130.122.146.121
Wogegen die Java-Klasse folgenden String liefert (.getSubjectDN().getName()) :
CN=130.122.146.121, OU=AB1, O=ORG, L=Bielefeld, ST=NRW, C=DE
Die Reihenfolge scheint genau umgekehrt zu sein und auch die Trenner sind anders, und wenn man den String nun als Auth-String benutzen will bekommt man ein Problem.
Leider bietet uns Java hier keine Möglichkeit den String anders zu sortieren, man kann zwar über die Methode getSubjectX500Principal.getName(String format) einen RFC als Parameter mitgeben, welcher die Sortierung etwas anpasst, aber teilweise noch komischere Ergebnisse liefert.
Hier bleibt einem nun leider nichts anderes übrig, als selbst einzugreifen und die Sortierung selbst vorzunehmen. Dazu habe ich eine kleine Klasse geschrieben die das für euch erledigt. Diese hat zwei Methoden, eine die einen OpenSSL Kompatiblen String ausgibt und eine bei der ihr die Reihenfolge der Parameter selbst bestimmen könnt:
/**
* Teilt die Informationen aus dem X509Certificate.SubjectDN in die
* einzelnen Bestandteile auf und macht diese einzeln verfügbar
* @author sebastian gross blog.bigbasti.com
*/
public class CertificateDataSeperator {
private String _C = "";
private String _ST = "";
private String _L = "";
private String _O = "";
private String _OU = "";
private String _CN = "";
private String _EMAILADRESS = "";
/**
* Erstellt eine neue instanz
* @param cert Das Zwetifikat aus dem die Daten gelesen werden sollen
* @param seperator Die Zeichenfolge ander die parameter
* getrennt werden sollen, bei Java ist Standard ", "
*/
public CertificateDataSeperator(X509Certificate cert, String seperator){
String [] subjectDNData;
subjectDNData = cert.getSubjectDN().getName().split(seperator);
seperateData(subjectDNData);
}
/**
* Duchläuft das Array und ordnet die Informationen
* @param data Array mit dem SubjectDN String
*/
private void seperateData(String [] data){
for(String s : data){
if((s.toUpperCase()).startsWith("C=")){
_C = s;
}else if((s.toUpperCase()).startsWith("ST=")){
_ST = s;
}else if((s.toUpperCase()).startsWith("L=")){
_L = s;
}else if((s.toUpperCase()).startsWith("O=")){
_O = s;
}else if((s.toUpperCase()).startsWith("OU=")){
_OU = s;
}else if((s.toUpperCase()).startsWith("CN=")){
_CN = s;
}else if((s.toUpperCase()).startsWith("EMAILADRESS=")){
_EMAILADRESS = s;
}
}
}
/**
* Generiert einen angepassten SubjectDN String
* @param params Die gewünschten Werte in der gewünschten Reihenfolge als Array
* @param seperator Die Zeichenfolge, die zum Trennen der Parameter genutzt werden soll
* @return Einen angepassten SubjectDN String
*/
public String getNewDNString(String [] params, String seperator){
StringBuilder sb = new StringBuilder();
for(String s : params){
if((s.toUpperCase()).equals("C")){
sb.append(_C);
}else if((s.toUpperCase()).equals("ST")){
sb.append(_ST);
}else if((s.toUpperCase()).equals("L")){
sb.append(_L);
}else if((s.toUpperCase()).equals("O")){
sb.append(_O);
}else if((s.toUpperCase()).equals("OU")){
sb.append(_OU);
}else if((s.toUpperCase()).equals("CN")){
sb.append(_CN);
}else if((s.toUpperCase()).equals("EMAILADRESS")){
sb.append(_EMAILADRESS);
}
if(!s.equals(params[params.length-1])){
sb.append(seperator);
}
}
return sb.toString();
}
/**
* Erzeugt einen DN String in dem Format, wie ihn auch die
* X509_NAME_oneline Methode von OpenSSL generiert, dies ist
* auch das Format, welches von dem Exim Email Server genutzt wird.
* Das Forman ist wiefolgt: /C=$/ST=$/L=$/O=$/OU=$/CN=$
* @return OpenSSL
*/
public String getOpenSSLCompatibleDNString(){
StringBuilder sb = new StringBuilder();
sb.append("/").append(_C).append("/").append(_ST).append("/").append(_L).append("/")
.append(_O).append("/").append(_OU).append("/").append(_CN);
//EMAILADRESS Ist ein optionales Feld und könnte nicht gesetzt sein
if(_EMAILADRESS != null){
sb.append("/").append(_EMAILADRESS);
}
return sb.toString();
}
public String getC() {
return _C;
}
public String getCN() {
return _CN;
}
public String getEMAILADRESS() {
return _EMAILADRESS;
}
public String getL() {
return _L;
}
public String getO() {
return _O;
}
public String getOU() {
return _OU;
}
public String getST() {
return _ST;
}
}
Mit dieser kleinen Klasse ist es sehr einfach einen OpenSSL kompatiblen SubjectDN zu erhalten:
X509Certificate myCert = session.getClientCertificate(); CertificateDataSeperator sep = new CertificateDataSeperator(myCert, ", "); String dn = sep.getOpenSSLCompatibleDNString();
Wenn man selbst die Reihenfolge der parameter bestimmen will kann man das z.B. so machen:
X509Certificate myCert = session.getClientCertificate();
String [] myParams = new String[]{"O", "CN", "L"};
CertificateDataSeperator sep = new CertificateDataSeperator(myCert, ", ");
String dn = sep.getNewDNString(myParams, "|");
Nun soll erst O, dann CN und zum Schluss L ausgegeben, jeweils von einem | (pipe) getrennt.
Das Ganze lässt sich natürlich je nach verwendeten optionalen SSL Parametern noch beliebig erweitern, und ist eine einfache und schnelle Methode um den SubjectDN neu zu sortieren.
Entstehung einer ASP.NET Webanwendung anhand eines Beispiels
Mit diesem Artikel will ich skizzieren wie eine Webanwendung mittels ASP.NET entworfen und umgesetzt wird. Ich will einen Einblick in die Vorüberlegungen, sowie in die Umsetzung des Projektes geben. Nicht mehr, aber auch nicht weniger.
Vorüberlegungen
Wie fast alle Hobbyprogrammierer erstelle auch ich eine Unzahl unnötiger und zum Teil auch unsinniger Anwendungen. Dies ist der Luxus den man sich leisten kann, wenn man damit nicht sein Geld verdienen muss.
Da ich ab und zu auf Dienstreise gehe, ist die aktuelle Uhrzeit am gegenwärtigen Standort immer ein Thema. Oft kommt auch die Frage auf, wie spät es denn bei den Lieben zu Hause wohl sein mag.
Hierfür kommt natürlich nur, mein neuestes Steckenpferd, eine Webanwendung in Frage.
Die ersten Überlegungen:
- Welcher Aufbau einer Webanwendung ist der beste
- Wie ermittle ich meinen aktuellen Standort
- Welche zusätzlichen Informationen kann ich noch anzeigen
1. Welcher Aufbau einer Webanwendung ist der beste?
Ich hab mich hier, sehr schnell, für eine Webanwendung entschieden, die ihre Informationen aus einem RSS-Feed bezieht. Vorteil hierbei, es handelt sich um ein XML-Format, welches leicht zu lesen ist und auch anderweitig verwendet werden kann.
Eine Windows-, Linux-, MacOS- oder Android Anwendung könnte den RSS-Feed ebenfalls nutzen.
2. Wie ermittle ich meinen aktuellen Standort
Im Prinzip wäre GPS das ideale Werkzeug um den aktuellen Standort auf der Welt bestimmen zu können. Da ich jedoch nur ein Notebook auf meinen Dienstreisen zur Verfügung habe, fällt das mit dem GPS aus, denn die wenigsten Notebooks verfügen über GPS – meines schon gar nicht.
Eine weitere Möglichkeit meinen Standort zu bestimmen wäre über die IP Adresse, mit der sich mein Computer über den jeweiligen Provider mit dem Internet verbindet. Nicht sehr genau, aber wenn es um die Zeitzone geht – genau genug.
Eine andere Alternative, die mir erst vor kurzem bekannt wurde, ist die Ermittlung des Standortes via HTML 5 – Voraussetzung hierfür sind ein Browser der diese neue Technologie unterstützt und aktiviertes JavaScript.
Die Umsetzung via HTML 5 ist zudem genauer als der Weg über die IP-Adresse. Ideal wäre also die Verknüpfung beider Techniken.
Da meine Seite aktuell mit der IP-Adresse arbeitet, möchte ich das als Basis der folgenden Abhandlungen nehmen.
3. Welche zusätzlichen Informationen kann ich noch anzeigen
Weitere Informationen bietet ASP.NET von allein. IP-Adresse, Sprache, Browser, Auflösung usw.
Die Umsetzung:
Wie schon erwähnt ist ein RSS-Feed nichts anderes als eine XML Datei. Diese wird als Antwort (Response) auf eine Anfrage (Request) vom Webservice zurückgegeben.
Im Prinzip also recht einfach
- Daten ermitteln
- XML erstellen
- Daten zurückgeben
Überlegung:
Welche Daten benötigen wir und welche Daten soll der RSS-Feed zusätzlich enthalten?
Im Prinzip benötigen wir nur drei Werte um die aktuelle Uhrzeit und Zeitzone übermitteln zu können.
- Die Zeitzone als ID – damit kann dann ganz einfach die passende Zeitzone auf dem Windows-PC gefunden werden.
(Wie das auf einem MAC OS oder einem Linux Rechner umgesetzt werden kann wäre eine weitere Überlegung wert) - Die Zeitverschiebung in Stunden (Als Basis gilt hier immer die koordinierte Weltzeit)
- Die Zeitverschiebung in Minuten (Als Basis gilt hier immer die koordinierte Weltzeit)
Alle weiteren Daten sind Beiwerk und blähen unseren RSS-Feed nur auf. Aber wer kommt denn schon ohne Beiwerk aus?
Interessant sind natürlich auch
- Die momentane IP-Adresse
- Die eingestellte Sprache
- Der aktuelle Ort (Stadt) bzw. das Land
- Die Region (hier kommen wir später darauf zurück)
- Die Geokoordinaten (geographische Länge und Breite)
Umsetzung:
Die erste Schwierigkeit besteht darin aus der IP-Adresse den aktuellen Standort zu ermitteln.
Hierfür gibt es eine Menge Webservices, die aber alle nicht sehr aktuell sind und ihre Daten nicht so einfach zur Verfügung stellen wollen.
Als die beste aller Möglichkeiten hat sich die MaxMind GeoLight City Datenbank in Verbindung mit dem Google Maps UserControl von SubGurim.net gezeigt.
Nachteil hier – die Datenbank ist 30MB schwer und muss jeden Monat aktualisiert werden, da sich die IP-Adressen ständig ändern. Was soviel heißt wie, je länger der Monat dauert je ungenauer die Angaben.
Abhilfe würde hier die kostenpflichtige Datenbank schaffen – diese ist zwar noch Größer wird aber in einem kürzeren Zyklus aktualisiert. Da ich das Ganze aber nur als Hobby-Projekt umsetzen will, sind mir die Kosten dafür zu hoch.
Die kostenpflichtige Datenbank gibt es ebenfalls hier (GeoCity) und kann natürlich auch mit dem gleichen UserControl betrieben werden.
Ich hab mich für das Binary Format der Datenbank entschieden, da diese Version in Verbindung mit dem SubGurim UserControl sehr schnell ist. Die Zugriffszeiten auf meinen RSS-Feed liegen bei etwa 0.5 Sekunden.
Das SubGurim Control wurde für ASP.NET 2.0 entwickelt, arbeitet aber auch im Framework 4 ohne Probleme.
Bindet man nun alles ein – ein Lesen der Dokumentation hilft wie immer weiter - reduziert sich der Code zur Ermittlung der Geokoordinaten auf
Dim location As Location = GetLocationFromIP(userIP)
Im Prinzip schon fast alles. Nur war es nicht unser Ziel die Geokoordinaten zu ermitteln, wir wollten die Uhrzeit und Zeitzone am aktuellen Standort ermitteln.
Anmerkung:
Die Klasse Location ist hier ein Teil des SubGurim.Net UserControls und enthält neben den GeoKoordinaten auch die ermittelte Stadt, das Land (beides als Klartext) und die Region als Kürzel (z.B. 02 = Bayern, 05 = Hessen).
Da ich für die nächsten Schritte keine weiteren Controls, Dll’s oder anderes frei verfügbares Werkzeug gefunden habe, musste ich die Verknüpfung der Standortdaten zur Zeitzone und das Decodieren des Regionkürzels selbst erledigen.
Überlegung:
Ein jeder Punkt dieser Welt gehört einer bestimmten Zeitzone an. Hier gilt es 3 Fälle zu beachten.
- Länder die als Ganzes einer Zeitzone angehören (z.B. Deutschland, Österreich, Schweiz)
- Länder die mehreren Zeitzonen angehören (z.B. USA, Russland, Indonesien)
- Gebiete die nicht erfasst werden können (Oceane und Wüsten)
1. Länder die als Ganzes einer Zeitzone angehören
Die einfachste Form, die Inhalte der Klasse Location können direkt zur Ermittlung der Zeitzone herangezogen werden.
Vereinfacht: Land = Zeitzone = Zeitverschiebung
2. Länder die mehreren Zeitzonen angehören
Nicht mehr ganz so einfach. Die Zeitzone wird hier anhand des Landes und der Region bestimmt.
Vereinfacht: Land (z.B. USA) - Region (z.B. CA - Kalifornien) = Zeitzone = Zeitverschiebung
Sonderfall: Eine Region enthält mehrere Zeitzonen (z.B. USA/Indiana – Eastern- und Central Time)
Hierfür habe ich noch keine Lösung – mir fehlen die genauen Daten – aber vermutlich wird es dann eine weitere Unterteilung anhand der ermittelten Stadt geben.
Land (z.B. USA) – Region (z.B. IN - Indiana) – Stadt (?) = Zeitzone = Zeitverschiebung
3. Gebiete die nicht erfasst werden können
Da es auf dem Meer oder in der Wüste keinen Provider bzw. keine Empfang gibt, kann dieser Fall ausgeschlossen werden.
Lösung hierfür wäre eine Ermittlung des Standortes via Geokoordinaten (GPS), was im Moment noch nicht umgesetzt wurde.
Die beschriebene Funktionalität hab ich in eine DLL gepackt, das alles beschreiben zu wollen würde hier aber zu weit führen.
Die Punkte 1. und 2. werden komplett durch eine List(of Structure) abgebildet, die alle Länder dieser Welt, inkl. den unter 2. beschriebenen Fällen, enthält.
Public Structure myTimeZone
Public Country As String
Public Region As String
Public TimeZoneInfo As TimeZoneInfo
Public Sub New(ByVal c As String, ByVal r As String, ByVal tzi As TimeZoneInfo)
Country = c
Region = r
TimeZoneInfo = tzi
End Sub
End Structure
Private mytimezones As New List(Of myTimeZone)
Die Klasse „TimeZoneInfo“ ist schon Bestandteil des Frameworks und bildet die auf dem jeweiligen System installierten Zeitzonen ab.
Die durch das SubGurim.Net UserControl bereitgestellten Informationen (Klasse Location) dienen somit als Filter für die Suche innerhalb der Liste. Die dadurch ermittelten Daten können nun den RSS-Feed füllen.
Erstellen eines RSS-Feeds:
Wie schon erwähnt ist ein RSS-Feed nichts anderes als eine XML-Datei welche durch eine Webseite zurückgegeben wird.
Dies kann auf vielfältige Art und Weiße geschehen.
Anfangs erstellte ich meinen Feed innerhalb einer WebForm (*.aspx). Später jedoch bin ich auf eine Methode gestoßen, die mir eleganter erschien, nämlich das Erstellen eines RSS-Feeds mittels eines „Generic Handlers“ (*.ashx)
Im Prinzip wird nichts anderes gemacht, als den kompletten Feed mittels LINQ zusammenzubauen und am Schluss als Response zurückzugeben.
Dim doc As New XDocument()
Dim rss As New XElement("rss")
rss.Add(New XAttribute("version", "2.0"))
Dim channel As New XElement("channel")
rss.Add(channel)
channel.Add(New XElement("title", "The TimeZoneNotifier RSS-Feed"))
channel.Add(New XElement("link", "http://TimeZoneNotifier.free-file-download.de"))
channel.Add(New XElement("description", "This site detects the timezone you're currently staying in !"))
channel.Add(New XElement("ttl", "60"))
Dim item As New XElement("item")
Channel.Add(item)
item.Add(New XElement("userIP", userIP))
item.Add(New XElement("userBrowser", userBrowser))
item.Add(New XElement("userLanguage", userLanguage))
item.Add(New XElement("userCity", userCity))
item.Add(New XElement("userRegion", userRegion))
item.Add(New XElement("userLongitude", userLongitude))
item.Add(New XElement("userLatitude", userLatitude))
item.Add(New XElement("userTimeOffsetHours", userTimeOffsetHours))
item.Add(New XElement("userTimeOffsetMinutes", userTimeOffsetMinutes))
item.Add(New XElement("CurrentUTCTime", CurrentUTCTime.ToString))
item.Add(New XElement("userTimeZone", userTimeZone))
doc.Add(rss)
(Den RSS-Feed wie er dann ausgegeben wird, sehen Sie am Ende dieses Artikels)
Sind wir erst einmal hier angekommen, wurde die meiste Arbeit schon erledigt. Der Rest besteht nur noch im Anzeigen der ermittelten Daten.
Anzeigen der Daten mittels einer WebForm:
Hier zum Verständnis, noch der Code für die Anzeige der Daten.
Da wir den Feed mittels einer Webanwendung abfragen, sind manche Informationen für den RSS-Feed nicht sichtbar. Bei diesen Daten verwerfen wir die Informationen die der RSS-Feed enthält einfach und füllen die Ausgabe mit den Werten der WebForm.
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.Load
if Postback Then
'RSS-Feed holen
Dim retRss As New Xml.XmlDocument
retRss.Load("http://timezonenotifier.free-file-download.de/rss.ashx?ip=" &amp; _
Request.UserHostAddress)
'IP-Adresse das Feeds stimmt
Me.LabelIP.Text = retRss.SelectSingleNode("/rss/channel/item/userIP").InnerText
'Wurde eine nicht gültige IP-Adresse erkannt -> Anzeigen der Daten abbrechen
If Me.LabelIP.Text = "No valid IP-Adress !!" Then
Exit Sub
End If
'Kein Browser im RSS-Feed vorhanden, da vom Server aus angefragt wird
Me.LabelBrowser.Text = Request.Browser.Browser
'Keine Sprache im RSS-Feed vorhanden, da vom Server aus angefragt wird
Try
Dim ci As New CultureInfo(Request.UserLanguages(0))
Me.LabelLanguage.Text = ci.DisplayName
Catch ex As Exception
Me.LabelLanguage.Text = "Unknown"
End Try
'erkannte Stadt
Me.LabelCity.Text =
retRss.SelectSingleNode("/rss/channel/item/userCity").InnerText
'erkannte Region
Me.LabelRegion.Text =
retRss.SelectSingleNode("/rss/channel/item/userRegion").InnerText
'erkannte Geogr. Länge
Me.LabelLong.Text =
retRss.SelectSingleNode("/rss/channel/item/userLongitude").InnerText
'erkannte Geogr. Breite
Me.LabelLat.Text =
retRss.SelectSingleNode("/rss/channel/item/userLatitude").InnerText
'erkannte Zeitverschiebung
Dim _offsethours As Integer =
CInt(retRss.SelectSingleNode("/rss/channel/item/userTimeOffsetHours").InnerText)
Dim _offsetminutes As Integer =
CInt(retRss.SelectSingleNode("/rss/channel/item/userTimeOffsetMinutes").InnerText)
'aktuelle Zeit am Standort berechnen
Dim tempRSS As String =
retRss.SelectSingleNode("/rss/channel/item/CurrentUTCTime").InnerText
Me.LabelCurrentTime.Text =
CDate(tempRSS).AddHours(_offsethours).AddMinutes(_offsetminutes).ToLongDateString &amp; " - " &amp; CDate(tempRSS).AddHours(_offsethours).AddMinutes(_offsetminutes).ToLongTimeString
Me.LabelCurrentTimeDiff.Text = _offsethours.ToString &amp; _
" h " &amp; _offsetminutes.ToString &amp; " min"
'erkannte Zeitzone
Me.LabelCurrentTimezone.Text =
retRss.SelectSingleNode("/rss/channel/item/userTimeZone").InnerText
Else
'kein Postback
End If
End Sub
Die Ergebnisse meiner Arbeit:
Das oben beschriebene hab ich unter http://timezonenotifier.free-file-download.de als ASP.NET Webseite veröffentlicht.
Den RSS-Feed gibt es unter http://timezonenotifier.free-file-download.de/rss.ashx
RSS-Feed (mein Standort)
<pre>
<rss version="2.0">
<channel>
<title>The TimeZoneNotifier RSS-Feed</title>
<link>http://TimeZoneNotifier.free-file-download.de</link>
<description>
This site detects the timezone you're currently staying in !
</description>
<ttl>60</ttl>
<item>
<userIP>79.249.177.227</userIP>
<userBrowser>Chrome</userBrowser>
<userLanguage>Deutsch (Deutschland)</userLanguage>
<userCity>Mering / Germany</userCity>
<userRegion>Bayern</userRegion>
<userLongitude>10,9833</userLongitude>
<userLatitude>48,2667</userLatitude>
<userTimeOffsetHours>2</userTimeOffsetHours>
<userTimeOffsetMinutes>0</userTimeOffsetMinutes>
<CurrentUTCTime>07.06.2011 09:40:14</CurrentUTCTime>
<userTimeZone>
(UTC+01:00) Amsterdam, Berlin, Bern, Rom, Stockholm, Wien
</userTimeZone>
</item>
</channel>
</rss>
Quellen:
http://www.maxmind.com/app/geolitecity
http://de.googlemaps.subgurim.net/
http://lukencode.com/2010/05/19/ip-to-geo-location-in-asp-net-mvc/
http://www.aspcode.net/Creating-an-RSS-feed-for-your-ASPNET-site.aspx
Copyright:
Die Rechte der verwendeten Datenbanken und des UserControls liegt bei den Autoren.
Der angebotene RSS-Feed darf frei verwendet werden – eine Nennung der Quelle des Feeds bzw. der Adresse dieses Artikels wird vorausgesetzt.
Ein Dank geht an Sebastian Gross der mir einige Anregungen gegeben hat.
Dieser Service bleibt kostenlos, die Bedingungen zur Nutzung des RSS-Feed können sich jedoch jederzeit ändern.
Über den Autor

Michael Bernhard
Hobby-Programmierer von VB.NET, C# und ASP.NET Anwendungen. Hauptberuflich als Inbetriebnehmer bei der Böwe Systec GmbH beschäftigt. Vorzeigbare Projekte – TimeZone Notifier und TSV Neusäß Junioren.
.NET WebServices anlegen und konsumieren
Letzte Woche haben uns angeschaut, wie man unter Java einen WebService anlegen kann und diesen dann auch verwenden (konsumieren), heute gucken wir wie das Ganze unter .NET funktioniert und legen den selben Service auch hier mal an.
HINWEIS: Ich verwende hier Visual Studio 2010 Ultimate, soweit ich weiß können die C#/VB.NET Express Versionen keine WebServices anlegen, dazu solltet ihr den Visual Web Developer Express herunterladen.
Klickt also auf Datei>Neu> Projekt und Wählt "Web" bzw. "Internet" unter C#. Damit euch das WebService Projekt überhaupt vorgeschlagen wird müsst ihr die verwendete Frameworkversion auf 2.0 stellen (Siehe Bild), wählt dann ASP.NET Webdienstanwendung. Benennt das Projekt nun "USerDB" (genauso wie im Java Beispiel) und klickt auf OK.
Wenn das Projekt angelegt ist, solltet ihr ungefähr folgendes sehen:
Im Grunde haben wir hier bereits einen funktionierenden WebService den wir auch schon ausführen können. Ähnlich wie beim Glassfish unter Java bekommen wir dann eine Testseite angezeigt auf der alle Methoden des Dienstes sichtbar sind und getestet werden können:
Bevor wir hier aber weiter machen implementieren wir erst einmal unsere Methode aus dem Java Beispiel:
Wenn wir den Service erneut ausführen und auf der Testseite unsere Funktion wählen bekommen wir ein ähnliches Bild zu sehen wie unter Java:
Auch hier wird anschließend ein SOAP Request für uns erzeugt und an den Service gesendet, dieser wird dann inklusive dem SOAP Response auf der Ergebnisseite angezeigt.
Damit haben wir den WebService bereits fertiggestellt, machen wir uns nun also daran diesen zu konsumieren.
Fügen wir als erstes ein neues Konsolen Projekt zu unserer Projektmappe hinzu (Bild 6). Nachdem das Projekt angelegt ist fügen wir einen neuen Webverweis hinzu (Bild 7). In dem Dialog der nun auftaucht kann man die Quelle für die Schnittstellenbeschreibung (WSDL) wählen. Man kann hier direkt eine Referenz auf das Projekt in der Projektmappe anlegen. Wir wollen aber direkt die URL der WSDL in der Obere Textfeld angeben.
An die WSDL kommt man übrigens sehr einfach, indem man die Testerseite des WebServices aufruft und dort auf den Link namens "Dienstbeschreibung" klickt, diese URL müssen wir hier angeben. Das Tool analysiert die WSDL und zeigt uns eine Übersicht über die Operationen dieses Dienstes an (Bild 8), hier tragen wir noch den gewünschten Namen ein (bei mir "UserDBService") und klicken auf Verweis hinzufügen.
An dieser Stelle hat die IDE für uns den harten Teil schon erledigt und alle nötigen Klassen und Methoden generiert, die nötig sind um unseren WebService zu nutzen. Implementieren wir also unsere Funktion aus dem letzten Artikel:
Die interessanten Stellen habe ich hier markiert, so sollte man nicht vergessen eine Referenz auf unseren WebService zu setzen bevor man loslegt.
Im Vergleich zu Java bekommen wir hier sogar noch mehr fürs Geld, denn in den generierten Klasse finden wir zu unseren Methoden auch schon asynchrone Aufrufe inklusive der Completed-Events:
Und da die Klasse von System.Web.Services.Protocols.SoapHttpClientProtocol erbt, bekommen wir auch alle anderen wichtigen Properties und Funktionen die man in einem web-bezogenen Projekt benötigen könnte
Die generierten Klassen und auch die WSDL findet ihr übrigens im Projektverzeichnis in dem Ordner "Web References".
Führen wir nun unser Programm aus, erwartet uns das Selbe wie in unserem Java Beispiel:
Natürlich muss der Dienst nicht unbedingt in .NET sein, so könntet ihr genauso gut die WSDL aus unserem Java Beispiel verwenden um einen .NET Consumer für einen Java WebService zu erstellen .
Java: Webservice anlegen und konsumieren
Aus gegebenen Anlass gibt es heute mal ein kleines Tutorial wie man in Java ein WebService anlegt und diesen dann benutzt. Das ganze wird mit NetBeans 9.6.1 als IDE und einem Glassfish als Application Server realisiert.
Erstmal kurz: was ist ein WebService?
Ein WebService unter Java ist (vergleichbar mit RMI) eine Art Dienst, der sowohl von anderen Anwendungen als auch von Webseiten genutzt werden kann. Anders als eine gewöhnliche Webseite besitzt ein WebService kein eigenes Benutzerinterface sondern stellt nur in einer Schnittstellenbeschreibung seine Dienste nach außen zur Verfügung.
Diese Schnittstellenbeschreibungen werden in WSDL Dateien (Web Service Description Language) gespeichert, diese sind XML Dokumente, die genau beschreiben, welche Methoden der WebService bietet und wie diese angesprochen werden. Glücklicherweise müssen wir diese WSDL Dateien nicht selbst erstellen sondern können uns diese generieren lassen, aber dazu gleich mehr.
Kommunikation mit dem WebService
Unser WebService wird dann später über das SOAP Protokoll (Simple Object Access Protocol) kommunizieren, dieses ist ebenfalls XML basierend und auch hier müssen wir nicht selbst Hand anlegen sondern lassen alles für uns generieren
In diesem Beispiel werden wir einen einfachen WebService erstellen und eine gewöhnliche App, die dann mit dem WebService kommunizieren wird. Wir werden hier die NetBeans IDE 6.9.1 benutzen, diese wird uns hierbei viel Arbeit abnehmen, sodass der unangenehme Teil der Arbeit klein gehalten wird
Downloads - falls nicht bereits vorhanden
NetBeans - Download
Glassfish - Download
JDK - Download
Legen wir mal los
Als Erstes sollten wir damit starten den WebService zu erstellen. Dazu legen wir ein neues Web Application Projekt an und nennes es "CheckUser" die Restlichen Dialoge könnt ihr unverändert lassen und schließlich auf Fertigstellen klicken.
Nun sollte das Projekt angelegt sein und ihr solltet den Quelltext der index.jsp vor euch haben. Diese Datei benötigen wir nicht da wir einen WebService erstellen wollen, deswegen könnt ihr diese Löschen, wenn ihr mögt.
Nun müssen wir einen neuen WebService anlegen, das machen wir, indem wir auf unser Projekt rechtsklicken und über "New -> WebService" wählen. Nennt den Service "UserDB" und packt es in das "com.bigbasti.db.user.service" Package rein. Zum Schluss macht ihr noch den Haken bei "Implement Web Service as Stateless Session Bean" rein und klickt auf Fertig.
Was ist nun passiert? NetBeans hat für uns nun einen neuen Ordner im Projekt namens "Web Services" angelegt und auch eine neue Enterprise Bean die so heißt wie unser Service ist im Projekt aufgetaucht. NetBeans kümmert sich größtenteils selbst um diese automatisch generierten Beans so sollten wir erst einmal die Finger davon lassen.
Nun solltet ihr die UserDB.java vor euch aufgeklappt sehen, diese Klasse ist mit der Annotation Stateless und WebService markiert. Da ein WebService mindestens eine Methode (Operation besitzen muss wird euch wahrscheinlich ein Fehler angezeigt. Das wollen wir nun ändern und fügen dem Service eine neue Operation hinzu.
Macht dazu einen Rechtsklick innerhalb der UserDB Klasse und klickt auf "Insert Code..." wählt hier nun "Add Web Service Operation...". In dem nun angezeigten Fenster solltet ihr eure Operation "checkUserName" nennen und den Rückgabetyp auf Boolean stellen. Desweiteren fügt ihr einen Parameter hinzu und tauft ihn "userName".
Bestätigt das Fenster mit "OK" und ihr solltet folgendes zu sehen bekommen:
Diese neue Methode ist mit "WebMethod" annotiert und weist so darauf hin, dass auf diese von außen zugegriffen werden darf. Alle Parameter sind ebenfalls annotiert sodass diese zugreifbar sind. Diese Annotation ist wichtig, damit wir später WSDLs und Requests generieren können. Beim Arbeiten mit dieser Methode könnt ihr diese aber für euch ausblenden
Diese erzeugte Methode funktioniert genauso wie jede andere Java Methode, somit könnt ihr mit ihr wie gewohnt arbeiten. Diese Methode soll für uns prüfen, ob ein Benutzername bereits von jemandem verwendet wird. Da wir das simel halten wollen nutzen wir eine LinkedList als Datenbank
So können wir eine einfache Prüffunktion implementieren:
Nun haben wir einen WebService mit einer Operation. Diese wollen wir nun mal testen. Dazu verwenden wir zunächst die Test-Tools vom Glassfish Server. Dazu müssen wir unsere Web Application zunächst Deployen (Veröffentlichen), das machen wir ganz einfach indem wir das Projekt rechts-klicken und dann "Deploy" wählen. Nun wird der Glassfish Server gestartet, die Anwendung kompiliert und auf dem Server veröffentlicht.
Wenn dieser Vorgang erfolgreich war könnt ihr im "Services"-Tab unter "Servers->GlassFish Server 3" euere Web App sehen (Siehe Bild 11). Nun wollen wir mal gucken ob unsere Operation funktioniert, dazu macht ihr einen Rechtsklick auf dem "GlassFish Server 3" und wählt View Admin Console (Bild 12).
Nun könnt ihr euch einen Kaffee holen...
Wenn die Admin Console irgendwann offen ist, und es kann dauern, klickt ihr auf "Anwendungen" und wählt eure "CheckUser" App. In dem "Allgemein"-Tab seht ihr nun unten eine Tabelle mit allen Komponenten eurer App, sucht hier unseren Service "UserDB" und klickt auf "Endpunkt anzeigen".
Hier seht ihr nun alle wichtigen Informationen zu eurem Web Service, für uns ist im Moment die Tester URL wichtig (Roter Pfeil) Diese öffnen wir mal in einem neuen Tab.
Was wir hier sehen ist die Testseite für unseren Dienst. Hier hat der Glassfish für jede von uns erstellte Operation (momentan haben wir nur eine) eine Testmethode erstellt. Diese Testmethode macht nichts anderes, als eine SOAP Nachricht zu generieren und an unseren Dienst zu senden und uns die Antwort von diesem anzuzeigen.
Probieren wir das Ganze mal aus, probiert einfach mal ein paar Namen durch und verwendet auch ab und zu einen der von uns Reservierten Namen, ihr werdet feststellen, dass unsere Methode super funktioniert.
Dabei zeigt das Test-Tool auch an welche SOAP Requests es generiert und welche Antworten unser Service gibt, so wird folgender SOAP Aufruf getätigt um zu prüfen, ob der Name vorhanden ist:
Hier finden wir alle unsere Werte aus der Klasse wieder. Der Name der Operation die wir mit diesem Request aufrufen wollen (Rot), diese Operation benötigt einen Parameter namens "userName" den wir hier angeben (Blau) und der Wert für diesen Parameter (Orange). Das Ganze ist dann noch in einen SOAP Envelope verpackt und wird so zum Server geschickt.
Dieser führt die gewünschte Operation aus und gibt uns eine Antwort:
Die Response ist weitestgehend genauso aufgebaut die der Request. Der Knoten hat nun noch "Response" an den Namen der Methode/Operation angehängt bekommen (Rot) und auch das Ergebnis der Operation ist angegeben (Blau).
Das Ergebnis können wir nun mit Hilfe eines Parsers auslesen, aber dazu gleich mehr. Nach dem Test wissen wir nun, dass unser dienst läuft und auch dass die Operation zuverlässig arbeitet, Zeit unsere "Consumer"-Applikation zu bauen.
Web Service benutzen (konsumieren)
Nun haben wir also einen Web Service laufen, und natürlich wollen wir diesen nun auch nutzen (konsumieren ist das Fachwort), aus einer App mit der sich Benutzer irgendwo registrieren können. Diese App muss natürlich irgendwie prüfen, ob der gewünschte Benutzername schon vorhanden (bzw. bereits benutzt) ist, das wird die App über unseren Web Service UserDB erledigen.
Legen wir zunächst ein neues Projekt in NetBeans an, diesmal eine normale Java Application, die wir "RegisterUser" nennen.
Nun müssen wir unserer frischen Applikation beibringen mit einem WebService zusammen zuarbeiten. Das macht man über so genannte Schnittstellen, denn diese beschreiben genau was möglich ist und wie man da dran kommt. Schnittstellen haben hier das Format WSDL, und wenn ihr gut aufgepasst habt habt ihr sicher schon gesehen dass der GlassFish für unser Service eine WSDL erstellt hat.
Geht die Schritte in den Bildern 13,14 und 15 durch (falls ihr den Tab schon geschlossen habt), sodass ihr wieder die URLs des Services angezeigt bekommt. Der Blaue Pfeil im Bild 15 zeigt euch die URL der WSDL, diesen link klickt ihr bitte an, sodass ihr die XML Datei zu Gesicht bekommt.
Auf den Inhalt werde ich hier nicht weiter eingehen, das könnt ihr euch ja bei Wiki durchlesen
Die URL zu der WSDL sieht bei mir wie folgt aus:
http://localhost:8080/UserDBService/UserDB?wsdl
Diese könnt ihr mal in die Zwischenablage kopieren, da wir diese gleich benötigen um einen Web Service Client zu erstellen. Das machen wir mit einem Rechtsklick auf das Projekt und wählen "New->Web Service Client". Hier wählt ihr als Bezugsort für die WSDL "URL" aus und fügt die URL ein. Als Paketnamen habe ich "com.bigbasti.user.register" gewählt.
NetBeans lädt nun die WSDL herunter und erzeugt daraus für euch die nötigen Klassen für den Zugriff auf diese Schnittstelle, deswegen sind einige neue Dateien bei euch im Projektordner aufgetaucht (Bild 24). Der etwas ausgegraute Ordner "Generated Sources" enthält hierbei die wichtigen Klassen und Methoden für das Erzeugen von SOAP Requests und für das Lesen von Responses. Diese Dateien solltet ihr auch möglichst nicht anfassen.
Nun sind wir komplett gerüstet um unseren Web Service zu nutzen. Der Code der dafür geschrieben werden muss ist auch nicht weltbewegend:
Als Erstes sind die imports wichtig, damit wir Zugriff auf die generierten Klassen haben. Mit denen können wir nun (Blaue Pfeile) die nötigen Instanzen der Service Objekte anlegen. Ist das erst geschafft können wir mit unserem UserDB Objekt, das den Service repräsentiert bequem arbeiten und die Service Operationen aufrufen, als ob es eine gewöhnliche kleine Klasse wäre (Oranger Pfeil).
Nachdem wir unsere Operation aufgerufen haben bekommen wir eine Antwort ob der Name schon benutzt wird oder nicht. So könnte es aussehen wenn man das obige Programm startet:
Wie man sieht funktioniert das Ganze sehr gut und es hat gerade mal ein paar Minuten gedauert diesen Service zu erstellen. Ihr könnt natürlich selber weiter herumspielen und weitere komplexere Operationen hinzufügen, die mit echten Datenbanken kommunizieren.













































