DIRECTORY DB USING [Aborted, AbortTransaction, B2V, BoolType, CloseTransaction, DeclareAttribute, DeclareDomain, DeclareEntity, DeclareIndex, DeclareRelation, DeclareRelship, DeclareSegment, DestroyRelship, Error, Failure, GetF, GetP, I2V, Initialize, IntType, MarkTransaction, NameOf, OpenTransaction, S2V, SetF, SetP, RopeType, T2V, TimeType, TransactionOf, V2B, V2E, V2I, V2S, V2T], GVBasics USING [RName], IO USING [Close, GetCard, RIS, STREAM], Log USING [ Problem ], Nuthatch USING [EncryptionKey, ID, IDType, NHTime, NuthatchUserHandle, pd, Tune, VoiceFileID], NuthatchDB, Rope USING [ROPE], BasicTime USING [earliestGMT, Now, Update, GMT, nullGMT], Thrush USING [nullKey], UserCredentials USING [Get], UserProfile USING [ Token ] ; NuthatchDBImpl: CEDAR MONITOR IMPORTS DB, IO, BasicTime, Log, Nuthatch, UserCredentials, UserProfile EXPORTS NuthatchDB = { OPEN NuthatchDB, pd: Nuthatch.pd; VoiceMessageDomain: PUBLIC Domain; interestRelation: PUBLIC Relation; irMsg: PUBLIC Attribute; irUser: PUBLIC Attribute; irID: PUBLIC Attribute; irIDType: PUBLIC Attribute; irInterest: PUBLIC Attribute; msgInfo: PUBLIC Relation; miMsg: PUBLIC Attribute; miTuneNumber: PUBLIC Attribute; miRecordTime: PUBLIC Attribute; miCreator: PUBLIC Attribute; miSamples: PUBLIC Attribute; miStartSample: PUBLIC Attribute; miExpirationDate: PUBLIC Attribute; miEncryptionKey: PUBLIC Attribute; miType: PUBLIC Attribute; miReferenceCount: PUBLIC Attribute; nuthatchLogInfo: PUBLIC Relation; nlLogReadPoint: PUBLIC Attribute; nlLogFileName: PUBLIC Attribute; nlUser: PUBLIC Attribute; InitializeDB: PUBLIC ENTRY PROC[close: BOOL_TRUE] RETURNS [success: BOOL_FALSE, logReadPoint: INT] = { abortFirst: BOOL_FALSE; { ENABLE { UNWIND => NULL; DB.Error, DB.Failure => GOTO Failed; DB.Aborted => { abortFirst _ TRUE; RETRY; }; }; relship: Relship_NIL; segmentNoGood: BOOL_FALSE; alreadyOpen: BOOL_FALSE; filePath: Rope.ROPE=UserProfile.Token[ key: "NuthatchSegment", default: "[Luther.Alpine]Strowger>Nuthatch.Segment"]; IF abortFirst THEN AbortNuthatchTransaction[]; abortFirst_FALSE; logReadPoint_0; DB.Initialize[]; DB.DeclareSegment[filePath: filePath, segment: $Nuthatch, number: 250B ! DB.Error => IF code=$FileNotFound OR code=$IllegalFileName THEN {segmentNoGood_TRUE; CONTINUE} ELSE IF code=$TransactionAlreadyOpen THEN {alreadyOpen_ TRUE; RESUME} ]; IF segmentNoGood THEN RETURN; IF alreadyOpen THEN CloseNuthatchTransaction[!DB.Error=>CONTINUE]; OpenNuthatchTransaction[]; relship_DB.DeclareRelship[nuthatchLogInfo, LIST[[nlUser, DB.S2V[UserCredentials.Get[].name]]]]; IF relship#NIL THEN logReadPoint_DB.V2I[DB.GetF[relship, nlLogReadPoint]]; IF close THEN CloseNuthatchTransaction[]; success_TRUE; EXITS Failed => { AbortNuthatchTransaction[]; success _ FALSE; Log.Problem["Problem initializing Nuthatch Database", $System]; }; }; }; InitializeDBVars: PROC = { VoiceMessageDomain_ DB.DeclareDomain["VoiceMessage", $Nuthatch]; interestRelation_ DB.DeclareRelation["interestRelation", $Nuthatch]; irMsg_ DB.DeclareAttribute[interestRelation, "irMsg", VoiceMessageDomain]; irUser_ DB.DeclareAttribute[interestRelation, "irUser", DB.RopeType]; irID_ DB.DeclareAttribute[interestRelation, "irID", DB.RopeType]; irIDType_ DB.DeclareAttribute[interestRelation, "irIDType", DB.RopeType]; irInterest_ DB.DeclareAttribute[interestRelation, "irInterest", DB.BoolType]; -- index1: Index -- []_ DB.DeclareIndex[interestRelation, LIST[irUser, irID, irIDType]]; msgInfo_ DB.DeclareRelation["msgInfo", $Nuthatch]; miMsg_ DB.DeclareAttribute[msgInfo, "miMsg", VoiceMessageDomain, Key]; miTuneNumber_ DB.DeclareAttribute[msgInfo, "TuneNumber", DB.IntType]; miRecordTime_ DB.DeclareAttribute[msgInfo, "miRecordTime", DB.TimeType]; miCreator_ DB.DeclareAttribute[msgInfo, "miCreator", DB.RopeType]; miSamples_ DB.DeclareAttribute[msgInfo, "miSamples", DB.IntType]; miStartSample_ DB.DeclareAttribute[msgInfo, "miStartSample", DB.IntType]; miExpirationDate_ DB.DeclareAttribute[msgInfo, "miexpirationDate", DB.TimeType]; miEncryptionKey_ DB.DeclareAttribute[msgInfo, "miEncryptionKey", DB.RopeType]; miType_ DB.DeclareAttribute[msgInfo, "miType", DB.RopeType]; miReferenceCount_ DB.DeclareAttribute[msgInfo, "miReferenceCount", DB.IntType]; nuthatchLogInfo_ DB.DeclareRelation["nuthatchLogInfo", $Nuthatch]; nlLogReadPoint_ DB.DeclareAttribute[nuthatchLogInfo, "nlLogReadPoint", DB.IntType]; nlUser_ DB.DeclareAttribute[nuthatchLogInfo, "nlUser", DB.RopeType]; }; OpenNuthatchTransaction: PROC = { IF pd.transactionOpen THEN RETURN; pd.transactionOpen _ FALSE; DB.OpenTransaction[segment: $Nuthatch, userName: UserCredentials.Get[].name, password: UserCredentials.Get[].password, useTrans: NIL, noLog: FALSE ! DB.Error => IF code=TransactionAlreadyOpen THEN { pd.transactionOpen_ TRUE; CONTINUE } ]; IF ~pd.transactionOpen THEN InitializeDBVars[]; pd.transactionOpen _ TRUE; }; AbortTransaction: PUBLIC ENTRY PROC = { ENABLE UNWIND => NULL; AbortNuthatchTransaction[]; }; AbortNuthatchTransaction: INTERNAL PROC = { pd.transactionOpen _ FALSE; IF DB.TransactionOf[$Nuthatch]#NIL THEN DB.AbortTransaction[DB.TransactionOf[$Nuthatch]!DB.Failure=>CONTINUE]; }; CloseNuthatchTransaction: INTERNAL PROC = { pd.transactionOpen _ FALSE; IF DB.TransactionOf[$Nuthatch]#NIL THEN DB.CloseTransaction[DB.TransactionOf[$Nuthatch]]; }; CloseTransaction: PUBLIC ENTRY PROC = { ENABLE UNWIND => NULL; CloseNuthatchTransaction[]; }; MarkTransaction: PUBLIC ENTRY PROC = { ENABLE UNWIND => NULL; MarkNuthatchTransaction[]; }; MarkNuthatchTransaction: INTERNAL PROC = { IF DB.TransactionOf[$Nuthatch]#NIL THEN DB.MarkTransaction[DB.TransactionOf[$Nuthatch]]; }; Catalog: INTERNAL PROC[ msg: Nuthatch.VoiceFileID_ NIL, tuneNumber: INT_ -1, recordTime: BasicTime.GMT _ BasicTime.nullGMT, creator: GVBasics.RName_ NIL, samples: INT_ -1, startSample: INT_ -1, expirationDate: BasicTime.GMT _ BasicTime.nullGMT, encryptionKeyRope: Rope.ROPE_ NIL, type: Rope.ROPE_ NIL, referenceCount: INT_ 0 ] RETURNS [New: BOOL_FALSE] = { entity: Entity; OpenNuthatchTransaction[]; entity _ DB.DeclareEntity[VoiceMessageDomain, msg, OldOnly]; recordTime_Nuthatch.NHTime[recordTime]; IF entity=NIL THEN { -- entry is new -- entity _ DB.DeclareEntity[VoiceMessageDomain, msg]; New_TRUE; [] _ DB.SetP[entity, miTuneNumber, DB.I2V[tuneNumber]]; [] _ DB.SetP[entity, miRecordTime, DB.T2V[[recordTime]]]; [] _ DB.SetP[entity, miCreator, DB.S2V[creator]]; [] _ DB.SetP[entity, miSamples, DB.I2V[samples]]; [] _ DB.SetP[entity, miStartSample, DB.I2V[startSample]]; IF expirationDate=BasicTime.nullGMT THEN expirationDate_ BasicTime.Update[BasicTime.Now[], LONG[14] * 24 * 60 * 60]; -- Two weeks from now, in seconds [] _ DB.SetP[entity, miExpirationDate, DB.T2V[[expirationDate]]]; [] _ DB.SetP[entity, miEncryptionKey, DB.S2V[encryptionKeyRope]]; [] _ DB.SetP[entity, miType, DB.S2V[type]]; [] _ DB.SetP[entity, miReferenceCount, DB.I2V[referenceCount]]; } ELSE { -- entry already exists. Only update attributes with interesting parameters. -- IF tuneNumber#-1 THEN [] _ DB.SetP[entity, miTuneNumber, DB.I2V[tuneNumber]]; [] _ DB.SetP[entity, miRecordTime, DB.T2V[[recordTime]]]; IF creator#NIL THEN [] _ DB.SetP[entity, miCreator, DB.S2V[creator]]; IF samples#-1 THEN [] _ DB.SetP[entity, miSamples, DB.I2V[samples]]; IF startSample#-1 THEN [] _ DB.SetP[entity, miStartSample, DB.I2V[startSample]]; [] _ DB.SetP[entity, miExpirationDate, DB.T2V[[expirationDate]]]; IF encryptionKeyRope#NIL THEN [] _ DB.SetP[entity, miEncryptionKey, DB.S2V[encryptionKeyRope]]; IF type#NIL THEN [] _ DB.SetP[entity, miType, DB.S2V[type]]; IF referenceCount#0 THEN [] _ DB.SetP[entity, miReferenceCount, DB.I2V[referenceCount + DB.V2I[DB.GetP[entity, miReferenceCount]]]]; New_FALSE; }; }; CatalogVoiceFile: PUBLIC ENTRY PROC[ msg: Nuthatch.VoiceFileID_ NIL, tuneNumber: INT_ -1, recordTime: BasicTime.GMT _ BasicTime.nullGMT, creator: GVBasics.RName_ NIL, samples: INT_ -1, startSample: INT_ -1, expirationDate: BasicTime.GMT _ BasicTime.nullGMT, encryptionKeyRope: Rope.ROPE_ NIL, type: Rope.ROPE_ NIL, referenceCount: INT_ 0, close: BOOL_TRUE ] RETURNS [New: BOOL_FALSE] = { ENABLE UNWIND => NULL; New _ Catalog[ msg, tuneNumber, recordTime, creator, samples, startSample, expirationDate, encryptionKeyRope, type, referenceCount]; IF close THEN CloseNuthatchTransaction[]; }; GetVoiceFileID: PUBLIC ENTRY PROC[ user: GVBasics.RName, refIDType: Rope.ROPE, refID: Rope.ROPE, close: BOOL_TRUE] RETURNS [voiceFileID: Nuthatch.VoiceFileID_ NIL] = { ENABLE UNWIND => NULL; relship: Relship; OpenNuthatchTransaction[]; relship _ DB.DeclareRelship[interestRelation, LIST[[irUser, DB.S2V[user]], [irIDType, DB.S2V[refIDType]], [irID, DB.S2V[refID]]] , OldOnly]; IF relship#NIL THEN voiceFileID _ DB.NameOf[DB.V2E[DB.GetF[relship, irMsg]]]; IF close THEN CloseNuthatchTransaction[]; }; GetVoiceFileEntry: PUBLIC ENTRY PROC[ msg: Nuthatch.VoiceFileID_ NIL, close: BOOL_TRUE] RETURNS [ tuneNumber: Nuthatch.Tune_ -1, recordTime: BasicTime.GMT, creator: GVBasics.RName_ NIL, samples: INT_ -1, startSample: INT_ -1, expirationDate: BasicTime.GMT, encryptionKey: Nuthatch.EncryptionKey_Thrush.nullKey, type: Rope.ROPE_NIL , referenceCount: INT_ 0, found: BOOL_FALSE] = TRUSTED { ENABLE UNWIND=>NULL; cardKey: LONG POINTER TO ARRAY[0..2) OF LONG CARDINAL=LOOPHOLE[LONG[@encryptionKey]]; keyStream: IO.STREAM; entity: Entity; OpenNuthatchTransaction[]; entity _ DB.DeclareEntity[VoiceMessageDomain, msg, OldOnly]; recordTime _ BasicTime.earliestGMT; expirationDate _ BasicTime.earliestGMT; IF entity#NIL THEN { tuneNumber_ DB.V2I[DB.GetP[entity, miTuneNumber]]; recordTime_ DB.V2T[DB.GetP[entity, miRecordTime]]; creator_ DB.V2S[DB.GetP[entity, miCreator]]; samples_ DB.V2I[DB.GetP[entity, miSamples]]; startSample_ DB.V2I[DB.GetP[entity, miStartSample]]; expirationDate_ DB.V2T[DB.GetP[entity, miExpirationDate]]; type_ DB.V2S[DB.GetP[entity, miType]]; referenceCount_ DB.V2I[DB.GetP[entity, miReferenceCount]]; found_ TRUE; keyStream _ IO.RIS[DB.V2S[DB.GetP[entity, miEncryptionKey]]]; MarkNuthatchTransaction[]; cardKey[0] _ IO.GetCard[keyStream]; cardKey[1] _ IO.GetCard[keyStream]; IO.Close[keyStream]; }; IF close THEN CloseNuthatchTransaction[]; }; MakeInterestEntry: PUBLIC ENTRY PROC[ voiceFileID: Nuthatch.VoiceFileID, refIDType: Nuthatch.IDType, refID: Nuthatch.ID, user: GVBasics.RName, close: BOOL_TRUE ] = { ENABLE UNWIND => NULL; entity: Entity; relship: Relship; OpenNuthatchTransaction[]; entity _ DB.DeclareEntity[VoiceMessageDomain, voiceFileID]; relship _ DB.DeclareRelship[interestRelation, LIST[[irMsg, entity], [irUser, DB.S2V[user]], [irIDType, DB.S2V[refIDType]], [irID, DB.S2V[refID]]] --, NewOrOld --]; IF close THEN CloseNuthatchTransaction[]; }; AddInterest: PUBLIC ENTRY PROC[ voiceFileID: Nuthatch.VoiceFileID_NIL, refIDType: Nuthatch.IDType_NIL, refID: Nuthatch.ID_NIL, user: GVBasics.RName_NIL, close: BOOL_TRUE ] = { ENABLE UNWIND => NULL; old: BOOL; relship: Relship; OpenNuthatchTransaction[]; IF voiceFileID#NIL THEN { entity: Entity _ DB.DeclareEntity[VoiceMessageDomain, voiceFileID]; relship _ DB.DeclareRelship[interestRelation, LIST[[irMsg, entity], [irUser, DB.S2V[user]], [irIDType, DB.S2V[refIDType]], [irID, DB.S2V[refID]]] , NewOrOld]; } ELSE relship _ DB.DeclareRelship[interestRelation, LIST[[irUser, DB.S2V[user]], [irIDType, DB.S2V[refIDType]], [irID, DB.S2V[refID]]], OldOnly]; IF relship=NIL THEN NULL -- New entry, so message was never received! Error! -- ELSE { -- Old entry; only bump reference count if interest is new -- interested: BOOL_ DB.V2B[DB.GetF[relship, irInterest]]; IF NOT interested THEN { DB.SetF[relship, irInterest, DB.B2V[TRUE]]; voiceFileID _ DB.NameOf[DB.V2E[DB.GetF[relship, irMsg]]]; IF voiceFileID#NIL THEN old_IncrementReferenceCount[voiceFileID: voiceFileID]; }; }; IF close THEN CloseNuthatchTransaction[]; }; LoseInterest: PUBLIC ENTRY PROC[ voiceFileID: Nuthatch.VoiceFileID_NIL, refIDType: Nuthatch.IDType_NIL, refID: Nuthatch.ID_NIL, user: GVBasics.RName_NIL, close: BOOL_TRUE ] = { ENABLE UNWIND => NULL; relship: Relship; old: BOOL; OpenNuthatchTransaction[]; IF voiceFileID#NIL THEN { entity: Entity _ DB.DeclareEntity[VoiceMessageDomain, voiceFileID]; relship _ DB.DeclareRelship[interestRelation, LIST[[irMsg, entity], [irUser, DB.S2V[user]], [irIDType, DB.S2V[refIDType]], [irID, DB.S2V[refID]]] , NewOrOld]; } ELSE relship _ DB.DeclareRelship[interestRelation, LIST[[irUser, DB.S2V[user]], [irIDType, DB.S2V[refIDType]], [irID, DB.S2V[refID]]], OldOnly]; IF relship#NIL AND DB.V2B[DB.GetF[relship, irInterest]] THEN { voiceFileID _ DB.NameOf[DB.V2E[DB.GetF[relship, irMsg]]]; DB.SetF[relship, irInterest, DB.B2V[FALSE]]; IF voiceFileID#NIL THEN old _ DecrementReferenceCount[voiceFileID: voiceFileID]; }; -- voiceFileID=NIL is an Error, shouldn't happen. All interest entries have voiceFileID. IF close THEN CloseNuthatchTransaction[]; }; RemoveInterestEntry: PUBLIC ENTRY PROC[ voiceFileID: Nuthatch.VoiceFileID, refIDType: Nuthatch.IDType, refID: Nuthatch.ID, user: GVBasics.RName, close: BOOL_TRUE ] = { ENABLE UNWIND => NULL; entity: Entity; relship: Relship_NIL; OpenNuthatchTransaction[]; IF voiceFileID#NIL THEN { entity _ DB.DeclareEntity[VoiceMessageDomain, voiceFileID]; IF entity#NIL THEN relship _ DB.DeclareRelship[interestRelation, LIST[[irMsg, entity], [irUser, DB.S2V[user]], [irIDType, DB.S2V[refIDType]], [irID, DB.S2V[refID]]] , OldOnly]; } ELSE relship _ DB.DeclareRelship[interestRelation, LIST[[irUser, DB.S2V[user]], [irIDType, DB.S2V[refIDType]], [irID, DB.S2V[refID]]], OldOnly]; IF relship#NIL THEN { IF DB.V2B[DB.GetF[relship, irInterest]] THEN { IF voiceFileID = NIL THEN voiceFileID _ DB.NameOf[DB.V2E[DB.GetF[relship, irMsg]]]; IF voiceFileID # NIL THEN [] _ DecrementReferenceCount[voiceFileID: voiceFileID]; }; DB.DestroyRelship[relship]; }; IF close THEN CloseNuthatchTransaction[]; }; IncrementReferenceCount: INTERNAL PROC [ voiceFileID: Nuthatch.VoiceFileID_NIL] RETURNS [Old: BOOL_TRUE] = { msg: Entity _ DB.DeclareEntity[VoiceMessageDomain, voiceFileID, OldOnly]; IF msg#NIL THEN -- voice message exists -- [] _ DB.SetP[msg, miReferenceCount, DB.I2V[1+DB.V2I[DB.GetP[msg, miReferenceCount]]]] ELSE { -- there's no entry to up ref count for -- Old_FALSE; [] _ Catalog[msg: voiceFileID, referenceCount: 1]; }; }; DecrementReferenceCount: INTERNAL PROC [ voiceFileID: Nuthatch.VoiceFileID_NIL] RETURNS [Old: BOOL_FALSE] = { oldRefCount: INT; msg: Entity _ DB.DeclareEntity[VoiceMessageDomain, voiceFileID, OldOnly]; IF msg=NIL THEN RETURN; -- There's no entry to up ref count for Old_TRUE; oldRefCount_ DB.V2I[DB.GetP[msg, miReferenceCount]]; IF oldRefCount<=0 THEN RETURN; [] _ DB.SetP[msg, miReferenceCount, DB.I2V[DB.V2I[DB.GetP[msg, miReferenceCount]]-1]]; }; SetReadPoint: PUBLIC ENTRY PROC [nuthatchUserHandle: Nuthatch.NuthatchUserHandle, close: BOOL_TRUE] = { ENABLE UNWIND => NULL; relship: Relship; OpenNuthatchTransaction[]; relship_DB.DeclareRelship[nuthatchLogInfo, LIST[[nlUser,DB.S2V[UserCredentials.Get[].name]]]]; IF relship#NIL THEN DB.SetF[relship, nlLogReadPoint, DB.I2V[nuthatchUserHandle.logReadPoint]]; IF close THEN CloseNuthatchTransaction[]; }; }. À-- NuthatchDBImpl.mesa --Last Edited by: Lia, September 30, 1983 11:46 am --Last Edited by: Swinehart, April 30, 1984 2:15:04 pm PDT Setting up DB segment. -- start up DB code -- Check out DB segment. -- Define the schema. -- Clients are responsible for all transaction restart management (catching DB.various) cardKey: LONG POINTER TO ARRAY[0..2) OF LONG CARDINAL=LOOPHOLE [LONG[@encryptionKey]]; keyRope: Rope.ROPE _IO.PutFR["%bB %bB", IO.card[cardKey[0]], IO.card[cardKey[1]]];-- If the entry is new, updates all of its attributes. If it's old, just update the non-defaulted ones. get tune number and recording time from the voiceFileId? -- This routine looks in the interest relation to see if there is an interest entry relating this user, refIDType, and refID to a VoiceFileID. IF so, it is returned. Convert entity to VoiceFileID This routine is called to read an entry in the voice file directory. If there is an entry with its VoiceFileID equal to msg, then the entry is returned and found is set to TRUE. Otherwise, found is set to FALSE and the default parameters are returned. -- Open DB.V2S[DB.GetP[entity, miEncryptionKey]] as a stream keyStream. -- If there's no relship, it's a new entry, so message was never received! Unhandled Error! Otherwise it's an old entry; only decrease reference count if interest is old. RemoveInterestEntry eliminates the interest-describing relationship if it exists. <> Look for the directory entry for the voice message whose VoiceFileID is voiceFileID. If there is one, increment its reference count. If not, do what??? Create one and wait for the entry to appear. (Could happen if logs are being played back, and the receiver's log precedes the recorder's. create entry for this voice message, with 1 as reference count. -- Look for the directory entry for the voice message whose VoiceFileID is voiceFileID. If there is one, decrement its reference count. There must already be some entry for this voice file, since no log can contain a decrement entry without a preceding increment entry. -- Create entry for this voice message, with 1 as reference count. Modify CatalogVoiceFile?-- Makes sense to decrement -- Create entry for this voice message, with 1 as reference count. Modify CatalogVoiceFile?-- Get the current read point for the user's log.-- Ê!˜Jšœ™Jšœ2™2šœ:™:J˜—šÏk ˜ Jšœœï˜÷Jšœ œ ˜Jšœœœœ˜'Jšœœ ˜Jšœ œœ>˜_J˜ Jšœœœ˜Jšœ œœ ˜9Jšœœ ˜Jšœœ˜Jšœ œ ˜J˜J˜—šœœ˜š˜Jšœœ9˜?—Jšœ˜J˜Jšœ˜!J˜Jšœœ˜"Jšœœ ˜"Jšœœ ˜Jšœœ ˜Jšœœ ˜Jšœ œ ˜Jšœ œ ˜J˜Jšœ œ ˜Jšœœ ˜Jšœœ ˜Jšœœ ˜Jšœ œ ˜Jšœ œ ˜Jšœœ ˜ Jšœœ ˜#Jšœœ ˜"Jšœœ ˜Jšœœ ˜#J˜Jšœœ ˜!Jšœœ ˜!Jšœœ ˜ Jšœœ ˜—J˜šÏn œœœœœœœ œœœ˜fJšœ™Jšœ œœ˜˜šœ˜Jšœœ˜Jšœœ œ˜$Jšœœœ˜,J˜—Jšœœ˜Jšœœœ˜Jšœ œœ˜šœœ˜&J˜J˜?—Jšœ œ˜.Jšœ œ˜J˜Jšœ™Jšœ˜Jšœ™šœGœ ˜Uš œœœœœ˜RJš œœœœœ˜E—J˜—Jšœœœ˜Jšœ œœœ˜BJ˜Jšœœ!œ œ$˜_Jš œ œœœœ ˜JJšœœ˜)Jšœœ˜ š˜˜ J˜Jšœ œ˜Jšœ?˜?J˜——J˜—Jšœ˜J˜J˜—šžœœ˜Jšœ™Jšœœ*˜@J˜Jšœœ0˜DJšœœA˜JJšœœ.œ ˜EJšœœ,œ ˜AJšœ œ0œ ˜IJšœ œ2œ ˜MJ˜JšÏcœœ œ˜XJ˜Jšœ œ'˜2Jšœœ=˜FJšœœ)œ ˜EJšœœ+œ ˜HJšœ œ(œ ˜BJšœ œ(œ ˜AJšœœ,œ ˜IJšœœ/œ ˜PJšœœ.œ ˜NJšœœ%œ ˜Jšœœœœ˜9Jšœœœ˜,Jšœ œœ9˜PJ˜—JšŸY˜YJšœœ˜)Jšœ˜J˜—šžœœœœ˜'J˜#J˜Jšœœ˜J˜Jšœœ˜J˜JšœQ™Q™fJ™B—Jšœœœ˜Jšœ˜Jšœœ˜Jšœ˜šœ œœ˜Jšœ œ0˜;Jšœœœ œ"œœœœ˜°Jšœ˜—Jš œ œ"œ œœœ˜šœ œœ˜šœœœ˜.Jš œœœœœœ˜SJšœœœ8˜QJ˜—Jšœ˜J˜—Jšœœ˜)Jšœ˜J˜—šžœœœ˜(Jšœ"œ˜&Jšœ œ˜Jšœ¥™¥Jšœœ9˜IšœœŸœ˜+Jš œœœœœ˜U—šœŸ*œ˜2Jšœœ˜ JšœC™CJ˜2J˜—Jšœ˜J˜—šžœ œ˜(Jšœ"œ˜'Jšœœœ˜Jšœ‘™‘Jšœ œ˜Jšœœ9˜IJš œœœœŸ'˜?Jšœ[™[Jšœœ˜ Jšœ œœ˜4Jšœœœ˜JšŸ™Jš œœœœœ"˜VJšœ[™[Jšœ˜J˜—š ž œœœœ:œœ˜gJšœœœ˜Jšœ0™0Jšœ˜Jšœ˜Jšœœ!œ œ$˜^šœ œ˜Jšœœ'˜J—Jšœœ˜)Jšœ˜J˜—Jšœ˜J˜J˜—…—;VY7