Die technischen Fortschritte der letzten Zeit, wie schnelle Prozessoren und Datenübertragung, bessere Displays sowie weitere Miniaturisierung, haben es möglich gemacht, dass Anwendungen, die auf einer erweiterten Realität basieren, auf einem Smartphone oder einem Tablet-Computer laufen können. Solche Anwendungen sind dabei, in die alltägliche Welt vorzudringen, und momentan vor allem für die Bereiche Marketing, Verkauf und Werbung konzipiert. Viele sind auch einfach Spielerei, die das Potential der neuen Technik demonstrieren wollen.

In der Industrie gibt es ernstzunehmende Anwendungen mit erweiterter Realität schon seit längerer Zeit. Allerdings musste bislang einiger Aufwand für die Hardware betrieben werden, wie z.B. spezielle Kamerasysteme, Tracking-Sensoren, Displays oder Brillen. Deshalb waren die Anwendungen auf sehr spezielle Bereiche beschränkt.

Stellen Sie sich eine iOS App vor, die ein HTML5 Video bereitstellt, das Sie abspielen und stoppen können, und mit dem Sie interagieren können. Das ist genau das, was wir in diesem Tutorial zusammenbauen wollen.

HTML

Da nur kleine Änderungen notwendig sind, werden wir damit beginnen, den HTML/JS-Code zu bearbeiten, der in diesem Artikel bereitgestellt wird. Zuerst verbinden wir ein JavaScript onclick-Attribut mit jedem der Elemente, die

umgeben. Dazu nehmen wir die setupItems()-Funktion:

var t = $(items[i][3]);

Beachten Sie, dass das Encoding notwendig ist, weil der String oben als Funktionsparameter gegeben ist.

Wie Sie sehen können, wird Overlay.call() mit der entsprechenden Item-ID aufgerufen, wenn Sie auf eine Box klicken. Call() arbeitet ähnlich zu der Art und Weise, wie wir windows.location in einem vorangegangenen Artikel verwendet haben, um Daten an die iOS parent app zu schicken.  Es nimmt die ID und leitet die Seite weiter zu so etwas wie ?cmd=call¶m=2. Diese Zeichenfolge kann später von der iOS-App gelesen werden.

Das ist es auch schon! Starten Sie nun Xcode und erzeugen Sie eine neue, view-based Applikation. Nennen Sie es App Solut InteractiveVideoInterface, und öffnen Sie Ihre ViewController header Datei. Fügen Sie folgenden Code ein:

@interface App_Solut_InteractiveVideoInterfaceViewController : UIViewController {

IBOutlet UIWebView *webView;
IBOutlet UILabel *label;
}
@property(retain, nonatomic) UIWebView *webView;
@property(retain, nonatomic) UILabel *label;

@end

Speichern Sie die Datei, wechseln Sie zum Interface Builder und bearbeiten Sie die xib Datei des ViewControllers. Ziehen Sie ein WebView und ein Label auf dem Bildschirm und verbinden Sie die Outlets, die soeben erzeugt wurden. Dazu ziehen Sie (während Sie die Strg-Taste gedrückt halten) aus dem WebView Objekt zum Besitzer der Dateien und wählen Sie delegate. Dann das gleiche umgekehrt: ziehen Sie (während Sie die Strg-Taste gedrückt halten, oder nehmen Sie die rechte Maustaste, wenn Sie eine Standardmaus benutzen) vom Besitzer der Dateien zum  WebView und zum Label und wählen Sie die entsprechenden Einträge aus der Liste. Sie haben nun die Verbindung zwischen Ihrem Code und Ihrem Interface geschaffen.

In Ihrer Implementierungs-Datei führen Sie nun ein synthesize für WebView und Label durch:

@synthesize webView;
@synthesize label;

So wie beim WebView Interface-Code fügen wir die folgende Funktion hinzu, um alle Webpage Requests abzufangen.

- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
NSString *url = [[request URL] absoluteString];
NSArray *urlArray = [url componentsSeparatedByString:@"?"];
NSString *cmd = @"";
NSMutableArray *paramsToPass = nil;
if([urlArray count] > 1) {
NSString *paramsString = [urlArray objectAtIndex:1];
NSArray *urlParamsArray = [paramsString componentsSeparatedByString:@"&"];
cmd = [[[urlParamsArray objectAtIndex:0] componentsSeparatedByString:@"="] objectAtIndex:1];
int numCommands = [urlParamsArray count];
paramsToPass = [[NSMutableArray alloc] initWithCapacity:numCommands-1];
for(int i = 1; i < numCommands; i++){
NSString *aParam = [[[urlParamsArray objectAtIndex:i] componentsSeparatedByString:@"="] objectAtIndex:1];
[paramsToPass addObject:aParam];
}
}
if([cmd compare:@"call"] == NSOrderedSame) {
NSString *message = [[paramsToPass objectAtIndex:0]stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
[label setText:[NSString stringWithFormat:@"You just clicked button #%@", message]];
}
/* Only load the page if it is not an appcall */
NSRange aSubStringRange = [url rangeOfString:@"appcall"];
if(aSubStringRange.length != 0){
NSLog(@"App call found: request cancelled");
return NO;
} else {
return YES;
}
}

Sie sehen, dass der Wert des param Parameters mit einem String verbunden ist, der als Text des Labels benutzt wird. In diesem Szenario schicken alle Video-Overlays ihre eigene ID zur App, wenn auf sie geklickt wird.

Von diesem Punkt an ist Ihre Fantasie frei. Sie können auf den Benutzerinput reagieren und tun, was Sie wollen.

Beispielprojekt herunterladen

Sie finden das Beispielprojekt bei GitHub: InteractiveVideoInterface-Example.

Ein WebView-Element ist eine schnelle und einfache Art, sowohl plattformunabhängige als auch dynamische Inhalte in eine gewöhnliche iOS App zu integrieren. Nun ist es nicht schwierig, das WebView-Element zu benutzen, um Webseiten anzuzeigen, aber die Dinge werden komplizierter, wenn eine Interaktion zwischen der Parent App und der Webseite erforderlich ist. Im Moment gibt es keinen gangbaren Weg für eine Webseite, Nachrichten an den WebView Controller zu senden. Dieser Artikel zeigt, wie man trotz dieser fehlenden Funktion eine Zwei-Wege-Kommunikation implementieren kann. Am Ende des Artikels finden Sie einen Download-Link zu einem vollständigen iOS Beispielprojekt.

JavaScript Code von Ihrer App aus aufrufen

Dieser Teil ist einfach: Nehmen wir an, in Ihrem Projekt benutzen Sie ein WebView-Element für die Anzeige der Seiten. Es gibt eine Methode namens stringByEvaluatingJavaScriptFromString, die die gewünschte Funktionalität bereitstellt. Beispiel:

[myWebView stringByEvaluatingJavaScriptFromString:@"alert('Hello World!');"];

Diese Methode – Sie ahnen es – zeigt eine Warnmeldung, die von Ihrer Webseite geschickt wurde. Auf diese Weise können Sie alle JavaScript-Funktionen aufrufen, die Ihrer Webseite von außen zur Verfügung stehen. Damit ist es ein Kinderspiel, Seiten ohne großen Aufwand zu ändern.

In Ihrer App Funktionen vom WebView-Element aus aufrufen

Der umgekehrte Weg ist weitaus komplizierter. Wir müssen eine Methode finden, wie eine Webseite mit einer App kommunizieren kann. Glücklicherweise bietet das WebView-Element einige hilfreiche Attribute und Ereignisse. Zunächst einmal wird ein Ereignis namens shouldStartLoadWithRequest ausgelöst, wenn eine Seite angefordert wird. Hier ist ein Beispiel für die Implementierung:

-(BOOL)myWebView:(UIWebView*)myWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNaviagtionType)navigationType {
   [...]
}

Was diese Funktion so interessant macht ist, dass sie aufgerufen wird, bevor die Seite tatsächlich geladen wird, und dass sie einen booleschen Wert als Rückgabe erwartet. Der Rückgabewert YES veranlasst das WebView-Element, die gewünschte Seite zu laden, während der Rückgabewert NO keine Seite lädt. Der Plan ist wie folgt: Eine JavaScript-Funktion lädt eine fiktive Seite einschließlich einiger GET-Parameter über window.location. Durch das spezielle URL Stichwort ist die App in der Lage herauszufinden, ob eine echte Seite geladen werden soll oder ob eine In-App Funktion aufgerufen wird (App-Call). An diesem Punkt kann die Funktion entweder den Aufruf stoppen und die Aktion durchführen oder die Seite laden. Die Funktion erledigt also zwei Dinge:

  • Sie prüft, ob ein normaler Link oder ein App-Aufruf angefordert wird und gibt den entsprechenden booleschen Wert zurück.
  • Sie holt die gegebenen Parameter und überprüft, ob sie zu den vordefinierten Aktionen passen.

Basierend auf dem Beispiel von tetontech ist dies der Code, über den wir sprechen:

- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
 NSString *url = [[request URL] absoluteString];
 NSLog(@"Requesting: %@",url);
 NSArray *urlArray = [url componentsSeparatedByString:@"?"];
 NSString *cmd = @"";
 NSMutableArray *paramsToPass = nil;
 // isolate command and parameters
 if([urlArray count] < 1){
 NSString *paramsString = [urlArray objectAtIndex:1];
 NSArray *urlParamsArray = [paramsString componentsSeparatedByString:@"&"];
 cmd = [[[urlParamsArray objectAtIndex:0] componentsSeparatedByString:@"="] objectAtIndex:1];
 int numCommands = [urlParamsArray count];
 paramsToPass = [[NSMutableArray alloc] initWithCapacity:numCommands-1];
 for(int i = 1; i < numCommands; i++){
 NSString *aParam = [[[urlParamsArray objectAtIndex:i] componentsSeparatedByString:@"="] objectAtIndex:1];
 [paramsToPass addObject:aParam];
 }
 }
 if([cmd compare:@"toggleWorking"] == NSOrderedSame){
 NSLog(@"Turning working indicator...");
 if([UIApplication sharedApplication].networkActivityIndicatorVisible == NO) {
 [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
 NSLog(@"...on");
 } else {
 [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
 NSLog(@"...off");
 }
 } else if([cmd compare:@"logMessage"] == NSOrderedSame) {
 NSString *message = [[paramsToPass objectAtIndex:0] stringByReplacingOccurrencesOfString:@"%20" withString:@" "];
 NSString *message = [[paramsToPass objectAtIndex:0] stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
 NSLog(@"Received JS message: %@",message);
 }
 // only load the page if it is the initial index.html file
 NSRange aSubStringRange = [url rangeOfString:@"index.html"];
 if(aSubStringRange.length != 0){
 return YES;
 } else {
 NSLog(@"App call found: request cancelled");
 return NO;
 }
}

Wie schon gesagt, extrahiert und isoliert diese Funktion jeden gegebenen Parameter und Befehl und überprüft mit einigen if-Anweisungen, ob ein Befehl erkannt wird. Wenn das der Fall ist, können zusätzliche Parameter verwendet werden, um eine Aktion auszuführen. In diesem Beispiel gibt es nur zwei mögliche Aktionen:

  • Mit der Anforderung ?cmd=toggleWorking wird in der Tab-Leiste des iPad - abhängig von seinem gegenwärtigen Zustand - ein sich drehendes Rädchen ein- oder ausgeschaltet.
  • Die Aktion logMessage kann aufgerufen werden mit: ?cmd=logMessage¶m=Hello%20World. Es wird eine Nachricht als log-Datei an die Debug-Konsole übermittelt.
NSRange aSubStringRange = [url rangeOfString:@"index.html"];
if(aSubStringRange.length != 0){

Die letzte Überprüfung ist sehr wichtig! Angenommen, Ihre Hauptseite heißt index.html, wird dadurch sicher gestellt, dass die Seite einmal beim Start geladen wird. Weitere Anforderungen sind im Moment blockiert, aber Sie kennen sicherlich andere Ansätze, um sicherzustellen, dass App-Aufrufe nicht geladen werden.

Beispielprojekt herunterladen

Gehen Sie zu GitHub und laden sie das WebViewInterface-Example herunter. Quelle:

Ob man nun einer App sein eigenes Layout zu verleihen will oder Design-Vorgaben einhalten muss, oft ist notwendig, vom Apple-Standard-Design abzuweichen.
In diesem Fall geht es um UITableViewCells mit runden Ecken. Das kann z.B. so aussehen:

UITableViewCells with round corners

So eine Vorgabe lässt sich mit einer eigenen UITableViewCell Klasse und den Core Graphics einfach lösen.

Wir öffnen unser XCode-Projekt und fügen eine  neue Datei vom Typ UITableViewCell hinzu (*.m und *.h) und nennen sie z.B. „RoundedTableViewCell“.

Damit wir die Core Graphics zu Verfügung haben, müssen wir das QuarzCore Framework in der *.h-Datei importieren:

#import 

Nicht vergessen: Das Framework muss auch den „frameworks“ des Projekts hinzugefügt werden:

add QuartzCore FRamework to your project add QuartzCore framework

In der RoundedTableViewCell.h-Datei definieren wir ein CALayer für jede Ecke der Zelle. Diese kleinen Quadrate „überdecken“ später die abgerundeten Ecken, wo gewünscht.

CALayer* topleft;
CALayer* topright;
CALayer* bottomleft;
CALayer* bottomright;
CALayer* bglayer;
...
@property (nonatomic, retain) CALayer* topleft;
@property (nonatomic, retain) CALayer* topright;
@property (nonatomic, retain) CALayer* bottomleft;
@property (nonatomic, retain) CALayer* bottomright;
@property (nonatomic, retain) CALayer* bglayer;

Außerdem brauchen wir zwei BOOLsche Variablen..

BOOL roundTop;
BOOL roundBottom;
...
@property (nonatomic) BOOL roundTop;
@property (nonatomic) BOOL roundBottom;

… und zwei Methoden, um bei der einzelnen Zelle runde Ecken oben und/ oder unten anzuzeigen:

-(void) drawRoundTop;
-(void) drawRoundBottom;

Das war’s mit der *.h-Datei. In der RoundedTableViewCell.m Datei werden nun die runden Ecken implementiert. Zunächst legen wir einen Eck-Radius und eine Hintergrundfarbe für die Tabellenzelle fest (zwischen #import und @implemetation):

#define rad 15  // radius
#define normalColor [UIColor colorWithRed:0.39 green:0.15 blue:0.24 alpha:1.0]  // cell background color, dark red

Man beachte: Kein Strichpunkt hinter #define-Angaben!

Im @implementation-Block fehlt noch das @synthesize unserer CALayers and BOOLschen Variablen. Das release können wir uns bei diesen Datentypen netterweise sparen.

@synthesize topleft;
@synthesize topright;
@synthesize bottomleft;
@synthesize bottomright;
@synthesize roundTop;
@synthesize bglayer;
@synthesize roundBottom;

In der initWithStyle-Methode definieren wir ein UILabel mit ein paar Style-Attributen:

// add label
 label = [[UILabel alloc] initWithFrame:self.contentView.frame];
 label.textAlignment = UITextAlignmentLeft;
 label.backgroundColor = [UIColor clearColor];
 label.textColor = [UIColor whiteColor];
 label.font = [UIFont fontWithName:@"Zapfino" size:16];
 [self.contentView addSubview:label];

// initial values
 roundTop = NO;
 roundBottom = NO;
// set layer background color (= cell background color)
 self.layer.backgroundColor = normalColor.CGColor;

In der gleichen Methode setzen wir noch die Standardwerte, und zwar ohne runde Ecken, sowie die Hintergrundfarbe:

roundTop = NO;
roundBottom = NO;
self.layer.backgroundColor = normalColor.CGColor;

Um die runden Ecken zu zeichnen, brauchen wir unsere eigene drawRect Methode. Dort werden auch die CALayer für jede Ecke erzeugt, beides abhängig vom oben definierten Radius:

- (void) drawRect:(CGRect)rect{
	CGRect fr = rect;

	fr.size.width = fr.size.width-2*rad;
	fr.size.height = fr.size.height-1;
	fr.origin.x = rad;

	// draw round corners layer
	bglayer = [CALayer layer];
    bglayer.backgroundColor = normalColor.CGColor;
	bglayer.cornerRadius = rad;
	bglayer.frame = fr;
	bglayer.zPosition = -5;	// important, otherwise delete button does not fire / is covered
	[self.layer addSublayer:bglayer];

	// set label size (adjust to heightForRowAtIndexPath..)
	label.frame = CGRectMake(rad, 5, fr.size.width, fr.size.height);

	// corner layer top left
	topleft = [CALayer layer];
    topleft.backgroundColor = normalColor.CGColor;
	CGRect tl = CGRectMake(rad, 0, rad, rad);
	topleft.frame = tl;
	topleft.zPosition = -4;
	if(roundTop){
		topleft.hidden = YES;
	}else {
		topleft.hidden = NO;
	}
    [self.layer addSublayer:topleft];

	// corner layer top right
	topright = [CALayer layer];
    topright.backgroundColor = normalColor.CGColor;
	topright.frame = CGRectMake(fr.size.width, 0, rad, rad);
	topright.zPosition = -3;
	if(roundTop){
		topright.hidden = YES;
	}
	else {
		topright.hidden = NO;
	}
    [self.layer addSublayer:topright];

	// corner layer bottom left
	bottomleft = [CALayer layer];
    bottomleft.backgroundColor = normalColor.CGColor;
	bottomleft.frame = CGRectMake(rad, fr.size.height-rad, rad, rad);
	bottomleft.zPosition = -2;
	if(roundBottom){
		bottomleft.hidden = YES;
	}else {
		bottomleft.hidden = NO;
	}
    [self.layer addSublayer:bottomleft];

	// corner layer bottom right
	bottomright = [CALayer layer];
	bottomright.backgroundColor = normalColor.CGColor;
	bottomright.frame = CGRectMake(fr.size.width, fr.size.height-rad, rad, rad);
	bottomright.zPosition = -1;
	if(roundBottom){
		bottomright.hidden = YES;
	}else {
		bottomright.hidden = NO;
	}
    [self.layer addSublayer:bottomright];

	[super drawRect:rect];

}

Jetzt fehlen nur noch die beiden Methoden, um die Tabellenzelle oben oder unten mit runden Ecken darzustellen:

-(void) drawRoundTop{
roundTop = YES;
topleft.hidden = YES;
topright.hidden = YES;
}

Für die Unterseite:

-(void) drawRoundBottom{
roundBottom = YES;
bottomleft.hidden = YES;
bottomright.hidden = YES;
}

So, unsere UITableViewCell Klasse ist jetzt gebrauchsfertig. Also öffnen wir unsere UITableViewController.m-Datei (oder legen eine neue an). In der viewDidLoad Methode muss zumindest die erste  Zeile des folgenden Codes implementiert werden, sonst überdeckt der standardmäßige Tabellenhintergrund unsere neuen Zellen:

// important! without this line it does not work!
[self.tableView setBackgroundView:[[[UIView alloc] init] autorelease]];
// set plain background color
 [self.tableView setBackgroundColor:[UIColor colorWithRed:0.69 green:0.81 blue:0.79 alpha:1.0]];
// remove seperator color
 self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
 [self.tableView setSeparatorColor:[UIColor clearColor]];

Hier im Beispiel wurden zwei Sections definiert, eine mit „normalen“ und eine mit runden Tabellenzellen, die wie ein seperater Block erscheinen sollen. Der Zellentyp und die jeweiligen Zellenattribute werden in der cellForRowAtIndexPath Methode festgelegt:

if (indexPath.section == SECTION_NORMAL) {

 static NSString *CellIdentifier = @"Cell";

 NormalTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
 if (cell == nil) {
 cell = [[[NormalTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
 }
 cell.label.text = @"normal cell";
 return cell;

 } else if (indexPath.section == SECTION_ROUNDED) {
// define round cell
 static NSString *CellRoundedIdentifier = @"RoundedTableViewCell";
 RoundedTableViewCell *cell = (RoundedTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellRoundedIdentifier];
 if (cell == nil) {
 cell = [[[RoundedTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellRoundedIdentifier] autorelease];
 }

 // Configure the cell.
 [cell.label setText:[NSString stringWithFormat:@"my round cell No.%d",(indexPath.row+1)]];
 cell.selectionStyle = UITableViewCellSelectionStyleNone;
 // draw round top corners in first row
 if(indexPath.row == 0){
 [cell drawRoundTop];
 }
// draw round corners in last row
if (indexPath.row == [self.tableView  numberOfRowsInSection:indexPath.section]-1) {
 [cell drawRoundBottom];
 }

 return cell;
 }

Um einen Abstand zwischen den einzelnen Tabellenzellen zu erzuegen, verwenden wir die heightForHeaderInSection und die viewForHeaderInSection Methoden:

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

 UIView* header = nil;
 if(section == SECTION_ROUNDED){
 CGRect rect = CGRectMake(0, 0, self.tableView.frame.size.width, [self tableView:(tableView) heightForHeaderInSection:section]);
 header = [[UIView alloc] initWithFrame:rect];

 }
 return header;
}

- (CGFloat) tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
 CGFloat h = 0;
 if(section == SECTION_ROUNDED){
 h = 15;
 }
 return h;
}

Das XCode-Beispielprojekt kann als zip-Archiv von github heruntergeladen werden.

Bei der Entwicklung von Anwendungen für das Android OS kommt es vor, dass allgemeine Activities oder Services mit wenig Änderungen in weiteren Anwendungen wieder verwendet werden könnten. Da Sie diese Klassen nicht in jedes einzelnen Projekt kopieren möchten – was zu einem kaum handhabbarem Code führen würde – wäre es besser, einen Weg zu finden, den Quellcode dieser Klassen aus weiteren Projekten heraus zu referenzieren. Für Nicht-Android-Anwendungen kann man das Problem lösen, indem man den Code in mehrere Bibliotheken aufteilt, so dass die gewünschte Funktionalität von mehreren Projekten referenziert werden kann. Solange keine Interaktion mit externen Ressourcen nötig ist, ist das auch in Android Projekten ohne weiteres möglich (erstellen Sie ein jar und referenzieren Sie es in den Projekten). Aber wenn Sie mit externen Ressourcen arbeiten, können diese Klassen nicht innerhalb einer Bibliothek verwendet werden, weil das Android SDK keine passenden IDs innerhalb der „R“-Klasse für externe Ressourcen erzeugt, die in einer referenzierten jar-Datei enthalten sind.

In diesem Beitrag möchte ich Ihnen verschiedene Lösungen für dieses Problem zeigen, wodurch Sie vermeiden können, den Quellcode in die Projekte zu kopieren.

Vor etwa einem Jahr haben wir beschlossen, die Bean Validation API zu testen, und am Anfang waren wir begeistert über die einfach zu bedienende, Annotation-basierte Schnittstelle. Doch als wir Cross-field Validierung durchführen wollten, mussten unseren Eindruck revidieren. Leider bietet die Bean Validation API keinen einfachen Weg der Validierung eines Felds basierend auf dem Wert eines anderen Feldes. Da wir im Einklang mit dem API-Design bleiben wollten, sind wir den steinigen Weg gegangen und unsere haben eigene Class-level Validation Annotations und eigene Validators geschrieben.

Das ist leider eine langweilige, sich wiederholende Arbeit, die die Benutzung der Reflection API erfordert. Solche Aufgaben sollten möglichst weit vereinfacht und automatisiert werden. Deshalb begann ich zu erforschen, wie andere dieses Problem gelöst haben. In diesem Artikel skizziere und analysiere ich einige der Lösungen, auf die ich gestoßen bin.

Der Standardansatz

Das Erstellen einer neuen Validierungslogik besteht aus zwei Schritten. Es müssen eine Constraint Annotation und ein Validator, der die Validierungslogik enthält, geschrieben werden. Der Einfachheit halber werden wir einen Constraint auf Class-level implementieren. Er wird verwendet, um sicherzustellen, dass der Wert eines Feldes größer ist als der Wert eines anderen Feldes des Objekts, das validiert werden soll.

Dies ist die Definition der Annotation:

/**
* Compares two of the fields of an object.
*/
@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = GreaterThanValidator.class)
public @interface GreaterThan {

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String field();

    String greaterThan();

    /**field > greaterThan*/
    Class<? extends Comparator<Object>> comparator();

    @Target({ TYPE, ANNOTATION_TYPE })
    @Retention(RUNTIME)
    @interface List {
        GreaterThan [] value();
    }
}

Die “field” und “greaterThan” Felder enthalten die Namen der Bean-Eigenschaften, die verglichen werden sollen. Für diesen Vergleich brauchen wir auch einen Comparator. Schließlich fügen wir ein “Liste” Feld hinzu, so dass wir mehr als eine Instanz der Annotation auf Class-level nutzen können. Diese Definition der Annotation bedeutet bereits eine Menge Arbeit für die bescheidene Aufgabe des Vergleichs von zwei Werten. Das ist aber gar nichts im Vergleich zur Definition des Validators:

/**
* Validates the GreaterThan constraint
*/
public class GreaterThanValidator implements
ConstraintValidator<GreaterThan, Object> {

    /** field > greaterThan ? */
    private String field;
    private String greaterThan;
    private Comparator<Object> comparator;

    /**
    * This will be called before isValid to initialize the validator
    */
    @Override
    public void initialize(GreaterThan ann) {

        field = ann.field();
        greaterThan = ann.greaterThan();
        Class<? extends Comparator<Object>> comparatorClass = ann.comparator();
        try {
            comparator = comparatorClass.newInstance();
        } catch (Exception e) {
            throw new IllegalArgumentException("Can't instantiate comparator",e);
        }
    }

    /**
    * This will be called to validate an object
    */
    @Override
    public boolean isValid(Object validateThis, ConstraintValidatorContext ctx) {
        if (validateThis == null) {
            throw new IllegalArgumentException("validateThis is null");
        }
        Field fieldObj = null;
        Field greaterThanObj = null;
        // Find getters the properties
        try {
            fieldObj=validateThis.getClass().getDeclaredField(field);
        } catch (Exception e) {
            throw new IllegalArgumentException("Invalid field name",e);
        }
        try {
            greaterThanObj = validateThis.getClass().getDeclaredField(greaterThan);
        } catch (Exception e) {
            throw new IllegalArgumentException("Invalid greaterThan name",e);
        }
        if (fieldObj == null || greaterThanObj == null) {
            throw new IllegalArgumentException("Invalid field names");
        }

        try {
            fieldObj.setAccessible(true);
            greaterThanObj.setAccessible(true);
            // get field values
            Object fieldVal = fieldObj.get(validateThis);
            Object largerThanVal = greaterThanObj.get(validateThis);
            // compare
            return fieldVal == null && largerThanVal == null
            || fieldVal != null && largerThanVal != null
            && comparator.compare(fieldVal, largerThanVal) > 0;
        } catch (Exception e) {
            throw new IllegalArgumentException("Can't validate object", e);
        }

    }

}

Eine Menge Arbeit für einen einfachen größer-als-Vergleich, und es kommt nicht einmal produktiv einsetzbarer Code heraus, müssten Sie doch noch ein paar Zeilen für die Verwendung in Ihren eigenen Projekten hinzuzufügen. Natürlich könnte man sich Arbeit sparen, indem Sie einige der Details in Superklassen einfügen, aber es gibt schnellere Wege, um dieselben Ergebnisse zu erzielen. Ein weiteres Problem mit dieser Annotation ist, dass sie keine compile-time Sicherheit bietet. Tippfehler in den Feldnamen oder die Angabe der falschen Comparator-Klasse führen zu Laufzeitfehlern.

Der folgende Ansatz löst einige dieser Probleme. Er ist aus einer Forumsdiskussion zum Thema entstanden (siehe http://stackoverflow.com/questions/1972933/cross-field-validation-with-hibernate-validator-jsr-303).

Der @AssertTrue Ansatz

Die @AssertTrue Annotation ist eine der Annotations der Bean Validation API, die auch für Methoden anwendbar sind. Sie stellt sicher, dass der Rückgabewert der annotierten Methode true ist, wenn sie während der Validierung aufgerufen wird. Wir fügen also einfach eine öffentliche Methode namens isConstraint1Satisfied hinzu und annotieren sie wie folgt:

public class AssertTrueExample {

public int i=0;
public int j=0;

@AssertTrue(message="Constraint 1 is not satisfied")
public boolean isConstraint1Satisfied()
{
return i==j;
}
}

Auf diese Weise können wir unser Objekt validieren, ohne zu in Schweiß auszubrechen. Mal sehen, was die Autoren der JSR-303 Spezifikation dazu zu sagen haben.

“Validieren von Daten ist eine allgemeine Aufgabe, die sich in einer Anwendung von der Präsentations-Schicht bis zur Persistenz-Schicht stellt. Oft sind die gleichen Validierungslogiken in jeder Schicht implementiert, was zeitaufwändig und fehleranfällig ist. Um diese Probleme zu vermeiden, bündeln Entwickler oft die Validierungslogik direkt in das Domain-Modell, und stopfen Domain-Klassen mit Validierungscode voll, der im übrigen nichts anderes darstellt als Metadaten über die Klasse selbst. […]“

Nun, die @AssertTrue Lösung verstößt offensichtlich gegen diese Trennung von Belangen (separation of concerns), um die es in der Bean Validation API geht, und stellt die Validierungslogik in die Model-Klasse. Sie sind vielleicht nicht einverstanden damit, aber meiner Meinung nach ist oft zu wenig durch die Trennung von Belangen gewonnen, wenn es um interne Abhängigkeiten geht. Manchmal ist es sogar schädlich. Ich gebe Ihnen ein Beispiel.

In einem unserer Projekte mussten wir die gültigen Parameter für den Aufruf einer Legacy-Statistik Bibliothek in ein Objekt kapseln. Die Gültigkeit der Parameter für diesen Aufruf ist stark abhängig von dem Wert anderer Parameter, und es gab keine Möglichkeit, diese Constraints aufzuteilen in separate wiederverwendbare Constraint Annotations. Wir erzeugten eine Annotation und einen Validator, die wir einmal benutzten; es gab darüberhinaus keine Chance, diese Annotation jemals wieder zu verwenden.

Die Autoren der JSR-303 Spezifikation sind der Meinung, dass Validierungslogik im Domain-Model oft oder immer Unordnung bedeutet, aber ich würde zögern, diese besondere Art von Validierungscode so zu nennen. Sicher, Validierungscode für eine E-Mail-Adresse und ähnliches kann man als Unordnung bezeichnen, da E-Mail-Adressen reichlich in den meisten IT-Systemen vorkommen, und jeder weiß, was eine E-Mail-Adresse ist und wie sie aussieht; daher enthält die Validierungslogik wenig Informationen für jemanden der den Code liest. Die Validierungslogik für die Parameter-Klasse ist aber ein ganz anderes Thema. Sie gehört zur Model-class, ist nicht wieder verwendbar und enthält viele wichtige Informationen. Sie ist mehr oder weniger die Klasse, und deshalb macht es wenig Sinn, es in eine Annotation zu extrahieren. Ich denke, der @AssertTrue Ansatz (und auch der @ScriptAssert Ansatz) passt für diese Verwendung viel besser als jeder andere Ansatz, dem ich bis jetzt begegnet bin.

Der @ScriptAssert Ansatz

Hibernate Validator 4.1 brachte uns die @ScriptAssert Class-level Annotation. Sie ist eine geniale Anwendung der Java Scripting API. Statt dass Sie Ihren Validierungscode in Java schreiben, können Sie eine der vielen unterstützten Skriptsprachen verwenden. Dieser Ansatz teilt mehr oder weniger die Stärken und Schwächen des @AssertTrue Ansatzes, ist aber prägnanter und hat den zusätzlichen Vorteil, das Interface der Klasse nicht mit Validierungsmethoden zu verstopfen. Das letzte Beispiel zeigt, wie es wäre, wenn wir die @ScriptAssert Annotation nutzen würden:

@ScriptAssert(lang="javascript",script="_this.i==_this.j")
public static class ScriptAssertExample
{

int i=0;
int j=0;

}

Der große Nachteil verglichen mit dem @AssertTrue Ansatz ist natürlich, dass der Gebrauch einer Skriptsprache den Verlust der compile-time Sicherheit bedeutet.

Fazit

Der wichtigste Aspekt der drei Ansätze für Cross-field Validierung, die hier betrachtet wurden, ist, wohin sie die Validierungslogik setzen; in die Model-class oder in eine externe Validierungsklasse. In diesem Artikel habe ich in erster Linie umrissen, warum es eine schlechte Idee ist, jedes Mal einen Validator und eine Annotation zu kreieren, wenn die Annotationen der Bean Validation API nicht Ihren Bedürfnissen entsprechen. Aber verstehen Sie mich nicht falsch, ich sage nicht, dass das Hinzufügen der Validierungslogik in die Model-class in jedem Fall eine gute Idee ist. Es gibt mindestens zwei Dinge, die berücksichtigt werden müssen:

  • Ist es unwahrscheinlich, dass die Validierungslogik wiederverwendet werden kann?
  • Enthält sie wichtige Informationen, die unbedingt in die Model-class gehören?

Ich denke, wenn Sie eine dieser Fragen mit “Ja” beantworten, können Sie die Validierungslogik tatsächlich in die Model-class einfügen, ohne ein schlechtes Gewissen zu haben.

Im nächsten Artikel werde ich einen hybriden Ansatz präsentieren, der auf dem Strategiemuster basiert. Dieser Ansatz ermöglicht eine einfache Implementierung neuer wiederverwendbarer Constraints, ohne dass ein Validator für einen Constraint nötig ist. Er lässt Sie wählen, wo Sie die Validierungslogik implementieren wollen, und ermöglicht es Ihnen, nicht nur auf Typen, sondern auch auf Felder zu zielen.