5.3 Implementation af program
5.3.2 Design patterns
I dette afsnit vil vi beskrive hvordan vi har overholdt model-view-controller i implementationen, samt hvordan vores data-access-objects er implementeret.
5.3.2.1 MVC
Model.
Implementationen følger MVC design patternet på den måde at der er oprettet en model hvori de forskellige DAO’s er placeret. Dette bliver beskrevet nærmere i følgende DAO afsnit [Se afsnittet 5.3.2.2].
5.3 Implementation af program 55
I enhver applikation der benytter Play frameworket findes der en application.conf fil, der indeholder indstillinger for applikationen. I vores implementation inde-holder den opsætningen af forbindelserne til fakturerings og switchdatabasen samt login til API‘et på traffic shaperne. Derudover definerer application.conf også hvilken fil der fungrerer som startfil ind i applicationen. I vores tilfælde er det en Global fil, der starter en vejleder actor samt modtager login indstill-ingerne fra application.conf. Disse indstillinger bruges så af vejleder actoren til at starte de services der opretter forbindelserne.
View.
Der er som sådan ikke defineret noget view til API’et men Play frameworket in-deholder et predefineret view der f.eks. kan returnere fejlbeskeder fra Controller handlingerne.
Controller.
Controllerne i API‘et benytter sig som beskrevet, af en routes konfigurations fil, der bestemmer hvilken handling der controllen skal udføre. Hovedfunktionerne såsom at oprette en database, tilføje og fjerne kunder og ændre hastigheder for givne kunder. Et eksempel på en route kunne være
POST / c r e a t e u n i o n c o n t r o l l e r s . M a i n C o n t r o l l e r . c r e a t e U n i o n Som det ses fra koden kaldes controlles.TestController.createUnion via /create-union og dette vil svare til at kalde "servernavnet":9000/create/create-union.
Nedenstående figur viser hvordan et kald routes igennem Play API’et
Figure 5.2: En visuel repræsentation af routes i Play frameworket virker.
Controlleren modtager JSON objekter der repræsenterer parametrene i det data der skal indsættes/oprettes. Hvis dette valideres korrekt lægges der en besked
op på eventstreamen der så vil håndteres af en actor der f.eks. opretter en given forening.
5.3.2.2 DAO
Fakturering.
Vores faktureringsdao er et simpel DAO der ikke kan indsætte noget i databasen, men kun hente informationer ud af den. Det er ligeledes der informationer omkring brugeres hastigheder, adresser og kundenumre hentes ud. Vores fak-tureringsdao har derfor følgende operationer:
• getUsers(): Henter alle aktive brugere i databasen.
• getUser(customerid: Int): forsøger at hente en bestemt bruger ud, hvis denne ikke findes bliver der returneret et tomt resultat.
• getUsersByUnion(unionid: Int): Henter alle aktive brugere for en forening.
• getUnionIds(): Henter alle forenings id’er.
Hvis man f.eks. ønsker at hente informationer omkring en bestemt kunde hentes dette ved følgende SQL query:
SELECT c u s t o m e r . c u s t o m e r i d , a d d r e s s 1 , download , u p l o a d FROM p r i v a t . customer , p r i v a t . v g e t _ t r a f f i c _ s h a p i n g
WHERE c u s t o m e r . c u s t o m e r i d = v g e t _ t r a f f i c _ s h a p i n g . c u s t o m e r i d AND c u s t o m e r . c u s t o m e r i d = { c u s t o m e r i d }
Hvor customerid er kundenummeret for den ønskede kunde er givet i parame-teren customerid.
Denne query returnerer et resultsæt bestående af en liste af rækker der bliver returneret, på baggrund af disse rækker opbygger vi en SubscriptionInfo for hver række. SubscriptionInfo er vores egen datastruktur bestående af et kundenum-mer, adresse og internet hastighed.
c a s e c l a s s S u b s c r i p t i o n I n f o ( c u s t o m e r I d : Long ,
a d d r e s s : S t r i n g , s p e e d : Speed )
5.3 Implementation af program 57
hvor datastrukuren Speed er defineret som:
c a s e c l a s s Speed ( download : Option [ I n t ] , u p l o a d : Option [ I n t ] )
Alle operationer i vores faktureringsDAO, med undtagelse af getUnionIds, re-turnerer en liste af unikke SubstriptionInfo’s. Denne datastruktur bliver i Scala annoteret som:
S e t [ S u b s c r i p t i o n I n f o ]
Herunder vil vi derfor beskrive de fire DAO operationer der er nødvendige for vores program:
getUsers henter som tidligere nævnt alle aktive brugere ud. Dette gøres ved følgende metode:
d e f g e t U s e r s : S e t [ S u b s c r i p t i o n I n f o ] = {
l o g . i n f o ( s " r e c e i v e d r e q u e s t on a l l u s e r i n f o " )
DB. w i t h C o n n e c t i o n ( t h i s . dbName ) { i m p l i c i t c o n n e c t i o n =>
v a l q u e r y = SQL(
"""
SELECT c u s t o m e r . c u s t o m e r i d , a d d r e s s 1 ,
download , u p l o a d
FROM p r i v a t . customer ,
p r i v a t . v g e t _ t r a f f i c _ s h a p i n g WHERE 1=1
AND c u s t o m e r . c u s t o m e r i d = v g e t _ t r a f f i c _ s h a p i n g . c u s t o m e r i d
"""
)
q u e r y ( ) . map{ row =>
S u b s c r i p t i o n I n f o (
row [ Long ] ( " c u s t o m e r i d " ) , row [ S t r i n g ] ( " a d d r e s s 1 " ) , Speed (
row [ Option [ I n t ] ] ( " download " ) , row [ Option [ I n t ] ] ( " u p l o a d " ) )
)
} . t o S e t }
}
Først åbnes en connection til databasen ved DB.withConnection, dernæst bliver vores SQL query defineret i værdien query, denne bliver nu eksekveret ved at sætte et sæt paranteser og bliver mappet over:
q u e r y ( ) . map
Et map itererer igennem alle medlemmer i en liste og modulerer hvert medlem på baggrund af den kode der er inde i map funktionen. I dette tilfælde modtager vi en liste af SQLRows og for hvert SQLRow i denne liste opretter vi istedet en SubscriptionInfo.
Metoderne getUsers og getUsersByUnion er ens med undtagelse af en lettere variation i SQL querierne, vi har derfor valgt ikke at vise dem. getUser returner ligeledes et Set[SubscriptionInfo], hvor der enten kan være ingen eller et medlem i sættet.
getUnionIds henter alle forenings id’er fra faktureringsdatabasen. Kildeko-den til Kildeko-denne metode ser således ud:
d e f g e t U n i o n I d s : S e t [ Long ] = {
l o g . i n f o ( s " R e c e i v e d r e q u e s t f o r a l l u n i o n i d s " )
DB. w i t h C o n n e c t i o n ( t h i s . dbName ) { i m p l i c i t c o n n e c t i o n =>
v a l q u e r y = SQL(
"""
SELECT DISTINCT t a e l l e r FROM p r i v a t . f o r e n i n g ORDER BY t a e l l e r ;
"""
)
q u e r y ( ) . map{ row =>
row [ Long ] ( " t a e l l e r " ) } . t o S e t
} }
Vi kan derfor se at denne metode ikke tager imod nogle parametre, og den re-turnerer et sæt af Long værdier. Ligesom med getUsers, getUser og getUserByU-nion bliver en database forbindelse først givet ved DB.withConnection, derefter
5.3 Implementation af program 59
bliver det SQL query der skal bruges defineret. Derefter bliver det eksekveret, mappet over og alle medlemmer i sættet bliver lavet om til et tal af typen Long.
Switch.
Vores Switch DAO står for interaktionen med switchdatabasen og har derfor følgende Scala traits.
t r a i t SwitchDokDAO { // i n s e r t methods
d e f i n s e r t U n i o n ( u n i o n : Union ) : Long d e f i n s e r t S w i t c h ( s w i t c h : Switch , u n i o n R e l a t i o n : Option [ Long ] ,
m a n u f a c t u r e r R e l a t i o n : Option [ Long ] ) : Long
d e f i n s e r t S w i t c h D e t a i l ( s w i t c h D e t a i l : S w i t c h D e t a i l , s w i t c h R e l a t i o n : Option [ Long ] ) : Long
d e f i n s e r t A d d r e s s ( a d d r e s s : SDAddress , s w i t c h D e t a i l R e l a t i o n : Option [ Long ] , z i p C o d e R e l a t i o n : Option [ Long ] ) : Long d e f i n s e r t Z i p C o d e ( zipCode : ZipCode ) : Long d e f i n s e r t S M o d e l ( sModel : SModel ,
m a n u f a c t u r e r R e l a t i o n : Option [ Long ] ) : Long
d e f i n s e r t M a n u f a c t u r e r ( m a n u f a c t u r e r : M a n u f a c t u r e r ) : Long d e f i n s e r t C u s t o m e r ( c u s t o m e r : Customer ,
s w i t c h D e t a i l R e l a t i o n : Option [ Long ] ) : Long d e f i n s e r t V l a n ( v l a n : Vlan ,
s w i t c h D e t a i l R e l a t i o n : Option [ Long ] ) : Long d e f c r e a t e D a t a b a s e ( ) : L i s t [ B o o l e a n ]
d e f s e l e c t F r o m ( i n p u t : Any ) : L i s t [ ( S w i t c h D e t a i l ) ] d e f d e l e t e C u s t o m e r ( customerID : I n t ) : B o o l e a n }
Traits bruges til at definere objekt typer i Scala ved at specificere en signatur for de supporterede metoder. En tilsvarende Java metode er Interfaces. I oven-stående kode ses de forskellige traits, med tilhørende datatyper, til at indsætte og søge i databasen. Derudover er der også mulighed for at fjerne en kunde fra databasen via deleteCustomer. Datatyperne er en forlængelse af den generelle datatype SDStructure. Denne type overskriver Scala’s toString metode for nemt at kunne konvertere til SQL syntaxen brugt af Anorm. Hver datatype der for-længer SDStructure har sin egen toString definition til samme formål. Disse datatyper er implementeret i SwitchDok.scala [En oversigt over de forskelle datatypes findes i bilag D].
Som eksempel på en af metoderne der benytter et DAO ses indsættelse af en boligforening nedenfor. Denne er implementeret i SwitchDokDaoImpl:
d e f i n s e r t U n i o n ( u n i o n : Union ) = t h i s . i n s e r t I n t o ( Table ( " Union " , L i s t ( SQLColumn ( " name " ) ) ) , union ,
L i s t . empty ) . g e t O r E l s e ( 0 )
Metoden modtager en union af datatypen Union og denne indeholder et union-navn der er en streng. Navnet indsættes i union databasetabellen via et kald til insertInto.
p r i v a t e d e f i n s e r t I n t o ( t a b l e : Table , i n p u t : S D S t r u c t u r e , r e l a t i o n s : L i s t [ Option [ Long ] ] ) : Option [ Long ] = {
v a l column= t a b l e . columns . map(_. name ) . mkString ( " ( " , " , \ n " , " ) " ) v a l r e l a t i o n V a l u e s = i f ( r e l a t i o n s != L i s t . empty )
r e l a t i o n s . map(_. g e t O r E l s e ( 0 ) ) . mkString ( " , " , " , " , " ) " ) e l s e ""
v a l v a l u e s = i n p u t . t o S t r i n g + r e l a t i o n V a l u e s l o g . i n f o ( v a l u e s )
DB. w i t h C o n n e c t i o n ( t h i s . dbName ) { i m p l i c i t c o n n e c t i o n =>
v a l q u e r y = SQL(
s """
INSERT INTO t s . $ { t a b l e . name} $column VALUES $ v a l u e s
"""
)
q u e r y . e x e c u t e I n s e r t ( ) }
}
5.3 Implementation af program 61
Metoden modtager en tabel hvori data skal indsættes, data der består af en SDStructure og relationerne. Derefter konverteres det hele til en streng og der oprettes forbindelse til switchdatabasen og data indsættes.
For at søge efter kundedata i databasen er der oprettet en selectFrom metode der modtager input af typen Any d.v.s. data kan være en integer eller en streng.Nedenfor ses koden til selectFrom:
d e f s e l e c t F r o m ( i n p u t : Any ) : L i s t [ S w i t c h D e t a i l ] = { DB. w i t h C o n n e c t i o n ( t h i s . dbName ) { i m p l i c i t c o n n e c t i o n =>
l o g . i n f o ( s " i s i n p u t $ i n p u t an i p a d d r e s s :
"+ t h i s . i s I p ( i n p u t . t o S t r i n g ) )
l o g . i n f o ( s " i n p u t $ i n p u t i s o f t y p e $ { i n p u t . g e t C l a s s } " ) v a l column = i n p u t match {
c a s e i n t : I n t => " a . c u s t o m e r = "
c a s e i p : S t r i n g i f ( t h i s . i s I p ( i p ) ) => " sd . i p LIKE "
c a s e a d d r e s s : S t r i n g => " a . street_name LIKE "
}
v a l q u e r y = SQL(
s """
SELECT ∗
FROM t s . s w i t c h _ d e t a i l sd LEFT JOIN t s . a d d r e s s a ON
a . s w i t c h _ d e t a i l _ r e l a t i o n = sd . i d LEFT JOIN t s . o u t e r _ v l a n ov ON
ov . s w i t c h _ d e t a i l _ r e l a t i o n = sd . i d LEFT JOIN t s . i n n e r _ v l a n i v ON
i v . s w i t c h _ d e t a i l _ r e l a t i o n = sd . i d LEFT JOIN t s . z i p _ c o d e z c ON
z c . zi p _ c o d e = a . z i p _ c o d e _ r e l a t i o n WHERE 1=1
AND $ { column }{ p r e p a r e d } ;
"""
)
. on ( " p r e p a r e d " −> t h i s . p r e p a r e S t r i n g ( i n p u t ) ) q u e r y ( ) . map{ row =>
/ / ( ) ,
S w i t c h D e t a i l ( row [ I n t ] ( " p o r t " ) ,
row [ S t r i n g ] ( " i p " ) , row [ S t r i n g ] ( " s c o p e " ) , OuterVlan ( row [ I n t ] ( " o u t e r _ v l a n " ) ) , I n n e r V l a n ( row [ I n t ] ( " i n n e r _ v l a n " ) ) , Some ( row [ Long ] ( " i d " ) ) ,
Some ( SDAddress (
row [ S t r i n g ] ( " street_name " ) , row [ I n t ] ( " number " ) ,
row [ Option [ S t r i n g ] ] ( " f l o o r " ) , row [ Option [ S t r i n g ] ] ( " u n i t " ) , ZipCode (
row [ I n t ] ( " z i p _ c od e " ) , row [ S t r i n g ] ( " c i t y " ) ) , Some ( row [ I n t ] ( " c u s t o m e r " ) ) ) ) )
} . t o L i s t }
}
Efter input er givet forberedes det via en match case. Hvis input er en integer sendes det videre som et customerid. Hvis det er en streng checkes der på om strengen er en ip-adresse eller en adresse. Hvis det er en adresse fjernes al tegnsætning for at simplificere søgningen da adresser indsættes i databasen uden tegnsætning.
Udover alle insert metoderne og selectFrom metoden er der implementeret en DeleteCustomer metode der fjerner et kundenummer fra Databasen hvis kunden ikke længere har et abonnement.
Traffic shaper.
Vores traffic shaper DAO er et DAO, der kommunikerer med traffic shaperen.
Denne er anderledes opbygget end vores to andre DAO, da den ikke snakker direkte med en database, men istedet kommunikerer over en SOAP forbindelse direkte til et traffic shaper API. Som tidligere nævnt skal vores traffic shaper DAO have muligheder for følgende operationer:
• getSubscriber: finder en kunde baseret på outer og inner VLAN.
• addSubscriber: tilføjer en kunde baseret op outer og inner VLAN.
• deleteSubscriber: fjerner en kunde baseret på outer og inner VLAN.
På baggrund af dette har vi lavet et trait, der fungerer som et interface til vores soapklient:
5.3 Implementation af program 63
t r a i t A b s t r a c t S o a p C l i e n t {
p r i v a t e v a l l o g = L o g g e r F a c t o r y
. g e t L o g g e r ( c l a s s O f [ A b s t r a c t S o a p C l i e n t ] ) d e f h e a d e r s : Seq [ ( S t r i n g , S t r i n g ) ] = Seq ( ( " Content−Type " , " t e x t / xml ; c h a r s e t=UTF−8") )
d e f h o s t : S t r i n g
p r o t e c t e d d e f p r e p e n d e d H o s t ( r e l a t i v e : S t r i n g ) : S t r i n g = s " urn : $ h o s t#$ r e l a t i v e "
d e f wrap ( xml : Elem ) : S t r i n g
d e f sendMessage ( r e l a t i v e : S t r i n g , xml : Elem ) : Future [ Option [ Elem ] ]
d e f e r r o r ( msg : S t r i n g ) = {
l o g . e r r o r ( s " S o a p C l i e n t e r r o r : $msg " ) } }
Traitet har en sekvens af de nødvendige headers der er de samme for alle op-erationer. Derudover har den en metode til at finde den korrekte soap-action url i form af prependedHost. PrependedHost tager den soap-action vi ønsker at finde og prepender host adressen på. Wrap er en metode der pakker den givne XML i form af parametrerne til det givne kald, ind som en SOAP pakke som serveren til modtage. Til sidst har vores trait en sendMessage metode der sender en besked over en oprettet soap forbindelse og giver resultatet tilbage som en Future[Option[Elem]] hvor Elem er Scala’s XML datatype.
Da det SOAP API vi ønsker at kommunikere med kræver et brugernavn og adgangskode skaber vi yderligere et lag bestående af et trait ved navn Auth-SoapClient. Denne forlænger vores trait beskrevet ovenfor og tilføjer nogle nye metoder:
p r o t e c t e d d e f username : S t r i n g p r o t e c t e d d e f password : S t r i n g p r o t e c t e d d e f a u t h e n t i c a t e ( r e l a t i v e : S t r i n g ,
c o n t e n t L e n g t h : I n t ) : WSRequestHolder = { v a l head = t h i s . h e a d e r s ++
Seq (
( " SOAPAction " , t h i s . p r e p e n d e d H o s t ( r e l a t i v e ) ) , ( " Content−Length " , c o n t e n t L e n g t h . t o S t r i n g ) )
WS. u r l ( t h i s . h o s t ) . w i t h H e a d e r s ( head : _∗) . withAuth (
t h i s . username , t h i s . password , AuthScheme . BASIC ) }
Det er derfor nu krævet at vi har to strenge, en indeholdende et brugernavn og en et kodeord. Derudover har vi nu fået en authenticate metode der forbereder alle headers nødvendige for SOAP kaldet. Metoden tager i mod en string som er navnet på kaldet og længden af det data der skal sendes med. Det første der bliver gjort er at forberede en sekvens af alle headers i værdien head, derefter bliver den sat på en ny url til den givne hostadresse fra vores AbstractSoap-Client. Til sidst bruges metoden .withAuth hvor man bekræfter brugernavn og adgangskoden, samt hvilken krypteringsmetode af disse der skal bruges.
Vi er derfor nu klar til at implementere vores to traits. Dette gøres i klassen IPNettSoapClient:
c l a s s I P N e t t S o a p C l i e n t ( v a l h o s t : S t r i n g ,
v a l username : S t r i n g ,
v a l password : S t r i n g ) e x t e n d s A u t h S o a p C l i e n t { d e f wrap ( xml : Elem ) : S t r i n g = {
v a l b u f f e r = new S t r i n g B u i l d e r
b u f f e r . append ("<?xml v e r s i o n =\"1.0\"
e n c o d i n g =\"UTF−8\"
s t a n d a l o n e =\"no\"?>\n " )
. append("< s o a p e n v : E n v e l o p e " +
" xmlns : x s i=
\" h t t p : / /www. w3 . o r g /2001/XMLSchema−i n s t a n c e \" " +
" xmlns : xsd=
\" h t t p : / /www. w3 . o r g /2001/XMLSchema\" " +
" xmlns : s o a p e n v=
\" h t t p : / / schemas . xmlsoap . o r g / s o a p / e n v e l o p e /\" " +
" xmlns : i p e=
\" urn :"+ t h i s . h o s t +"\">\n " ) . append ("< s o a p e n v : Header/>\n " ) . append("< s o a p e n v : Body>\n " ) . append ( xml . t o S t r i n g ( ) )
5.3 Implementation af program 65
. append ( " \ n</s o a p e n v : Body>\n " ) . append ("</ s o a p e n v : Envelope >\n " ) . t o S t r i n g ( )
}
d e f sendMessage ( r e l a t i v e : S t r i n g ,
xml : Elem ) : Future [ Option [ Elem ] ] = { v a l c o n t e n t = wrap ( xml )
v a l u r l = t h i s . a u t h e n t i c a t e ( r e l a t i v e ,
c o n t e n t . g e t B y t e s ( ) . l e n g t h )
u r l . p o s t ( c o n t e n t ) . map{ r e s p o n s e =>
Some ( r e s p o n s e . xml ) }
} }
Denne klasse implementerer de to metoder fra AbstractSoapClient der endnu ikke er implementeret, nemlig wrap og sendMessage. Wrap bygger en streng der er bestående af det XML der skal sendes som en besked til SOAP API’et.
På baggrund af soap headeren som vi definerer i AuthSoapClient’s authenticate metode og denne besked vil API’et svare. SendMessage modtager en soap-action der skal foretages i form af strengen relative, og den XML vi ønsker at bruge som input parametre. SendMessage opretter nu en forbindelse til vores host og sender beskeden afsted. Svaret vi får tilbage sikrer vi os at det er af typen Future[Option[Elem]] ved at mappe over den Future vi får tilbage og gøre følgende:
. map{ r e s p o n s e =>
Some ( r e s p o n s e . xml ) }
Med disse beskrevne metoder kan vi derfor implementere alle de DAO opera-tioner som vi ønsker. Disse er dog blevet implementeret i en TSSoapActor og vil blive beskrevet senere [Se afsnit 5.3.3].