1 Faculteit Toegepaste Wetenschappen Vakgroep Informatietechnologie Voorzitter: prof. dr. ir. P. Lagasse Onderzoeksgroep Broadband communication netwo...
Faculteit Toegepaste Wetenschappen Vakgroep Informatietechnologie Voorzitter: prof. dr. ir. P. Lagasse Onderzoeksgroep Broadband communication networks Hoofd: prof. dr. ir. P. Demeester
Automatische generatie van GUIs voor het beheer van gedistribueerde software architecturen
door Tom Verdickt
Promotoren: prof. dr. ir. P. Demeester en prof. dr. ir. B. Dhoedt Thesisbegeleiders: ir. F. De Turck en ir. S. Vanhastel Afstudeerwerk ingediend tot het behalen van de academische graad van burgerlijk ingenieur in de computerwetenschappen
Academiejaar 2001 - 2002
Faculteit Toegepaste Wetenschappen Vakgroep Informatietechnologie Voorzitter: prof. dr. ir. P. Lagasse Onderzoeksgroep Broadband communication networks Hoofd: prof. dr. ir. P. Demeester
Automatische generatie van GUIs voor het beheer van gedistribueerde software architecturen
door Tom Verdickt
Promotoren: prof. dr. ir. P. Demeester en prof. dr. ir. B. Dhoedt Thesisbegeleiders: ir. F. De Turck en ir. S. Vanhastel Afstudeerwerk ingediend tot het behalen van de academische graad van burgerlijk ingenieur in de computerwetenschappen
Academiejaar 2001 - 2002
Woord vooraf Langs deze weg wil ik graag iedereen bedanken die een bijdrage heeft geleverd aan het tot stand komen van deze scriptie: • prof. dr. ir. P. Demeester en prof. dr. ir. B. Dhoedt, de promotoren van deze scriptie • ir. F. De Turck en ir. S. Vanhastel voor de dagdagelijkse begeleiding • mijn ouders en Sara Van de Velde voor het nalezen en corrigeren • iedereen die me op een bepaald moment met goede raad verder geholpen heeft
Toelating tot bruikleen De auteur geeft de toelating deze scriptie voor consultatie beschikbaar te stellen en delen van de scriptie te kopi¨eren voor persoonlijk gebruik. Elk ander gebruik valt onder de beperkingen van het auteursrecht, in het bijzonder met betrekking tot de verplichting de bron uitdrukkelijk te vermelden bij het aanhalen van resultaten uit deze scriptie.
Tom Verdickt, 28 mei 2002
i
Automatische generatie van GUIs voor het beheer van gedistribueerde software architecturen door Tom Verdickt
Afstudeerwerk ingediend tot het behalen van de academische graad van burgerlijk ingenieur in de computerwetenschappen Academiejaar 2001-2002 Universiteit Gent Faculteit Toegepaste Wetenschappen Vakgroep Informatietechnologie Voorzitter: prof. dr. ir. P. Lagasse Promotoren: prof. dr. ir. P. Demeester en prof. dr. ir. B. Dhoedt Thesisbegeleiders: ir. F. De Turck en ir. S. Vanhastel
Samenvatting In deze scriptie wordt de ontwikkeling besproken van een Visual Builder Tool die toelaat om op een grafische, intu¨ıtieve manier GUIs te cre¨eren die dienen als CORBA-clients. De GUIs zullen opgebouwd worden uit JavaBeans. De applicatie zal gebruik maken van de compiler ontwikkeld door Giovanni Mels in het kader van zijn scriptie (academiejaar 1999-2000). Deze compiler is in staat om GUIs te genereren uitgaande van een XML-beschrijving ervan. De hier ontwikkelde Visual Builder Tool zal dus XML gebruiken om de geconstrueerde GUIs voor te stellen. Voor het opzoeken van de te gebruiken CORBA-servers en de functionaliteit ervan, zal gebruik gemaakt worden van de Naming Service en de Interface Repository van CORBA.
Grafisch beheren van de bean-bibliotheek . . . . . . . . . . . . . . . . . . . . . . 55 iv
INHOUDSOPGAVE 7 Besluit
56
v
Hoofdstuk 1
Inleiding 1.1
Probleemstelling
Deze thesis bouwt voort op de thesis van Giovanni Mels [5]. Daarin heeft hij een compiler ontwikkeld die een grafische gebruikersinterface (GUI) opbouwde voor een CORBA-server, aan de hand van een XML-beschrijving van die GUI. Traditioneel is het schrijven van GUIs als CORBA-clients redelijk omslachtig. Je moet al een programmeur zijn die JAVA, CORBA en swing kent, en zelfs zeer eenvoudige toepassingen vergen al snel enkele honderden of zelfs duizenden lijnen code. In dat opzicht is de compiler van Giovanni Mels een hele verbetering. De JAVA-code wordt automatisch gegenereerd uitgaande van een relatief korte XML-file (slechts een paar tientallen lijnen XML) met een zeer eenvoudige syntax. Tegelijk wordt ook de tijd die nodig is voor het maken van de client, zeer sterk teruggeschroefd. Toch kan (zeker voor grote GUIs) het opstellen van de XML-beschrijving nog vrij omslachtig worden. Het doel van deze thesis is dan ook het bouwen van een applicatie, die toelaat om op grafische wijze snel GUIs te construeren. Het genereren van de XML-beschrijving wordt vervolgens door de applicatie zelf gedaan, waarna die XML-beschrijving verwerkt wordt door de compiler (zie figuur 1). Het construeren van een GUI door de gebruiker zal gebeuren in twee fasen, die echter door elkaar kunnen lopen. Eerst zal de gebruiker een aantal componenten in het te construeren venster moeten plaatsen. Dit kan hij doen door middel van een eenvoudige drag-and-drop procedure. Zoals bij de compiler zijn alle componenten die de GUI kan bevatten JavaBeans. 1
1.2 Extra specificaties
Figuur 1: Het construeren van een client
Daarna zal de gebruiker moeten specifi¨eren welke data de beans moeten voorstellen. Dat zal gebeuren door bepaalde vooraf vastgelegde methodes van de JavaBeans (inputs en outputs) te associ¨eren met methodes en attributen van CORBA-servers. Het concept van een dergelijk programma is niet nieuw. Het bestaat reeds in de zogenaamde Visual Builder Tools (VBT), waarmee je op een grafische manier GUIs en zelfs volledige programma’s kan maken, uitgaande van een bibliotheek van JavaBeans. Er zijn verschillende Visual Builder Tools op de markt, in allerhande vormen, met verschillende extra’s . . . In de meeste VBTs kunnen events van verschillende beans met elkaar verbonden worden, vergelijkbaar met het verbinden van de output van een bean met de input van een andere bean in deze applicatie. Wat in de commercieel beschikbare VBTs echter ontbreekt, is de mogelijkheid om de beans hun data te laten halen uit CORBA-objecten, en dus CORBA-clients te maken.
1.2
Extra specificaties
Vooraleer met de eigenlijke implementatie begonnen werd, werden er eerst een aantal extra specificaties vastgelegd, waaraan de applicatie moest voldoen. De meeste van deze voorwaarden zijn zeer vanzelfsprekend, maar het is misschien interessant ze toch expliciet te vermelden. • WYSIWYG (What You See Is What You Get): De gebruiker moet steeds kunnen zien hoe de GUI onder constructie er zal uitzien en de uiteindelijk resulterende GUI moet zo exact mogelijk overeenkomen met wat de gebruiker ziet in de GUI builder (want dat is deze applicatie uiteindelijk). Dit is een noodzakelijke voorwaarde om tot een bruikbare applicatie te komen. • minimale kennis van de gebruiker : Er werd naar gestreefd om de gebruiker tot zo weinig mogelijk voorkennis te verplichten. Dat impliceert onder andere dat de gebruiker niet mag hoeven te weten wat de exacte naam van een functie van een CORBA-servant is en 2
1.3 CORBA welke parameters die precies nodig heeft. Ook de syntax van de gebruikte XML-files moet verborgen blijven voor de gebruiker. • onafhankelijkheid van de bean-bibliotheek : De compiler die gebruikt wordt laat toe om heel eenvoudig nieuwe beans te gebruiken in een GUI. Een kleine aanpassing van de XML-files volstaat. Dit moet bij deze applicatie ook zo zijn. Er mag geen enkele kennis omtrent de beans hard gecodeerd worden. • onafhankelijkheid van de CORBA-servants: Analoog aan het voorgaande, maar dan voor de CORBA-servants. De applicatie moet steeds correct werken, welke servants er ook in het netwerk zitten. Enkel de aanwezigheid van de Naming Service en Interface Repository (zie sectie 1.3) mag ondersteld worden. • gebruiksvriendelijkheid: Dit is een evidente eis voor elke applicatie die enige interactie met een gebruiker heeft. Deze doelstellingen zijn meer dan gewoon maar idee¨en voor goed ontwerp. Het is noodzakelijk om ze zo goed mogelijk te bereiken, om de uiteindelijke applicatie bruikbaar te maken. Een groot deel van de ontwerpsbeslissingen is dan ook genomen om aan ´e´en of meerdere van deze doelstellingen tegemoet te komen.
1.3
CORBA
CORBA (Common Object Request Broker Architecture) is een object-model dat toelaat om operaties aan te roepen op objecten, die zich eender waar in een netwerk bevinden, alsof ze lokaal zijn voor de applicatie die de aanroep doet. CORBA verbergt dus de juiste plaats in het netwerk waar een object zich bevindt en laat een transparante client-server-architectuur toe op een taal- en platformonafhankelijke manier. Eerst wordt de interface tussen de client en de server gespecifieerd in IDL (Interface Definition Language). Uit deze IDL-interface wordt dan aan de client- en serverzijde een interface (of abstracte klasse) gegenereerd in een willekeurige programmeertaal (bv. C++ of JAVA). Dit zorgt voor de taalonafhankeljkheid. Vervolgens implementeert men aan de serverzijde een CORBA-object, een implementatie van de gegenereerde interface. Dit object wordt een servant genoemd. Wanneer nu een client operaties, gespecifieerd in de interface, op het server-object wil aanroepen, zal eerst lokaal een zogenaamd proxy-object gecre¨eerd worden. Alle aanroepen van operaties op 3
1.3 CORBA het server-object zullen dan uitgevoerd worden op het proxy-object, dat de aanroepen delegeert naar het server-object via de ORB (Object Request Broker ). Bij een aanroep op het lokale proxyobject zal de ORB de waarden van de argumenten doorsturen naar de server, die ze gebruikt om er de gewenste operatie mee uit te voeren. De eventuele returnwaarde en outputparameters volgen daarna de omgekeerde weg terug naar de client. Aangezien alle communicatie verborgen blijft voor de client, zal die dus de indruk hebben dat hij werkt op het lokale proxy-object. Om de ORB toe te laten functies van de servant aan te roepen, wordt die laatste met de ORB verbonden door middel van een object-adapter, tegenwoordig over het algemeen een POA (Portable Object Adapter ). De ORB en de POA zullen dus alle communicatie tussen de client en de server afhandelen, zoals ge¨ıllustreerd in figuur 2.
Client
Proxy
ORB POA Server
Figuur 2: CORBA
Twee heel belangrijke services die bij CORBA bestaan, zijn de Naming Service en de Interface Repository. De meeste CORBA-implementaties bevatten servers die deze services aanbieden. De Naming Service zal hier even kort besproken worden. De Interface Repository komt iets uitgebreider aan bod, omdat die een grotere rol speelt in het vervolg van deze scriptie.
1.3.1
Naming Service
Servants worden in CORBA ge¨ıdentificeerd aan de hand van een referentie (IOR genoemd). Die referenties zijn echter te ingewikkeld om door de gebruiker te laten onthouden. Daarbij komt 4
1.3 CORBA nog dat het herstarten van een server volstaat om hem een andere refeentie te geven, waardoor alle clients van die servant moeten aangepast worden. De Naming Service is hier de oplossing. Het principe van de Naming Service is vergelijkbaar met dat van DNS (Domain Name System) bij TCP/IP. Het laat toe om servants te lokaliseren uitgaande van hun naam. Op die manier kan een client gebruik maken van een servant zonder te weten waar in het netwerk de servant zich exact bevindt, of wat zijn referentie juist is. De client doet gewoon een resolve() met de juiste naam bij de Naming Service en krijgt een referentie naar de gevraagde servant terug.
1.3.2
Interface Repository
De Interface Repository is een soort databank van servant-interfaces. Wanneer een IDL-file bij de Interface Repository geregistreerd wordt, wordt die IDL-file geparst en opgeslagen. Applicaties kunnen vervolgens de Interface Repository ondervragen om gegevens over de interface van een zekere servant te weten te komen, zoals welke functies de servant aanbiedt, wat de parameters van een bepaalde functie zijn en welke types ze hebben . . . , kortom alle informatie die zich in de IDL-file bevindt. Het ondervragen van de Interface Repository gebeurt op een onrechtstreekse manier. De gebruiker moet eerst een referentie verkrijgen naar de servant waarover hij meer informatie wil. Dit kan bv. gebeuren met behulp van de Naming Service. Vervolgens roept hij op het zopas verkregen object de methode get interface def() aan. Het zal dan het object zelf zijn dat contact maakt met de Interface Repository en daar de gegevens over zichzelf ophaalt. Hiervoor is het wel nodig dat de server op een of andere manier de locatie van de Interface Repository kent (bv. door die als command-line parameter mee te geven). De methode get interface def() retourneert een object van de klasse InterfaceDef, waarmee de beschrijving van de interface gemanipuleerd kan worden. Wanneer men op dat object vervolgens de methode describe interface aanroept, verkrijgt men een object van de klasse FullInterfaceDescription. De attributen hiervan bevatten alle nuttige informatie, zoals informatie over de methodes van de server (attribuut operations), de attributen van de server (attributes), de naam (name) en het type (type). De attributen operations en attributes zijn lijsten van beschrijvingen van methodes en attributen. Elk van die beschrijvingen is ook van een gestructureerd type, waarvan de members verdere info verstrekken, zoals de parameters van een fuctie, het returntype ervan, het type van een attribuut, etc. 5
1.4 XML
1.4
XML
XML (Extensible Markup Language) is een markup-taal voor documenten die gestructureerde informatie bevatten. Je zou XML een vereenvoudigde subset van SGML (Specialized General Markup Language) kunnen noemen. Zoals SGML en het meer bekende HTML, werkt XML met tags (in XML elementen genoemd) om de structuur van het document weer te geven. In HTML liggen de syntax en de semantiek van de tags echter vast. Bij XML is dat niet zo. XML is eigenlijk een soort meta-taal waarin je markup talen kan ontwerpen. Je specifieert eerst een formele syntax (DTD’s of document type definitions), die de tags en de structurele relaties ertussen definieert. Vervolgens kan je dan documenten schrijven die aan die syntax voldoen. De semantiek van de syntax wordt dan bepaald door de applicatie die de documenten verwerkt. In deze applicatie (en in de compiler van Giovanni Mels) worden XML-documenten gebruikt om alle persistente data bij te houden, zoals opgeslagen GUIs, beschrijvingen van JavaBeans . . .
1.5
JavaBeans
JavaBeans zijn platformonafhankelijke, herbruikbare softwarecomponenten. Het zijn JAVAklassen die ontworpen zijn volgens bepaalde ontwerpsregels. De bedoeling van die regels is toelaten om beans te manipuleren in Visual Builder Tools (VBTs), waarvan het programma, beschreven in deze thesis, een voorbeeld is. Beans hebben een aantal belangrijke eigenschappen: • Properties: In de meest eenvoudige vorm is een property een instantie-variabele die kan gemanipuleerd worden via een get- en een set-method. De exacte naam van deze methods en de vorm van hun parameterlijsten is vastgelegd, zodat ze herkend kunnen worden door een VBT. Het wijzigen van een property zal dan gebeuren door het aanroepen van de overeenkomstige set-method. Een voorbeeld van een mogelijke property is de kleur van de curve in een grafiek-bean. • Persistentie: Alle beans moeten de Serializable-interface implementeren, waardoor de toestand van een bean kan opgeslagen worden en later kan hersteld worden. • Events: Beans genereren events om met elkaar te communiceren. Zo kan een bean bv. melden dat ´e´en van zijn properties veranderd is. De meeste VBTs bieden de gebruiker de 6
1.5 JavaBeans mogelijkheid om rechtstreeks te specifi¨eren naar welke events van welke bean een bepaalde bean juist moet luisteren en hoe hij daarop moet reageren.
7
Hoofdstuk 2
Beschrijving van de GUI-compiler 2.1
Werking
De compiler (gui2java genoemd) gaat uit van drie XML-bestanden (het eigenlijke bronbestand, resources.xml en convert.xml) en genereert daaruit de GUI. Het bronbestand beschrijft zowel de interface van de CORBA-servers als de inhoud en de lay-out van de GUI die moet geconstrueerd worden. De compiler zal het bronbestand omzetten in JAVA-source-code. Om uitvoerbare JAVA-code te bekomen, moet de verkregen source-code eerst nog gecompileerd worden met een JAVA-compiler (javac). De bekomen klassen vormen dan de gevraagde client voor de CORBA-servers. Merk wel op dat in die klassen geen main-functie voorkomt. Er moet dus apart nog een hoofdprogramma geschreven worden dat de bekomen klassen instantieert en op het scherm toont. Het XML-bestand wordt niet rechtstreeks naar pure JAVA-code vertaald. De vertaling gebeurt in termen van een aantal runtime-klassen, waar een groot deel van de functionaliteit van alle GUIs in zit. Dat maakt het werk van de compiler heel wat eenvoudiger.
2.2
Gemaakte aanpassingen
Omdat de compiler oorspronkelijk geschreven was voor een oudere versie van ORBacus dan degene die voor deze thesis gebruikt werd, was het nodig om een aantal kleine aanpassingen te doen in de compiler om hem met de nieuwe versie te laten werken. Tegelijk werden er ook een
8
2.3 Syntax van het XML-bronbestand aantal lichte aanpassingen aan de functionaliteit gedaan om hem geschikter te maken voor een grafische front-end. De belangrijkste aanpassingen zijn de volgende: • De GUI kan nu meerdere CORBA-servants tegelijk gebruiken. In de XML-file kunnen dus meerdere -elementen voorkomen. • CORBA-servants worden opgezocht via de Naming Service, in plaats van rechtstreeks gebruik te maken van hun host en poortnummer. • De genereerde GUIs gebruiken nog steeds GridBagLayout, maar componenten kunnen nu meerdere kolommen en rijen daarvan overspannen, wat meer algemene GUIs toelaat.
2.3
Syntax van het XML-bronbestand
Zoals reeds vermeld heeft de compiler 3 XML-bestanden nodig om een GUI te genereren. Een beschrijving van de bestanden resources.xml en convert.xml is te vinden in de thesis van Giovanni Mels [5]. De syntax van het bronbestand heeft echter een aantal aanpassingen ondergaan. Het is dan ook nuttig om de vernieuwde syntax hier volledig te beschrijven. Het is immers nog steeds mogelijk om manueel geschreven XML-files te laten compileren. Die moeten dan wel voldoen aan de vernieuwde syntax. Een paar opmerkingen in verband met de syntax-beschrijving: • Sommige attributen in de XML-elementen stellen JAVA-datatypes voor. Dit kunnen zowel klassenamen als primitieve datatypes (int, long, char, . . . ) zijn. Klassenamen moeten bereikbaar zijn langs het classpath. Het is aangeraden om klassen te vermelden met hun volledige naam, dus inclusief de naam van het package waartoe de klasse behoort, bv. java.lang.String i.p.v. String. De applicatie van deze thesis gebruikt steeds de volledige naam in de XML-files. • Voor de eenvormigheid is hier de vorm van de syntax-beschrijving van Giovanni Mels overgenomen. Meer concreet betekent dit: – Bij elk attribuut staat vermeld of het verplicht of optioneel is. Bij sommige optionele attributen wordt een default-waarde vermeld. – Bij elementen die uit een begin- en een eindtag bestaan, wordt de tekst tussen de twee elementen voorgesteld door een niet-terminaal symbool X. Er wordt dan een productieregel gegeven die aangeeft welke elementen X kan voorstellen.
9
2.3 Syntax van het XML-bronbestand • Wanneer in de beschrijving van een attribuut gesproken wordt over ”de gebruiker van de compiler”, wordt daarmee diegene bedoeld die de XML-file opstelt. Dat kan een persoon zijn, wanneer de XML-files manueel opgesteld worden, maar binnen het kader van deze thesis zal dat steeds de hier beschreven applicatie zijn. • Sommige elementen worden wel ondersteund door de compiler, maar niet door deze applicatie. In de beschrijving zal dat bij die elementen steeds aangegeven zijn. Hier volgt de volledige beschrijving van de syntax van het bronbestand: X
X → interface* window
Dit is het startelement en eindelement van de GUI beschrijving. Attributen: geen X
X → (data|method)* tabledata*
Dit is het startelement en eindelement van de interface-beschrijving. Attributen: • type (verplicht): het type van de door JIDL gegenereerde JAVA-interface, eventueel voorafgegaan door een package-naam. • name (verplicht): de naam waaronder het server-object geregistreerd werd bij de Naming Service, waarbij de verschillende componenten van de naam gescheiden worden door een ’/’. Dit object moet een implementatie zijn van de interface opgegeven in het attribuut type. X
X → field*
Dit element beschrijft een IDL-attribute. IDL-attributen worden vertaald naar een get- en een set-method in de gegenereerde interface. Attributen: • attribute (verplicht): de naam van het attribuut. In de interface moet een get- en een set-method voorkomen met deze naam. • type (verplicht): het type van het attribuut, eventueel voorafgegaan door een packagenaam. De get-method heeft dit type als return-type. De set-method neemt een object van dit type als enig argument.
10
2.3 Syntax van het XML-bronbestand • update (”10000”): de periode, in milliseconden, waarmee het attribuut moet worden uitgelezen met de get-method. • readonly (”false”): geeft aan of het attribuut enkel kan gelezen worden of niet. Mogelijke waarden zijn true of false. Indien de waarde true wordt opgegeven, wordt er enkel naar een get-method gezocht. • id (verplicht): een naam die de gebruiker van de compiler aan dit attribuut geeft, waarmee de waarde van dit attribuut kan ge¨ıdentificeerd worden. Dit element beschrijft de mogelijke publieke velden in een attribuut, als het attribuut gestructureerd is, zoals bijvoorbeeld een struct in een IDL-beschrijving. Attributen: • name (verplicht): de naam van het veld. • type (verplicht): het type van het veld, eventueel voorafgegaan door de package-naam. • id (verplicht): een naam waarmee de waarde van dit veld ge¨ıdentificeerd kan worden. <method> X
X → call*
Deze elementen starten en be¨eindigen de beschrijving van een get- of een set-method, die een onderdeel is van de interface. Om de XML-files compacter te maken, is het mogelijk om in ´e´en method-element zowel een get- als een set-method te specifi¨eren. Dit is vooral handig om het werk te beperken bij manueel gegenereerde XML-files. Attributen: • get (optioneel): de naam van de get-method. • set (optioneel): de naam van de set-method. • datatype (verplicht): het return-type van de get-method, of het type van de waarde die toegekend moet worden door de set-method. • paramtypes (verplicht): de parametertypes van de get-method. Indien de get-method meerdere parameters heeft, worden de parametertypes gescheiden door komma’s, in de volgorde van de declaratie in de get-method in de interface. Voor de set-method is dit zeer analoog, maar hierbij wordt verondersteld dat ze nog een extra parameter heeft met als type, het type opgegeven in het attribuut datatype. Deze parameter volgt in de interface de parameters opgegeven in paramtypes. 11
2.3 Syntax van het XML-bronbestand • id (verplicht): een naam die door de gebruiker van de compiler gekozen wordt, waarmee deze methods kunnen ge¨ıdentificeerd worden. Dit element wordt gebruikt om de get-method, opgegeven in een method-element, periodiek aan te roepen met de opgegeven argumenten. Attributen: • update (”10000”): de periode, in milliseconden, waarmee de get method moet worden aangeroepen. • args (verplicht): de argumenten die moeten gebruikt worden bij het aanroepen van de get-method. Indien de get-method meerdere parameters heeft, worden de argumenten opgegeven, gescheiden door komma’s, in de volgorde van de declaratie van de get-method in de interface. Het aantal argumenten moet gelijk zijn aan het aantal parameters. De argumenten worden opgegeven als een string. Om deze stringrepresentatie om te zetten naar een object van het juiste type, moet bij de compiler een klasse geregistreerd worden die deze conversie uitvoert. Voor de set-method geldt hetzelfde en wordt de waarde van de extra parameter geleverd door de gebruiker van de gegenereerde client-GUI. • id (verplicht): een naam, gekozen door de gebruiker, waarmee de waarde van de oproep kan ge¨ıdentificeerd worden. X
X → keycol+ datacol+ row*
Deze elementen starten en be¨eindigen de beschrijving van tabellarische data. Niet ondersteund door deze applicatie! Attributen: • update (”10000”): de periode, in milliseconden, waarmee de tabelwaarden moeten opgehaald worden. • id (verplicht): een naam waarmee de waarden in de tabel kunnen ge¨ıdentificeerd worden. Dit element beschrijft een kolom die een sleutelwaarde bevat. De waarden in sleutelkolommen worden gebruikt als argumenten door methods die de waarden van niet-sleutelkolommen ophalen. Niet ondersteund door deze applicatie!
12
2.3 Syntax van het XML-bronbestand Attributen: • name (optioneel): de naam van de kolom. Deze naam zal gebruikt worden als kolomtitel. • type (verplicht): het type van de waarden in deze kolom, eventueel voorafgegaan door een package-naam.
Dit element beschrijft een kolom voor niet-sleutelwaarden in een tabel. Niet ondersteund door deze applicatie! Attributen: • name (optioneel): de naam van de kolom. Deze naam zal worden weergegeven als kolomtitel. • methods (verplicht): het ID van een eerder gedeclareerde get-method. Deze methods zullen gebruikt worden om de waarden op te halen voor deze kolom. Per rij in de tabel zullen deze methods opgeroepen worden, met als argumenten de waarden in de sleutelkolommen. Indien de method read-only is (get-method zonder geassocieerde set-method) zullen de waarden in deze kolom niet kunnen gewijzigd worden. De types van de parameters van de method, gedeclareerd in het paramtypes-attribuut, moeten overeenstemmen met de types gedeclareerd in de voorafgaande keycol-elementen. Dit element voegt een rij toe aan een tabel. Niet ondersteund door deze applicatie! Attributen: • key (verplicht): de waarde voor de sleutelkolommen voor deze rij. Indien er meerdere sleutelkolommen zijn in deze tabel, worden de waarden gescheiden door komma’s. De waarden worden opgegeven als strings. Om deze stringrepresentatie om te zetten naar een object van het juiste type moet bij de compiler een klasse geregistreerd worden die deze conversie uitvoert. <window> X
X → (panel|bean)*
Deze elementen begrenzen het window-gedeelte van de GUI. Dit gedeelte bevat voornamelijk de elementen voor de opbouw van het venster dat aan de gebruiker getoond wordt. Attributen: • title (optioneel): de waarde van dit attribuut wordt gebruikt als titel van het venster. 13
2.3 Syntax van het XML-bronbestand • colspan (optioneel): de breedte van het venster, uitgedrukt in aantal kolommen. Dit attribuut heeft geen effect in manueel geschreven XML-files. • rowspan (optioneel): de hoogte van het venster, uitgedrukt in aantal rijen. Dit attribuut heeft geen effect in manueel geschreven XML-files. <panel> X
X → (panel|bean)*
De objecten die binnen deze elementen gedeclareerd worden, zullen in het venster gegroepeerd worden binnen ´e´en panel. Elk panel bevat een tweedimensionaal rooster. Elke cel van dit rooster kan een bean of een ander panel bevatten. Een cel heeft een x- en een y-co¨ ordinaat. De cel in de linker bovenhoek heeft co¨ ordinaten (0,0). Beans en panelen kunnen meerdere cellen overspannen. Attributen: • label (optioneel): dit attribuut kan gebruikt worden om het panel van een titel te voorzien. Het panel zal dan een kader hebben met daarin de opgegeven titel. • x en y (optioneel): deze attributen geven de positie van het panel in het rooster van het ouderpanel. Indien het x-attribuut wordt weggelaten, wordt het panel in de cel links van de laatst ingevulde cel van het ouderpanel geplaatst. Indien het y-attribuut wordt weggelaten, wordt het panel in de cel net onder de laatst ingevulde cel van het ouderpanel geplaatst. • colspan en rowspan (”1”): het aantal rijen en kolommen dat dit panel moet overspannen in het rooster van het ouderpanel. X
X → input* output?
Deze elementen worden gebruikt om beans te instanti¨eren. Attributen: • type (verplicht): hiermee wordt het type van de bean aangegeven. Dit is geen JAVAclassnaam! De klasse van de bean wordt aan de hand van dit attribuut opgezocht in het bestand resources.xml. • x en y (optioneel): deze attributen geven de positie van de bean in het rooster van het ouderpanel. Indien het x-attribuut wordt weggelaten, wordt de bean in de cel links van de laatst ingevulde cel van het ouderpanel geplaatst. Indien het y attribuut wordt weggelaten, wordt de bean in de cel net onder de laatst ingevulde cel van het ouderpanel geplaatst.
14
2.3 Syntax van het XML-bronbestand • w en h (optioneel): deze attributen bepalen de grootte van de bean, w voor de breedte, h voor de hoogte. Als deze attributen niet aanwezig zijn, worden de standaard waarden gebruikt zoals opgegeven in het bestand resources.xml. • colspan en rowspan (”1”): het aantal rijen en kolommen dat deze bean moet overspannen in het rooster van het ouderpanel. • config (optioneel): met dit attribuut wordt in het bestand resources.xml gezocht naar de overeenstemmende configuratie van de JAVA-bean. Een configuratie bestaat uit een lijst van (property, value)-paren. Zo is het mogelijk om de JAVA-bean te configureren, zonder dat de compiler de properties van de gebruikte bean hoeft te kennen. Indien geen configuratie wordt vermeld, worden voor alle properties de standaard waarden gebruikt. • label (optioneel): een optioneel label voor de bean. De positie van het label wordt bepaald door het attribuut loc. • loc (optioneel): dit attribuut heeft enkel zin als ook het label-attribuut aanwezig is. Het geeft de positie van dit label een ten opzichte van de bean. Mogelijke waarden zijn north, south, east en west. Indien niet gespecificeerd wordt north gebruikt. Opmerkingen: • Voor enkele veelgebruikte beans, die tijdens het ontwikkelen van de GUI-compiler beschikbaar waren, zijn er ook elementen beschikbaar die een afkorting zijn voor bovengenoemde . . . elementen. Het attribuut type vervalt in deze verkorte elementen, daar het type vervat zit in de naam van het element. De elementen zijn . . . , . . . , . . . , . . . en
. . .
. • Er is ook speciale bean, Dummy. Deze wordt apart behandeld door de compiler en is ge¨ımplementeerd als een onzichtbare component, met als enig doel het mogelijk maken van lege ruimtes. • Als aan de bean een label wordt toegevoegd, zal de grootte opgegeven in w of h vermeerderd worden met de grootte van het label, al naar gelang de positie van dit label. De input-elementen leggen verbindingen tussen enerzijds de elementen die beschreven werden in het interface-gedeelte van het bronbestand en anderzijds de methods van een bean.
15
2.3 Syntax van het XML-bronbestand Attributen: • name (optioneel): de naam van de bean-input. Met deze naam wordt in resources.xml gezocht naar de method en het type van haar argument, die de input vormen. Op deze manier kan de compiler code genereren om methods aan te roepen, zonder dat hij de juiste naam van de method moet kennen. Indien de naam van de input niet gespecificeerd is, worden de inputs in de volgorde van de declaratie in resources.xml opgezocht. • index (optioneel): dit attribuut is bedoeld om indexed properties te kunnen gebruiken als ingang. De index is een integer die een element in een array adresseert. • source of const (verplicht, maar mutueel exclusief): – source is het ID van een eerder gedeclareerd data- of call-element. De data die periodiek worden opgehaald, worden aan de bean aangeboden op deze ingang. – const: indien mogelijk kan de bean-input verbonden worden met een constante waarde. De constante wordt opgegeven als een string. Om deze stringrepresentatie om te zetten naar een object van het juiste type, moet hiervoor een klasse geregistreerd worden bij de compiler die deze conversie uitvoert. Het output-element legt, net zoals de input-elementen, verbindingen tussen de elementen die beschreven werden in het interface-gedeelte van het bronbestand en de methods van een bean. Attributen: • dest of id (verplicht, maar mutueel exclusief): – dest is het ID van een eerder gedeclareerd data- of method-element. In de file resources.xml wordt gezocht naar de method-naam horende bij de output. Als de gebruiker er om vraagt, zal deze method gebruikt worden om een waarde van de bean te lezen. Deze waarde wordt dan gebruikt om de data, voorgesteld door dest, aan te passen. – id: Als de output van de bean een bound property is, kan aan deze output een id toegekend worden, net zoals dit bij data afkomstig van de CORBA-server gebeurt. Met dit id kan de uitgang van een bean dus doorgekoppeld worden naar de ingang van een andere bean, zodat het mogelijk is om data door te geven van de eerste naar de tweede bean. De data op de uitgang van de eerste bean worden als readonly beschouwd. Mogelijke voorbeelden zijn toepassingen waarbij de eerste bean een conversie van of een berekening met de data uitvoert zoals bv. een bean die een 16
2.4 Het gebruik van het source bestand in de builder tool expliciete typeconversie doet, een bean die data aan zijn ingangen combineert tot een array die geschikt is voor het tonen in een grafiek, een bean die de maximumwaarde die op zijn ingang is verschenen zal bewaren, . . .
2.4
Het gebruik van het source bestand in de builder tool
Het is belangrijk om op te merken dat de zojuist besproken syntax geldt voor source-files die manueel geschreven worden. Het is dus de syntax die door de compiler gebruikt wordt. De hier ontwikkelde builder tool legt nog een aantal extra eisen op. Die zijn niet in de XML-syntax opgenomen om de algemeenheid van de compiler niet aan te tasten. De extra regels waaraan een XML-source-file moet voldoen om aanvaard te worden door de builder tool (of in elk geval om het gewenste resultaat te verkrijgen bij het laden van een source-file) zijn de volgende: • De positie van een bean moet steeds expliciet vermeld worden. • -tags van een bean moeten steeds expliciet de naam van de input vermelden. • De colspan- en rowspan-attributen bij een <window>-tag zijn verplicht. In het kort kan men dus zeggen dat het correct laden van een XML-file door de builder tool enkel gewaarborgd is wanneer de XML-file ook door de tool zelf gegenereerd is. Het opladen van, voor het overige syntactisch correcte, XML-files kan anders immers voor foutboodschappen zorgen of kan ongewenste resulterende GUIs voor gevolg hebben.
17
Hoofdstuk 3
Implementatie van een grafische gebruikersinterface voor de GUI-compiler Zoals reeds gezegd in hoofdstuk 1, valt de constructie van een GUI ruwweg uiteen in 3 delen: 1. De componenten kiezen die in de te construeren GUI moeten aanwezig zijn, hun positie in de GUI en hun voorkomen bepalen . . . 2. Een aantal methodes en attributen kiezen van CORBA-objecten en specifi¨eren met welke argumenten en met welke frequenties ze aangeroepen moeten worden. 3. De beans verbinden met de CORBA-methodes en -attributen via hun inputs en outputs. Deze opsplitsing is grotendeels te zien in de programmastructuur. Alles wat met het grafische aspect te maken heeft, wordt voornamelijk geregeld in de klassen DrawPanel (en afgeleiden) en BeanWrapper. De CORBA-objecten worden beheerd in het package interfacedescription. Connecties met inputs en outputs van beans worden dan weer behandeld in de klasse ioHandler. De andere klassen dienen voornamelijk ter ondersteuning van deze klassen of om alles met elkaar te verbinden. In het vervolg van dit hoofdstuk zullen de zopas vermelde klassen (en een aantal andere) wat uitgebreider aan bod komen. Opmerkingen: • Dit is een eerder beknopt overzicht, waarin enkel de belangrijkste klassen besproken worden. Voor sommige klassen wordt ook niet het volledige UML-schema gegeven. Enkel 18
3.1 Lay-out van de GUI de belangrijkste datamembers en methodes worden getoond. Een volledig overzicht van alle gebruikte klassen, met hun data members en methodes (met commentaar omtrent de functie en het gebruik) kan u vinden in de door javadoc gegenereerde documentatie. In dit hoofdstuk zal wel steeds wat meer uitleg gegeven worden omtrent de manier waarop de klassen gebruikt worden. • In het vervolg van dit hoofdstuk zal geregeld gesproken worden over GUI s. Meestal wordt hiermee de GUI bedoeld die de gebruiker aan het construeren is (de GUI onder constructie). De andere belangrijke GUIs zullen steeds expliciet vermeld worden, zoals de GUI van dit programma (de applicatie-GUI ) en de GUI die door de compiler gegenereerd wordt (de gegenereerde of de resulterende GUI). • Wanneer in dit hoofdstuk gesproken wordt over laden en opslaan, worden de begrippen XML-file en XML parse tree door elkaar gebruikt. De interactie met de XML-file zelf, gebeurt echter enkel door de klasse gui.Gui, die de rol van hoofdprogramma vervult. De andere klassen werken enkel met de parse tree. Omdat in de praktijk het opstellen van een XML-tree steeds zal gevolgd worden door het effectief opslaan van de tree in een XML-file (en analoog voor het laden), zou het vermengen van beide begrippen niet tot verwarring mogen leiden.
3.1
Lay-out van de GUI
Op elk moment wordt de huidige toestand van de GUI onder constructie getoond in wat verder het tekenvenster zal genoemd worden. Dit tekenvenster is volledig interactief. Elke verandering die de gebruiker aan de lay-out wil maken, doet hij rechtstreeks op het tekenvenster. Elke verandering wordt ook onmiddellijk op het tekenvenster weergegeven. Het tekenvenster ziet er dus uit zoals de uiteindelijke gegenereerde GUI er zou uitzien, op de Start- en Update-knop na, die in elke gegenereerde GUI automatisch toegevoegd worden door de compiler. Tijdens het ontwerp van het tekenvenster (en van alle klassen die iets met de lay-out van de GUI te maken hebben) is steeds getracht om de GUI, getoond in het tekenvenster, volledig te laten overeenkomen met de GUI die op dit moment door de compiler zou gegenereerd worden. Dit is belangrijk voor grafisch ontwerp, omdat de gebruiker moet weten welke resulterende GUI hij kan verwachten. Het plaatsen van componenten (beans) gebeurt op een regelmatig rooster met grid-snapping, waardoor alle componenten steeds uitgelijnd worden op het rooster. Deze lay-out is ge¨ımplemen19
3.1 Lay-out van de GUI teerd in de klasse drawpanel.RegularGridLayout. Voor het positioneren van componenten in een container met deze lay-out worden RegularGrigConstraints gebruikt. De idee is dezelfde als bij GridBagConstraints, maar de functionaliteit is veel beperkter (enkel de positie in het rooster wordt gespecifieerd). Het gebruik van ´e´en of ander rooster voor de lay-out was nodig om de hiervoor vermelde gelijkheid met de resulterende GUI te waarborgen. Die gebruikt immers GridBagLayout voor de lay-out, die ook een soort roosterstructuur heeft.
3.1.1
drawpanel.NumberedPanel javax.swing.JPanel
NumberedPanel -nextNumber : long -number : long +NumberedPanel() +equals(in o : Object) : boolean
Figuur 3: drawpanel.NumberedPanel
Voor het implementeren van drag-and-drop (zie verder) was het nodig om componenten uniek te identificeren. Aangezien het gebruikte package vereiste dat alle sleepbare componenten serializable waren, kon de identificatie niet gebeuren aan de hand van de referentie. Daarom zullen alle componenten die in het tekenvenster geplaatst worden, afgeleid zijn van deze basisklasse. De functie equals() vergelijkt enkel de identificatienummers van beide componenten.
3.1.2
drawPanel.Savable
Deze interface heeft maar 1 methode, createTree(). Die laat toe de component op te slaan in een XML-parse-tree.
3.1.3
drawpanel.DrawPanel
De compiler biedt de mogelijkheid om beans te groeperen in panels, met een titel en een kader. Dergelijke panels worden hier voorgesteld door de klasse Drawpanel (figuur 4). Merk wel op 20
3.1 Lay-out van de GUI
«interface» Savable
NumberedPanel
DrawPanel
+CELL_SIZE : int +dragSource : java.awt.dnd.DragSource #dropTarget : java.awt.dnd.DropTarget
+DrawPanel() +DrawPanel(in title : String) +addBean(in beanName : String, in x : int, in y : int, in w : int, in h : int, in config : String) : void +addBean(in beanName : String, in x : int, in y : int, in w : int, in h : int, in label : String, in config : String) : void +addBean(in beanName : String, in x : int, in y : int, in w : int, in h : int, in label : String, in position : String, in config : String) : void +addBean(in beanName : String, in x : int, in y : int, in config : String) : void #addListeners() : void +addPanel(in panel : DrawPanel, in x : int, in y : int) : void +createTree() : com.ibm.xml.parser.TXElement +getGridSize() : java.awt.Dimension +getLocation(in comp : java.awt.Component) : java.awt.Point +handleBeanElement(in elem : org.w3c.dom.Element) : void +handlePanelElement(in elem : org.w3c.dom.Element) : void +hasRoomFor(in comp : java.awt.Component, in x : int, in y : int) : boolean +setGridSize(in dim : java.awt.Dimension) : void +traverseContentsTree(in node : org.w3c.dom.Node) : void 1 1
#layout
RegularGridLayout
Figuur 4: drawpanel.DrawPanel
dat de mogelijkheid om dergelijke panels aan de GUI toe te voegen nog niet ge¨ımplementeerd is. De interne ondersteuning is echter wel grotendeels aanwezig (zij het nog niet volledig getest). De klasse DrawPanel dient op dit moment dan ook enkel als basisklasse voor RootDrawPanel, zijnde het tekenvenster zelf. De plaatsing van componenten binnen het panel gebeurt door middel van drag-and-drop. Hiervoor is gebruik gemaakt van het package java.awt.dnd. De reden waarom dit package gebruikt wordt, is dat het uitermate gemakkelijk is om daarmee objecten tussen verschillende panels en zelfs vensters te slepen. Wanneer een component op een andere component gedropt wordt, wordt de onderste component ervan verwittigd en kan hij zelf het droppen afhandelen. Daarna wordt de gesleepte component zelf verwittigd van de uitslag van de drop. Op die manier moet er dus niet gezocht worden naar het panel dat de component oorspronkelijk bevatte en naar het panel waarop hij gedropt wordt. Het grote nadeel van dat package is wel dat alle te slepen componenten serializable moeten zijn. Dat brengt een aantal problemen met zich mee wanneer die componenten toegang moeten hebben tot gemeenschappelijke datastructuren. Wanneer alle componenten een referentie naar 21
3.1 Lay-out van de GUI die structuur zouden bezitten, zou bij het slepen die hele structuur immers gedupliceerd worden, zodat verschillende componenten niet meer dezelfde structuur gebruiken. Er zullen dus andere oplossingen voor dergelijke problemen moeten gevonden worden. Een bean toevoegen gebeurt door een van de addBean-methodes. Het panel zal dan eerst via het beanpanel (zie 3.6.2) de resources-boom opzoeken. Vervolgens zal het panel aan die boom vragen een bean te instanti¨eren met de gegeven naam en parameters. Die bean zal in een BeanWrapper gestopt worden en die wrapper zal uiteindelijk aan het panel toegevoegd worden. Wanneer een bean wordt toegevoegd zal steeds eerst nagekeken worden of er wel plaats is voor die bean op de gekozen plaats. Het is niet toegelaten dat beans elkaar overlappen. Dit wordt gecontroleerd door de functie hasRoomFor. Het opslaan van een panel zorgt voor een probleem. De gui2java-compiler maakt gebruik van GridBagLayout voor de gegenereerde GUIs. Die lay-out heeft ook rijen en kolommen, maar de groottes daarvan worden door de layout-manager zelf bepaald. Lege rijen en kolommen zullen zelfs volledig “ingeklapt” worden tot grootte 0. Dat heeft voor gevolg dat de gegenereerde GUI niet meer overeenkomt met de GUI getoond in het tekenvenster. Daarnaast is het, zelfs bij het manueel programmeren in JAVA, niet evident om componenten in een GridBagLayout te plaatsen zoals bijvoorbeeld in figuur 5. De lay-out manager zal immers steeds proberen een aantal kolommen breedte 0 te geven om de componenten volledig onder elkaar of volledig naast elkaar te zetten.
Figuur 5: Moeilijk te maken lay-out
Om deze problemen te omzeilen, wordt een nieuw type “bean” gebruikt: het type Dummy. Eigenlijk is dit geen echte bean. Het is gewoon een leeg (= onzichtbaar) stukje JPanel met dezelfde grootte als 1 cel in het regelmatige grid van het panel. De beschrijving ervan staat niet in de resources-file, maar is hard-gecodeerd in de compiler en de grafische front-end. Dit heeft als nadeel dat een gebruiker nooit een bean met als naam “Dummy” kan gebruiken, maar vermijdt
22
3.2 Beschrijven van de beschikbare beans wel problemen door een gebruiker die bij het opruimen van een te grote bean-bibliotheek de dummy-bean verwijdert. Dergelijke dummies worden toegevoegd aan het drawpanel bij het opslaan en worden genegeerd bij het opnieuw laden. Ze zorgen ervoor dat de rijen en kolommen in de resulterende GUI ook een regelmatig rooster vormen, met dezelfde lay-out als de GUI in het tekenvenster. Er wordt steeds voor gezorgd dat er in elke rij en in elke kolom minimaal 1 dummy voorkomt. Een alternatieve oplossing was om de compiler aan te passen zodat hij ook een regelmatig rooster zou gebruiken bij de lay-out van de gegenereerde GUIs. Dit zou als voordeel gehad hebben dat er geen hard-coded componenten zouden nodig zijn en dat de XML-files heel wat compacter zouden zijn (geen nood meer aan dummy-elementen). Het zou echter een vrij grote aanpassing aan de compiler zijn geweest, die als resultaat zou hebben dat reeds bestaande XML-files na compilatie een andere GUI zouden opleveren dan met de vorige versie van de compiler. Er werd dan ook voor geopteerd om de aanpassingen aan de compiler zo klein mogelijk te houden en het systeem van de dummies te gebruiken. De functies loadPanel(), traverseContentsTree() en handleXXX() worden gebruikt bij het laden van het panel uit een XML-parse-tree.
3.1.4
drawpanel.BeanWrapper
Zoals reeds vermeld worden alle beans eerst in een BeanWrapper ge¨encapsuleerd, vooraleer ze in het tekenvenster geplaatst worden. De klasse BeanWrapper zorgt voor de ondersteuning van drag-and-drop, opslaan in XML, laden vanuit XML, configuraties van de bean . . . Voor het beheren van de inputs en de outputs rekent de klasse op een BeanIOHandler (zie sectie 3.5.2). Eigenlijk is deze klasse heel eenvoudig. Het is een pure wrapper-klasse, die eigenlijk niet veel meer doet dan functiecalls delegeren naar de gepaste functies van andere klassen. Het grootste deel hiervan komt aan bod in het vervolg van dit hoofdstuk.
3.2
Beschrijven van de beschikbare beans
De compiler vereist dat alle beschikbare beans beschreven worden in de file resources.xml. Een volledige beschrijving van de syntax en semantiek van dat bestand is te vinden in [5]. Om 23
3.2 Beschrijven van de beschikbare beans de informatie in de resources-file makkelijker beschikbaar te stellen voor andere klassen, is het package resources ontworpen.
Deze klasse (figuur 6) encapsuleert de beschrijving van een enkele bean (beter: een enkel type bean) in de vorm van een XML-parse-tree. Deze tree is een deel van de volledige parse-tree, verkregen door het parsen van de file resources.xml. Een BeanResources-object komt dus overeen met een -element in de resources-file. De klasse laat toe om de gegevens uit die parse tree te halen, zonder enige kennis van de exacte vorm van de parse tree zelf. Een tweede (en minstens even belangrijke) functie van deze klasse is het instanti¨eren van een bean. Dit gebeurt via de instantiate()-functies. Instanti¨eren van een bean zonder extra parameter komt eenvoudig neer op het instanti¨eren van de klasse van beans die beschreven wordt door deze BeanResources (zie figuur 7). Een eventuele parameter geeft de naam aan van de te gebruiken configuratie voor de bean. Een beschrijving van het gebruik van configuraties vindt u in sectie 3.3. Daar worden ook de functies uit deze klasse die met configuraties te maken hebben, verder besproken.
24
3.3 Configureren van een bean public Object instantiate() throws CorruptedResourcesException{ try{ return (Class.forName(bean.getAttribute("class"))).newInstance(); } catch(Exception e){ throw new CorruptedResourcesException(); } }
Deze klasse (figuur 8) bevat de volledige parse tree die verkregen werd uit de resources-file. De functie getBean() kan gebruikt worden om de beschrijving van een bepaald type bean te verkrijgen, onder de vorm van een BeanResources-object.
3.3
Configureren van een bean
Zoals in de compiler worden ook hier configuraties gebruikt om de properties van de beans in te stellen. De compiler laat niet toe om de waarde van individuele properties van een bean rechtstreeks te specifi¨eren. In plaats daarvan kan een configuratie vermeld worden bij de bean. Die configuratie (waarvan een beschrijving opgenomen is in de resources-file) bevat de specificatie van de waarden van verschillende properties van de bean. Je zou een configuratie kunnen zien als een pakket van property-waarden.
3.3.1
Specifi¨ eren van een configuratie voor een bean
Wanneer de gebruiker een configuratie specifieert voor een bepaalde bean, is het belangrijk dat die niet alleen bijgehouden wordt (om ze in de source-XML-file op te slaan en naar de compiler 25
3.3 Configureren van een bean door te geven), maar dat de configuratie ook onmiddellijk op de bean in het tekenvenster toegepast wordt. De reden hiervoor is opnieuw het tekenvenster zo exact mogelijk te laten overeenkomen met de uiteindelijk resulterende GUI. Bepaalde properties van een bean (en eigenlijk zelfs de meeste) zullen immers het uitzicht van de bean in meer of mindere mate be¨ınvloeden. Wanneer een configuratie op die manier toegepast wordt op een bean (bv. door de naam van de configuratie mee te geven als argument van de functie instantiate(String) in de klasse BeanResources), wordt voor elk property-waarde-paar in de configuratie de juiste write-method opgezocht (via de introspectie-eigenschappen van beans) en wordt die method gebruikt met de waarde als argument om de property de gewenste waarde te geven. Een probleem dat hierbij opduikt is wel dat de configuratie uit een XML-file gehaald wordt, waardoor dus alle property-waarden als strings verkregen worden. Het is dus nodig om die strings om te zetten naar een waarde van de juiste klasse. Hiervoor wordt gebruik gemaakt van de functie convert() uit de klasse resources.StringToObject (figuur 9).
Wanneer de string een literal (een letterlijke waarde) voorstelt (bv. “10”), wordt de conversie gedaan met behulp van de klassen uit het package runtime.stringconvertors van de compiler. Wanneer de string echter de volledig gekwalificeerde naam van een statisch veld van een klasse is (bv. “java.awt.BorderLayout.NORTH”), zoekt de functie het veld op en retourneert ze de waarde van het veld.
3.3.2
Defini¨ eren van een nieuwe configuratie
Natuurlijk heeft de gebruiker ook de mogelijkheid om nieuwe configuraties te defini¨eren voor een bean. Daarvoor wordt hem een lijst aangeboden, met daarin alle properties van de bean en invoervelden om waarden te specifi¨eren voor die properties. Voor het genereren van deze lijst wordt weerom gebruik gemaakt van introspectie van een bean. Hoewel het in principe mogelijk 26
3.4 CORBA-interface is, wordt er op dit moment geen controle iotgevoerd of de ingevoerde gegevens wel geldig zijn (bv. of ze van het juiste type zijn). Dat is ook niet strikt noodzakelijk, omdat dit getest wordt door de compiler, die de nodige foutboodschappen geeft op dat moment. Een andere opmerking bij het defini¨eren van nieuwe configuraties is dat de gebruiker een lijst te zien krijgt van alle properties van een bean. De lijst bevat dus ook properties die niet bedoeld zijn om op deze manier ingesteld te worden, omdat ze bv. als input of zelfs als output van de bean gebruikt worden. Op dit moment is het echter onmogelijk om zonder enige voorkennis van de beans dergelijke properties eruit te filteren. Het niet gebruiken van voorkennis is echter een voorwaarde om het toevoegen van nieuwe beans eenvoudig te houden. Mogelijkheden om dit probleem te omzeilen zouden kunnen zijn om de lijst met instelbare properties ook op te nemen in de resources-file (wat een vrij drastische ingreep zou zijn en wat het opstellen van de resources-file veel zwaarder zou maken), of het invoeren van extra naming conventions voor de beans (wat dan weer zou verhinderen dat een bean die niet specifiek voor deze applicatie geschreven is, zou gebruikt worden). Het lijkt dan ook het beste om gewoon te vertrouwen op de kennis en het gezond verstand van de gebruiker. Het instellen van de waarde van een input-property heeft trouwens geen negatieve gevolgen. De ingestelde waarde wordt gewoonweg genegeerd.
3.4
CORBA-interface
Er moest een manier gevonden worden om de gegevens van alle gebruikte CORBA-methodes en -attributen intern bij te houden en te bewerken. De voorzieningen hiervoor zijn samengebracht in het package interfacedescription. De bedoeling van dat package is een dubbelgelinkte boomstructuur op te bouwen (figuur 10) met daarin alle gegevens over CORBA-objecten, die uiteindelijk in de XML-file moeten terechtkomen. De belangrijkste basisklassen in dit package worden hier besproken. Bij het opslaan van de geconstrueerde GUI in een XML-file, wordt deze boom doorlopen en worden alle relevante gegevens ervan opgeslagen in de XML-file.
3.4.1
interfacedescription.Cleanable
Dit is de abstracte superklasse van alle elementen in de interface-boom (figuur 11). De bedoeling is een soort garbage collection te kunnen doen in de boom. Wanneer een gebruiker immers een 27
bean verwijdert uit de GUI is het belangrijk dat alle ermee geassocieerde CORBA-methodes en attributen uit de interface-boom verwijderd worden, als ze niet meer met een andere bean geassocieerd zijn. Anders zouden ze immers in de XML-file terecht komen als de GUI opgeslagen wordt. Dat zou ervoor kunnen zorgen dat GUIs die geregeld aangepast worden meer en meer “garbage” zouden bevatten. Om dat te vermijden wordt een systeem van reference counts gebruikt. Elke Cleanable bevat een referentie naar zijn ouder-knoop. Voor een Call-knoop is dit bv. een Method-knoop. Wanneer er nu een nieuwe referentie naar een bepaalde knoop gemaakt wordt (bijvoorbeeld omdat die knoop een nieuwe kind-knoop krijgt of omdat de knoop een call voorstelt die als input van een bean zal gebruikt worden), moet degene die de nieuwe referentie aanmaakt de 28
3.4 CORBA-interface methode addReference() van de knoop aanroepen. Wanneer een referentie verwijderd wordt (bv. een bean, die een referentie naar een zekere knoop had, wordt verwijderd) moet de procedure removeReference() aangeroepen worden. Deze methodes doen niets met de referenties zelf. Ze updaten enkel de reference count (bijgehouden in de data-member refCount). Wanneer een knoop nu merkt dat de laatste referentie er naartoe zopas verwijderd werd, weet die knoop dat hij overbodig is. Hij zal zijn ouderknoop hiervan verwittigen door diens cleanUpmethode aan te roepen met zichzelf als argument. Hij wordt dan door de ouder verwijderd uit de boom. Als daardoor de reference count van de ouder ook op 0 valt, wordt daar hetzelfde scenario herhaald. Het resultaat is dat de boom op elk moment enkel die knopen bevat die ook effectief gebruikt worden in de huidige GUI. De volledige implementatie hiervan vindt u in figuur 12. /** * Notifies the object of the removal of a reference to it. The * object will prepare to be garbage collected, if that reference * was the last one, by removing the reference to its parent and * notifying its parent of this fact. **/ public void removeReference(){ refCount --; if (refCount < 0){ //shouldn’t happen ErrorMsg.print("The reference count of a Cleanable got below " + "0!"); } else if(refCount == 0){ //Prepare to be collected if(parent != null){ //RootInterface has no parent. parent.cleanUp(this); parent = null; } } }
Figuur 12: Cleanable.removeReference()
Twee klassen hebben Cleanable als basisklasse. Zij zullen in de volgende paragrafen besproken worden.
3.4.2
interfacedescription.RootInterface
Dit is de root van de interface-boom. Het UML-schema wordt getoond in figuur 13. Deze root onderscheidt zich van de andere knopen doordat hij niet overeenstemt met een element in de XML-file. Deze klasse is gewoon ontworpen om de boom bijeen te houden en om er een aantal 29
globale bewerkingen op te verrichten, zoals het omzetten van de boom naar een XML-parse-tree en het laden van de boom uit zo’n parse-tree. Het omzetten naar een parse-tree gebeurt door de methode createTree(). Deze methode zal recursief de createTree van de kinderen van de root aanroepen. Ook heel belangrijk is de vector idElements. Alle IdentifiableInterfaceElements (zie sectie 3.4.4) in de boom waarvan deze knoop de root is, worden hierin opgesomd. Deze vector (in samenwerking met de functies getElement en getIndex) speelt een heel belangrijke rol in het connecteren van inputs en outputs aan CORBA-objecten. Hoe de vector juist gebruikt wordt, wordt verderop in dit hoofdstuk besproken.
3.4.3
interfacedescription.InterfaceElement
Deze abstracte (figuur 14) klasse omvat alle elementen die in de XML-file kunnen vermeld worden. Alle klassen die dergelijke elementen voorstellen, moeten dus van deze klasse erven. Deze klasse laat toe om ook de neerwaartse links in de boom te leggen, via de children-member. De opwaartse links zaten reeds in de klasse Cleanable. Hier wordt de methode cleanUp ge¨ımplementeerd voor alle elementen van de interface-boom. 30
Ze verwijdert de als argument meegegeven Cleanable uit de lijst van de kinderen en roept removeReference() aan. De methode createTree() is abstract gelaten en dient om de boom om te zetten in een XML parse tree. De bedoeling is dat door middel van oproepen naar createTree() de gehele interface-boom recursief doorlopen wordt. Elke knoop zal dus een element in de XML parse tree aanmaken met daarin alle gegevens over zichzelf en vervolgens createTree() aanroepen voor al zijn kinderen om hen ook toe te laten hun gegevens in de parse tree te schrijven.
3.4.4
interfacedescription.IdentifiableInterfaceElement
Deze klasse voegt een unieke identifier toe aan een InterfaceElement en laat toe om knopen te zoeken volgens identifier. Dat laatste zal zeer belangrijk zijn bij het laden van een GUI uit een XML-file (zie sectie 3.4.6). De assignID-methode kent aan elk IdentifiableInterfaceElement een unieke identifier toe op het moment dat de boom omgezet wordt naar een XML-parse-tree. Wanneer een object van dit type geconstrueerd wordt, wordt het automatisch (door de constructor van deze klasse) toegevoegd aan de idElements-vector van de RootInterface van deze boom. Op die manier wordt die vector steeds up-to-date gehouden.
31
3.4 CORBA-interface
3.4.5
De eigenlijke knopen
De ondersteunde elementen uit de XML-file worden gemapt op een aantal klassen die in 2 groepen uiteen vallen. Aan de ene kant zijn er de -elementen die op de Interface-klasse (afgeleid van InterfaceElement) gemapt worden. Aan de andere kant zijn er alle andere elementen, die gemapt worden op klassen die afgeleid zijn van IdentifiableInterfaceElement. Al deze klassen implementeren de methoden equals en createTree, specifiek voor hun datamembers. De klassen die nog kind-knopen kunnen hebben, hebben daarnaast ook nog eens ´e´en of meerdere methodes createXXX, waarbij XXX staat voor ´e´en van hun mogelijke kind-klassen. Dit wordt gebruikt om nieuwe knopen aan te maken op de meest economische manier, wanneer een bean een nieuwe input- of output-knoop wil. Die createXXX-methodes zullen immers steeds eerst zien of er al een knoop bestaat met de gevraagde gegevens erin. Als dat het geval is, is het economischer om die knoop te hergebruiken, dan om een nieuwe, identieke knoop aan te maken. Gewoon de reference count van de knoop verhogen en een referentie naar de knoop teruggeven is voldoende. Als de gevraagde knoop nog niet bestaat, wordt hij door createXXX aangemaakt en aan deze knoop toegevoegd als kind. Om het hergebruik van knopen maximaal te maken, wordt voor de klasse Method gebruik gemaakt van de methode isEquivalent. Als een input van een bean immers een get-method wil gebruiken van een CORBA-server, en die get-method is al aanwezig in een method-knoop in de boom, maar in die knoop staat ook nog een set-element, dan kan die knoop toch nog gebruikt worden voor de input van de nieuwe bean en moet er dus geen nieuwe knoop gemaakt worden. De knoop die de bean nodig heeft is dus in zekere zin “equivalent” met de bestaande knoop. Door het gebruik van de reference counts en de createXXX-methodes, wordt er dus voor gezorgd dat de interface-boom op elk moment enkel knopen bevat die effectief gebruikt worden. Dit vermijdt dat er, bij het opslaan van de boom naar een XML-file, overbodige XML-elementen zouden uitgeschreven worden. Er wordt wel wat compactheid verloren ten opzichte van manueel geschreven XML, doordat geen gebruik gemaakt wordt van de mogelijkheid om zowel een get- als een set-method te specifi¨eren in 1 <method>-element. Hoewel dit in principe wel mogelijk is, zou de implementatie daarvan in een programma zonder enige voorkennis omtrent de CORBA-servers een vrij onoverzichtelijk en obscuur resultaat hebben.
32
3.4 CORBA-interface
3.4.6
Laden van de interface uit een XML-file
Het laden van de boom uitgaande van een XML-parse-tree gebeurt op een recursieve wijze. De root haalt uit de parse-tree alle -elementen en geeft die ´e´en voor ´e´en door aan een constructor van de Interface-klasse. Een dergelijke constructor zal de interface initialiseren met de gegevens uit het -element en vervolgens alle kind-elementen doorgeven aan de constructors van de juiste klassen. Daar herhaalt zich een gelijkaardig scenario. Belangrijk hierbij is het gedrag van de IdentifiableInterfaceElements. Wanneer deze uit een parse-tree ge¨ınitialiseerd worden, roepen ze de addId-functie van de root aan. Op deze manier wordt de lijst idElements opgebouwd van alle IdentifiableInterfaceElements. Deze lijst zal dan gebruikt worden om elementen terug te vinden wanneer inputs en outputs van beans geladen worden en wanneer id’s moeten gegenereerd worden om de interface-boom om te zetten naar een XML-parse-tree. Ook hier is weer in zekere mate naar compactheid gestreefd. Door het gebruik van reference counts (en dus het verwijderen van ongebruikte knopen) zal de lijst met elementen na verloop van tijd een aantal lege plaatsen gaan bevatten. De lijst bevat dan meer plaatsen dan strikt nodig. Wanneer de interface-boom echter omgezet wordt naar een XML-parse-tree, worden alle id’s opnieuw toegekend, zonder aan de lege plaatsen een id toe te kennen. Op deze manier hebben alle knopen weer aaneensluitende id’s en zal de lijst de minimale omvang hebben wanneer de interface boom opnieuw geladen wordt uit de parse tree. Een heel belangrijk bijkomend voordeel hiervan is dat na het laden van de interface uit de XML-file, alle IdentifiableInterfaceElements de index bezetten in de vector idElements die overeenkomt met hun eigen id. Dit zal een grote rol spelen bij het laden van inputs en outputs uit de XML-file. Voor verdere uitleg hieromtrent, zie sectie 3.5.5.
3.4.7
Specifi¨ eren van een functie-call door de gebruiker
Het is evident dat de hiervoor beschreven boomstructuur door de gebruiker moet kunnen opgebouwd worden. Om te voldoen aan de eis van minimale voorkennis van de gebruiker, zal er gesteund worden op de Interface Repository en de Naming Service. Daarmee zal de gebruiker voorzien worden van de nodige informatie. In figuur 15 wordt getoond op welke manier de gebruiker van de nodige informatie voorzien wordt bij het specifi¨eren van een call naar een CORBA-functie. Voor het kiezen van een attribuut van een servant, wordt een zeer analoog systeem gebruikt. 33
3.5 Inputs en outputs
gebruiker
Applicatie
Naming Service
Interface Repository
list() combo box
lijst servants
kies servant x
x._get_interface_def()
lijst functies
beschrijving interface
kies functie y lijst parameters
Figuur 15: Het specifi¨eren van een functie
Eerst wordt uit de Naming Service een lijst opgehaald met alle servants in het netwerk. Wanneer de gebruiker er daar ´e´en uit gekozen heeft, wordt de interface ervan uit de Interface Repository gehaald. Daaruit worden de functies van de servant gedestilleerd en aan de gebruiker aangeboden. Deze kiest daar een functie uit. Vervolgens worden de namen en datatypes van de parameters van die functie uit de daarnet ontvangen interface-beschrijving gehaald. Ook die worden aan de gebruiker getoond, zodat hij er de waarden voor kan specifi¨eren, die gebruikt moeten worden bij het aanroepen van de functie.
3.5
Inputs en outputs
Het doel van de hele constructie beschreven in sectie 3.4 is natuurlijk de gespecifieerde CORBAmethodes en -attributen te gebruiken als de inputs en outputs van een bean. Om dit mogelijk te maken is het package drawpanel.beanwrapper.io ontwikkeld. De belangrijkste klassen (wat betreft de functionaliteit achter de schermen toch), worden hier besproken. De andere zijn voornamelijk GUI-gerelateerd.
Om bindingen van inputs en outputs bij te houden, werden twee klassen gemaakt, input en output. Zoals te zien in figuur 16, zijn dit geen echte klassen. Het zijn eigenlijk niet veel meer dan datastructuren (vergelijkbaar met structs in C). Dit is misschien een inbreuk op de regels van goed OO-ontwerp, maar het is voldoende voor de eenvoudige functie van deze klassen, nl. enkel het bijhouden van de bindingen. De exacte betekenis van de members van de klassen is net dezelfde als die van het gelijknamige attribuut in de source-XML-file. De compiler bood echter nog een andere mogelijkheid om inputs van beans te voorzien van data. Het is immers mogelijk om de output van de ene bean rechtstreeks te gebruiken als input van een andere bean. Om dit te modelleren werd een systeem van pipes uitgedacht. De functie is zeer eenvoudig. Een pipe heeft een ingang, een uitgang en een identifier. Aan de uitgang en de ingang wordt telkens 1 bean gekoppeld. Data die aan de ingang aangelegd worden, moeten naar de uitgang gestuurd worden. Dit kan heel eenvoudig door bij beide beans in de XML-file de unieke identifier van de pipe op te geven als source of destination. De idee is dus dat de gebruiker eerst een Pipe-object aanmaakt en vervolgens beans aan de ingang en de uitgang hangt. Het opslaan naar XML is heel eenvoudig. Bij het laden van een GUI uit een XML-file moet gewoon voor elke verbinding tussen beans een Pipe-object aangemaakt worden dat de twee beans verbindt. Door een specifieke naamgeving voor pipes te gebruiken, zou dit zelfs heel eenvoudig zijn. Opgelet: Er is nog geen ondersteuning voor het gebruik van pipes, en dus voor het rechtstreeks verbinden van twee beans. Het is enkel conceptueel voorzien.
3.5.2
BeanIOHandler
Deze klasse (figuur 17) houdt de inputs en de eventuele output van een bean bij. Via een aantal get-methods kunnen inputs en outputs van de bean gevonden worden. Het aanpassen van de 35
bindingen van de inputs en de output gebeurt echter niet door de klasse zelf. Als de connectie van een input moet aangepast worden, wordt via de BeanIOHandler van de bean de betreffende input opgezocht. Vervolgens wordt het aanpassen rechtstreeks op het verkregen Input-object zelf gedaan. De belangrijkste taak van de BeanIOHandler is dan ook het omzetten van de inputs en de output naar een XML-parse-tree en omgekeerd het laden van de connecties van de inputs en de output uit een XML-parse-tree. Omdat in de XML-file de inputs en de output niet gegroepeerd zijn onder een aparte tag, maar elk afzonderlijk als kind-element van het -element vermeld worden, is ervoor gekozen om aan de functie generateXML een parameter te geven, die de “root”-knoop moet worden van de - en