21 janvier 2014

avc-binding-dom et polymorphisme

On a des cas où une structure XML a évolué et où l’on aimerait pouvoir accéder aux contenus des différentes versions à travers une unique interface Java. Pour cela, avc-binding-dom permet de déclarer des sous-interfaces et suit un polymorphisme.

Prenons un exemple, avec une interface Java candidate à récupérer des informations d’un fichier xib (Apple iOS) :
public interface Xib { boolean hasUseAutoLayout(); }
Les deux versions du XML qu’on veut savoir traiter renseignent l’information « useAutoLayout » de deux façons différentes.
Ici avec Xcode 5.0.2 :
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="4514" systemVersion="12F45" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES"> ... </document>
Ici avec Xcode 4.6.3 :
<archive type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="8.00"> <data> ... <int key="IBDocument.defaultPropertyAccessControl">3</int> <bool key="IBDocument.UseAutolayout">YES</bool> <string key="IBCocoaTouchPluginVersion">2083</string> </data> </archive>
Écrivons des interfaces de binding pour ces deux versions :
@XPath("/document[@version = '3.0']") public interface Xib3 extends Xib { @Override @XPath("@useAutolayout = 'YES'") boolean hasUseAutoLayout(); }
et :
@XPath("/archive[@version = '8.00']") public interface Xib8 extends Xib { @Override @XPath( "data/bool[@key = 'IBDocument.UseAutolayout'] = 'YES'") boolean hasUseAutoLayout(); }


Pour lire des documents XML de versions différentes, on utilise les sous-interfaces pour le binding effectif, en ne déclarant pour l’accès aux données qu’une seule variable du type de la super-interface :
Xib xib; xib = DomBinderUtils.xmlContentToJava(new File( "MyViewController_iPhone_3_0.xib"), Xib3.class); assertTrue(xib.hasUseAutoLayout()); xib = DomBinderUtils.xmlContentToJava(new File( "MyViewController_iPhone_8_0.xib"), Xib8.class); assertTrue(xib.hasUseAutoLayout());
Maintenant, ajoutons des méthodes pour lire des sous-structures contenues dans le fichier xib :
public interface Xib { boolean hasUseAutoLayout(); IBUIView getIBUIViewByTag(int tag); // e.g. 1104 interface IBUIView { String getFrame(); // e.g. "{{82, 77}, {43, 43}}" String getUserLabel(); // e.g. "LButton 20" } }

Évidemment, les expressions XPath ne sont pas du tout les mêmes pour les deux structures XML.

Pour Xcode 5.0.2 :
@XPath("/document[@version = '3.0']") public interface Xib3 extends Xib { @Override @XPath("@useAutolayout = 'YES'") boolean hasUseAutoLayout(); @Override @XPath("//*[@tag = $arg0]") IBUIView3 getIBUIViewByTag(int tag); interface IBUIView3 extends Xib.IBUIView { @Override @XPath("concat('{{', rect[@key = 'frame']/@x, ', '" + ", rect[@key = 'frame']/@y, '}, {'" + ", rect[@key = 'frame']/@width, ', '" + ", rect[@key = 'frame']/@height, '}}')") String getFrame(); // e.g. "{{82, 77}, {43, 43}}" @Override @XPath("@userLabel") String getUserLabel(); // e.g. "LButton 20" } }
Et pour Xcode 4.6.3 :
@XPath("/archive[@version = '8.00']") public interface Xib8 extends Xib { @Override @XPath( "data/bool[@key = 'IBDocument.UseAutolayout'] = 'YES'") boolean hasUseAutoLayout(); @Override @XPath("//object[int[@key = 'IBUITag'] = $arg0] ") IBUIView8 getIBUIViewByTag(int tag); interface IBUIView8 extends Xib.IBUIView { @Override @XPath("string[@key = 'NSFrame']") String getFrame(); // e.g. "{{82, 77}, {43, 43}}" @Override @XPath("//object[@class = 'IBObjectRecord'" + " and reference[@key = 'object']/" + "@ref = $this/@id]/" + "string[@key = 'objectName']") String getUserLabel(); // e.g. "LButton 20" } }
Quel que soit le binding initial de l’objet « xib », on pourra valider le code suivant grâce au polymorphisme :
final IBUIView view = xib.getIBUIViewByTag(1104); assertEquals("{{130, 100}, {128, 128}}", view.getFrame()); assertEquals("Bouton OK", view.getUserLabel());

Passons maintenant à de l’héritage qui ajoute des fonctionnalités :


Dans notre exemple, les interfaces IBUIButton dérivent des interfaces IBUIView, en leur ajoutant la méthode « getNormalTitle() », dont l’annotation @XPath dépend encore une fois de la version du document XML.

avc-binding-dom permet alors d’écrire ceci :
final IBUIView view = xib.getIBUIViewByTag(1104); final IBUIButton button = BinderUtils.rebind( view, IBUIButton8.class); assertEquals("OK", button.getNormalTitle());
Eh oui, les relations d’héritage dans notre diagramme ne permettent pas de remonter à IBUIButton8 à partir de « Xib8.getIBUIViewByTag(int): IBUIView8 ». Il faut faire une sorte de transtypage grâce à la méthode rebind().
Noter que d’autres possibilités de transtypage existent, par exemple la méthode self(Class<?>) quand l’interface de binding dérive explicitement de Binding<Node>, mais rebind() est une solution qui fonctionne dans tous les cas.

Pour que le transtypage fonctionne de façon transparente avec des interfaces génériques, c’est-à-dire si on voulait pouvoir écrire « rebind(view, IBUIButton.class) » au lieu de « rebind(view, IBUIButton8.class) », il faudrait plutôt que « Xib8.getIBUIViewByTag(int) » renvoie directement un IBUIButton8 et non un IBUIView8.
Le diagramme devient alors :


Et le code :
final IBUIView view = xib.getIBUIViewByTag(1104); // ni rebind(), ni IBUIButton8, ni IBUIButton3 final IBUIButton button = (IBUIButton) view; assertEquals("OK", button.getNormalTitle());

Voilà :-)

Edit : pour le dernier exemple, je montre qu’on peut utiliser un cast, plus simple qu’un rebind().


Note : les bouts de code et les exemples de cet article sont inclus dans les tests unitaires de avc-binding-dom.