DIRECTORY CIFS, Commander, Environment, FileIO, FileLookup, FtpMan, IO, Process, RefText, DirectoryList, Rope, System, UserCredentials; FileTransferCommandsImpl: CEDAR MONITOR IMPORTS CIFS, Commander, FileIO, FileLookup, FtpMan, IO, Process, RefText, DirectoryList, Rope, UserCredentials ~ BEGIN defaultStoreTextServer: Rope.ROPE _ "Maxc"; defaultPublicServer: Rope.ROPE _ "Indigo"; defaultPersonalServer: Rope.ROPE _ "Ivy"; pauseAmount: Process.Milliseconds _ 1000; DoOperationProc: TYPE ~ PROC [host, directory, filename: Rope.ROPE, log: IO.STREAM] RETURNS [quit: BOOLEAN _ FALSE]; StoreTextCommand: Commander.CommandProc ~ {DoOpForEachFile[StoreTextOp, cmd, defaultStoreTextServer, defaultStoreTextServer, local]}; StoreCommand: Commander.CommandProc ~ {DoOpForEachFile[StoreOp, cmd, defaultPublicServer, defaultPersonalServer, local]}; RetrieveCommand: Commander.CommandProc ~ {DoOpForEachFile[RetrieveOp, cmd, defaultPublicServer, defaultPersonalServer, remote]}; ListRemoteCommand: Commander.CommandProc ~ {DoOpForEachFile[ListRemoteOp, cmd, defaultPublicServer, defaultPersonalServer, remoteAll]}; ListVerboseCommand: Commander.CommandProc ~ {DoOpForEachFile[ListVerboseOp, cmd, defaultPublicServer, defaultPersonalServer, remoteAll]}; StoreTextOp: ENTRY DoOperationProc ~ TRUSTED { ENABLE UNWIND => NULL; {ENABLE CIFS.Error => {log.PutRope[" "]; log.PutRope[error]; log.PutRope["\n"]; GOTO Quit}; tempStream: IO.STREAM _ FileIO.Open["StoreText$$$", overwrite]; source: IO.STREAM _ FileIO.Open[filename ! FileIO.OpenFailed => CHECKED { log.PutRope[fileName]; IF why = fileNotFound THEN log.PutRope[" not found.\n"] ELSE IF why = illegalFileName THEN log.PutRope[": illegal file name.\n"] ELSE log.PutRope[": open error.\n"]; GOTO Quit }]; text: REF TEXT _ RefText.ObtainScratch[1024]; version: INT; WHILE NOT source.EndOf DO nBytesRead: NAT _ source.GetBlock[text]; tempStream.PutBlock[text, 0, nBytesRead]; ENDLOOP; tempStream.Close; source.Close; RefText.ReleaseScratch[text]; log.PutF["Storing text into [%g]%g%g", IO.rope[host], IO.rope[directory], IO.rope[filename]]; version _ FtpMan.Store[host, PutInSlashes[directory].Concat[filename], FileIO.CapabilityFromStream[FileIO.Open["StoreText$$$"]]]; log.PutF["!%g\n", IO.int[version]]; EXITS Quit => {quit_TRUE}}}; StoreOp: DoOperationProc ~ TRUSTED { ENABLE UNWIND => NULL; {ENABLE CIFS.Error => {log.PutRope[" "]; log.PutRope[error]; log.PutRope["\n"]; GOTO Quit}; source: CIFS.OpenFile _ CIFS.Open[filename, CIFS.read]; version: INT; log.PutF["Storing [%g]%g%g", IO.rope[host], IO.rope[directory], IO.rope[filename]]; version _ FtpMan.Store[host, PutInSlashes[directory].Concat[filename], CIFS.GetFC[source]]; source.Close; log.PutF["!%g\n", IO.int[version]]; EXITS Quit => {quit_TRUE}}}; RetrieveOp: DoOperationProc ~ TRUSTED { ENABLE UNWIND => NULL; {ENABLE CIFS.Error => {log.PutRope[" "]; log.PutRope[error]; log.PutRope["\n"]; GOTO Quit}; dest: CIFS.OpenFile _ CIFS.Open[RemoveVersionNumber[filename], CIFS.replace]; log.PutF["Retrieving [%g]%g%g . . . ", IO.rope[host], IO.rope[directory], IO.rope[filename]]; FtpMan.Retrieve[host, PutInSlashes[directory].Concat[filename], CIFS.GetFC[dest]]; dest.Close; log.PutF["Done.\n"]; EXITS Quit => {quit_TRUE}}}; ListRemoteOp: DoOperationProc ~ { log.PutF["[%g]%g%g\n", IO.rope[host], IO.rope[directory], IO.rope[filename]]; }; paddedNameLength: INT _ 41; ListVerboseOp: DoOperationProc ~ { result: FileLookup.Result; version: CARDINAL; create: System.GreenwichMeanTime; count: LONG CARDINAL; nameLength: INT ~ host.Length + directory.Length + filename.Length + 2; log.PutF["[%g]%g%g", IO.rope[host], IO.rope[directory], IO.rope[filename]]; THROUGH [nameLength..paddedNameLength) DO log.PutChar[' ] ENDLOOP; [result: result, version: version, create: create, count: count] _ FileLookup.LookupFile[host, directory.Concat[filename]]; SELECT result FROM noResponse => log.PutF["no response from %g.\n", IO.rope[host]]; noSuchName => log.PutRope["does not exist.\n"]; noSuchPort => log.PutRope["no such port.\n"]; ok => log.PutF[" Of %g --%5d\n", IO.time[create], IO.card[count]]; ENDCASE => ERROR; }; ParsePath: PROC [name: Rope.ROPE] RETURNS [host, directory, filename: Rope.ROPE _ NIL] ~ { hostStart, hostEnd, directoryStart, directoryEnd: INT _ 0; filenameStart, filenameEnd: INT _ name.Length; state: NAT _ 0; loc: INT _ 0; Action: PROC [c: CHAR] RETURNS [BOOL_FALSE] ~ { SELECT state FROM 0 => IF c='[ THEN {hostStart _ loc+1; state _ 1} ELSE IF c='< THEN {directoryStart _ loc; directoryEnd _ name.Length; state _ 3} ELSE {directoryStart _ directoryEnd _ filenameStart _ loc; state _ 3}; 1 => IF c='] THEN {hostEnd _ loc; state _ 2} ELSE IF NOT Rope.Letter[c] THEN RETURN [TRUE]; 2 => IF c='< THEN {directoryStart _ loc; directoryEnd _ name.Length; state _ 3} ELSE {directoryStart _ directoryEnd _ filenameStart _ loc; state _ 3}; 3 => IF c='> THEN {directoryEnd _ filenameStart _ loc+1}; ENDCASE => ERROR; loc _ loc+1; }; IF name.Map[action: Action] OR state # 3 THEN RETURN; host _ name.Substr[start: hostStart, len: hostEnd-hostStart]; directory _ name.Substr[start: directoryStart, len: directoryEnd-directoryStart]; filename _ name.Substr[start: filenameStart, len: filenameEnd-filenameStart]; }; PutInSlashes: PROC [dir: Rope.ROPE] RETURNS [Rope.ROPE] ~ { Translator: PROC [old: CHAR] RETURNS [new: CHAR] ~ { new _ IF old = '> THEN '/ ELSE old; }; RETURN [dir.Translate[start: 1, translator: Translator]]; }; TakeOutSlashes: PROC [dir: Rope.ROPE] RETURNS [Rope.ROPE] ~ { Translator: PROC [old: CHAR] RETURNS [new: CHAR] ~ { new _ IF old = '/ THEN '> ELSE old; }; RETURN [Rope.Concat["<", dir.Translate[translator: Translator]]]; }; GetUserName: PROC RETURNS [Rope.ROPE] ~ { name: Rope.ROPE _ UserCredentials.GetUserCredentials[].name; length: INT _ 0; WHILE length", directory] ELSE directory] }; RemoveVersionNumber: PROC [filename: Rope.ROPE] RETURNS [Rope.ROPE] ~ { FOR i: INT IN [0..filename.Length) DO IF filename.Fetch[i] = '! THEN RETURN [filename.Substr[len: i]]; ENDLOOP; RETURN [filename]; }; CompareFileNames: PROC [a, b: Rope.ROPE] RETURNS [Environment.Comparison] = { versionStart: INT _ -1; FOR i: INT IN [0..MIN[a.Length, b.Length]) DO aChar: CHAR ~ a.Fetch[i]; bChar: CHAR ~ b.Fetch[i]; IF Rope.Upper[aChar] < Rope.Upper[bChar] THEN RETURN[less]; IF Rope.Upper[aChar] > Rope.Upper[bChar] THEN RETURN[greater]; IF aChar = '! THEN {versionStart _ i+1; EXIT}; ENDLOOP; IF versionStart < 0 THEN RETURN[SELECT a.Length FROM < b.Length => less, = b.Length => equal, > b.Length => greater, ENDCASE => ERROR] ELSE { aVersion: INT _ ValueOf[a, versionStart]; bVersion: INT _ ValueOf[b, versionStart]; RETURN[SELECT aVersion FROM < bVersion => less, = bVersion => equal, > bVersion => greater, ENDCASE => ERROR] }; }; EqualNames: PROC [a, b: Rope.ROPE] RETURNS [BOOLEAN] = { FOR i: INT IN [0..MIN[a.Length, b.Length]) DO aChar: CHAR ~ a.Fetch[i]; bChar: CHAR ~ b.Fetch[i]; IF Rope.Upper[aChar] # Rope.Upper[bChar] THEN RETURN[FALSE]; IF aChar = '! THEN RETURN[TRUE]; ENDLOOP; RETURN[a.Length = b.Length]; }; IsNat: PROC [rope: Rope.ROPE] RETURNS [BOOLEAN] = { IsNonDigit: PROC [c: CHAR] RETURNS [BOOL] ~ { RETURN[NOT (c IN ['0..'9])] }; RETURN [rope.Length IN [1..4] AND NOT rope.Map[action: IsNonDigit]]; }; ValueOf: PROC [rope: Rope.ROPE, start: INT _ 0] RETURNS [value: NAT _ 0] = { Action: PROC [c: CHAR] RETURNS [BOOL_FALSE] ~ { d: [0..10) _ c - '0; value _ value * 10 + d; }; [] _ rope.Map[action: Action, start: start]; }; HasVersion: PROC [filename: Rope.ROPE] RETURNS [BOOLEAN] ~ { Action: PROC [c: CHAR] RETURNS [BOOL] ~ { RETURN[c = '!] }; RETURN [filename.Map[action: Action]] }; HasWildCard: PROC [filename: Rope.ROPE] RETURNS [BOOLEAN] ~ { Action: PROC [c: CHAR] RETURNS [BOOL] ~ { RETURN[c = '*] }; RETURN [filename.Map[action: Action]] }; WildcardExpansion: TYPE ~ {local, remote, remoteAll}; DoOpForEachFile: PROC [DoOperation: DoOperationProc, cmd: Commander.Handle, defaultPublicServer: Rope.ROPE, defaultPersonalServer: Rope.ROPE, wildcardExpansion: WildcardExpansion] ~ { offset: INT _ 0; log: IO.STREAM _ cmd.out; commands: IO.STREAM _ IO.CreateInputStreamFromRope[cmd.commandLine]; Separators: IO.BreakProc ~ {RETURN[SELECT char FROM IO.SP, IO.CR, IO.TAB, ',, '; => sepr, ENDCASE => other ]}; UNTIL commands.EndOf OR UserAbort[cmd] DO name: Rope.ROPE ~ commands.GetToken[Separators]; host, directory, filename: Rope.ROPE; IF name.Length = 0 THEN EXIT; [host, directory, filename] _ ParsePath[name]; IF filename = NIL THEN log.PutF["Invalid name: %g\n", IO.rope[name]] ELSE { IF host.Length = 0 THEN host _ IF directory.Length = 0 OR directory.Fetch[0] # '< THEN defaultPersonalServer ELSE defaultPublicServer; directory _ SupplyTopLevel[directory]; IF HasWildCard[directory] OR HasWildCard[filename] OR wildcardExpansion # local THEN { IF wildcardExpansion = local THEN { IF HasWildCard[directory] THEN log.PutF["%g: illegal wildcard in directory.\n", IO.rope[directory]] ELSE { AbortProc: PROC RETURNS [BOOLEAN] ~ {RETURN [UserAbort[cmd]]}; matchingFiles: LIST OF Rope.ROPE _ DirectoryList.Local[filename, AbortProc]; FOR r: LIST OF Rope.ROPE _ matchingFiles, r.rest UNTIL r=NIL OR UserAbort[cmd] DO fname: Rope.ROPE _ r.first; IF DoOperation[host, directory, fname, log] THEN IO.SetUserAbort[cmd.in]; ENDLOOP; }; } ELSE { searchPattern: Rope.ROPE _ directory.Concat[filename]; AbortProc: PROC RETURNS [BOOLEAN] ~ {RETURN [UserAbort[cmd]]}; matchingFiles: LIST OF Rope.ROPE; errorMsg: Rope.ROPE; IF wildcardExpansion # remoteAll AND NOT HasVersion[filename] THEN searchPattern _ searchPattern.Concat["!h"]; [matchingFiles, errorMsg] _ DirectoryList.Remote[host, searchPattern, AbortProc]; IF errorMsg.Length > 0 THEN { log.PutF["[%g]%g: %g\n", IO.rope[host], IO.rope[searchPattern], IO.rope[errorMsg]]; } ELSE { FOR r: LIST OF Rope.ROPE _ matchingFiles, r.rest UNTIL r=NIL OR UserAbort[cmd] DO fullName: Rope.ROPE _ r.first; dir, fname: Rope.ROPE; [directory: dir, filename: fname] _ ParsePath[fullName]; IF DoOperation[host, dir, fname, log] THEN IO.SetUserAbort[cmd.in]; ENDLOOP } } } ELSE IF DoOperation[host, directory, filename, log] THEN IO.SetUserAbort[cmd.in]; }; ENDLOOP; IF commands.EndOf THEN log.PutRope["Done."] ELSE log.PutRope["Aborted."]; }; DelVerCommand: Commander.CommandProc ~ { offset: INT _ 0; log: IO.STREAM _ cmd.out; commands: IO.STREAM _ IO.CreateInputStreamFromRope[cmd.commandLine]; keepRope: Rope.ROPE; keep: NAT; Separators: IO.BreakProc ~ {RETURN[SELECT char FROM IO.SP, IO.CR, IO.TAB, ',, '; => sepr, ENDCASE => other ]}; keepRope _ commands.GetToken[Separators]; IF NOT IsNat[keepRope] OR (keep _ ValueOf[keepRope]) = 0 THEN { log.PutRope["First parameter is the number of versions to keep, and must be positive."]; RETURN; }; UNTIL commands.EndOf OR UserAbort[cmd] DO name: Rope.ROPE ~ commands.GetToken[Separators]; host, directory, filename: Rope.ROPE; IF name.Length = 0 THEN EXIT; [host, directory, filename] _ ParsePath[name]; IF host.Length = 0 OR directory.Length = 0 OR directory.Fetch[0] # '< THEN { log.PutF["%g: DelVerKeep requires explicit host and top-level directory\n", IO.rope[name]]; } ELSE { pattern: Rope.ROPE _ RemoveVersionNumber[directory.Concat[filename]].Concat["*!*"]; filesToDelete: LIST OF Rope.ROPE; errorMsg: Rope.ROPE; AbortProc: PROC RETURNS [BOOLEAN] ~ {RETURN [UserAbort[cmd]]}; log.PutRope["Enumerating remote directory . . . "]; [filesToDelete, errorMsg] _ DirectoryList.Remote[host, pattern, AbortProc]; IF errorMsg.Length # 0 THEN { log.PutF["%g: %g\n", IO.rope[name], IO.rope[errorMsg]]; } ELSE { log.PutRope["done.\n"]; filesToDelete _ SortRopeList[filesToDelete]; filesToDelete _ RemoveHighestVersions[filesToDelete, keep]; IF filesToDelete = NIL THEN log.PutF["%g: No old versions to delete.\n", IO.rope[name]]; UNTIL filesToDelete = NIL OR UserAbort[cmd] DO file: Rope.ROPE _ filesToDelete.first; filesToDelete _ filesToDelete.rest; log.PutF["Deleting [%g]%g ", IO.rope[host], IO.rope[file]]; Process.Pause[Process.MsecToTicks[pauseAmount]]; IF NOT UserAbort[cmd] THEN { msg: Rope.ROPE _ "done."; log.PutRope[". . . "]; TRUSTED {FtpMan.Delete[host, PutInSlashes[file] ! CIFS.Error => {msg _ error; CONTINUE}]}; log.PutRope[msg]; log.PutRope["\n"]; }; ENDLOOP; }; }; ENDLOOP; IF commands.EndOf THEN log.PutRope["Done."] ELSE log.PutRope["Aborted."]; }; SortRopeList: PROC [l: LIST OF Rope.ROPE] RETURNS [LIST OF Rope.ROPE] ~ { a, b, mergeTo: LIST OF Rope.ROPE; mergeToCons: LIST OF Rope.ROPE = CONS[NIL, NIL]; max: CARDINAL = 22; --number of bits in word-address space minus 2 lists: ARRAY [0..max) OF LIST OF Rope.ROPE _ ALL [NIL]; x: CARDINAL; --[0..max] xMax: CARDINAL _ 0; --[0..max) UNTIL (a _ l) = NIL OR (b _ a.rest) = NIL DO l _ b.rest; IF CompareFileNames[a.first, b.first] = less THEN { a.rest _ b; b.rest _ NIL } ELSE { b.rest _ a; a.rest _ NIL; a _ b }; x _ 0; DO IF (b _ lists[x]) = NIL THEN { lists[x] _ a; EXIT } ELSE { --merge (equal length) lists a and b lists[x] _ NIL; mergeTo _ mergeToCons; DO --assert a#NIL, b#NIL IF CompareFileNames[a.first, b.first] = less THEN { mergeTo.rest _ a; mergeTo _ a; IF (a _ a.rest) = NIL THEN { mergeTo.rest _ b; EXIT } } ELSE { mergeTo.rest _ b; mergeTo _ b; IF (b _ b.rest) = NIL THEN { mergeTo.rest _ a; EXIT } } ENDLOOP; a _ mergeToCons.rest; x _ x+1; IF x > xMax AND (xMax _ x) = max THEN ERROR } ENDLOOP; ENDLOOP; x _ 0; IF a = NIL THEN { UNTIL (lists[x] # NIL OR x = xMax) DO x _ x+1 ENDLOOP; a _ lists[x]; lists[x] _ NIL; x _ x+1 }; UNTIL x > xMax DO IF (b _ lists[x]) # NIL THEN { lists[x] _ NIL; mergeTo _ mergeToCons; DO IF CompareFileNames[a.first, b.first] = less THEN { mergeTo.rest _ a; mergeTo _ a; IF (a _ a.rest) = NIL THEN { mergeTo.rest _ b; EXIT } } ELSE { mergeTo.rest _ b; mergeTo _ b; IF (b _ b.rest) = NIL THEN { mergeTo.rest _ a; EXIT } } ENDLOOP; a _ mergeToCons.rest }; x _ x+1; ENDLOOP; RETURN [a] };--SortRopeList Reverse: PROC [list: LIST OF Rope.ROPE] RETURNS [LIST OF Rope.ROPE] ~ { new: LIST OF Rope.ROPE _ NIL; WHILE list # NIL DO current: LIST OF Rope.ROPE _ list; list _ list.rest; current.rest _ new; new _ current; ENDLOOP; RETURN [new] }; RemoveHighestVersions: PROC [files: LIST OF Rope.ROPE, numberToKeep: NAT] RETURNS [LIST OF Rope.ROPE] ~ { list: LIST OF Rope.ROPE _ Reverse[files]; delete: LIST OF Rope.ROPE _ NIL; keep: LIST OF Rope.ROPE _ NIL; n: NAT _ 0; UNTIL list = NIL DO current: LIST OF Rope.ROPE _ list; list _ list.rest; current.rest _ NIL; IF keep = NIL OR NOT EqualNames[keep.first, current.first] THEN { n _ 1; current.rest _ keep; keep _ current; } ELSE IF n < numberToKeep THEN { n _ n + 1; current.rest _ keep; keep _ current; } ELSE { current.rest _ delete; delete _ current; }; ENDLOOP; RETURN [delete]; }; UserAbort: PROC [cmd: Commander.Handle] RETURNS [BOOLEAN] ~ { RETURN [cmd.in.UserAbort] }; Commander.Register["StoreText", StoreTextCommand, "Store the text portion of Tioga files on a remote server."]; Commander.Register["Store", StoreCommand, "Store the files onto remote servers."]; Commander.Register["Retrieve", RetrieveCommand, "Retrieve the files from remote servers."]; Commander.Register["ListRemote", ListRemoteCommand, "List files on remote servers."]; Commander.Register["ListVerbose", ListVerboseCommand, "List info for files on remote servers."]; Commander.Register["DelVerKeep", DelVerCommand, "Delete old versions of files on remote servers."]; END. šFileTransferCommandsImpl.mesa Last edit by Michael Plass on May 2, 1983 1:34 pm The interprtation of state is as follows: 0 [ 1 ] 2 < 3 > 4 The sort is destructive and NOT stable, that is, the relative positions in the result of nodes with equal keys is unpredictible. For a nondestructive sort, copy l first. The sort does one CONS. lists[0] is a sorted list of length 2 or NIL, lists[1] is a sorted list of length 4 or NIL, lists[2] is a sorted list of length 8 or NIL, etc. When Sort returns, lists = ALL [NIL] again (we do some extra work to assure this). make each pair of consecutive elements of l into a sorted list of length 2, then merge it into lists. xMax now contains the largest x such that lists[x] # NIL. if l's length was even, a = NIL; if l's length was odd, a = single element list. merge a and elements of lists into result (held in a). try to make a # NIL. a # NIL OR x > xMax. a#NIL AND b#NIL Ê…˜J™J™1šÏk ˜ Jšœ˜Jšœ ˜ J˜ J˜J˜ J˜Jšœ˜J˜Jšœ˜J˜J˜J˜Jšœ˜—šœ ˜'Jš œœ œœ"œ˜oJšœ˜Jšœœ ˜+Jšœœ ˜*Jšœœ ˜)J˜)JšÏnœœœ"œœœœœœ˜tJšžœu˜…Jšž œm˜yJšžœq˜€Jšžœv˜‡Jšžœw˜‰šž œœœ˜.Jš œœœœœDœ˜rJšœ œœ*˜?šœœœ/œ˜IJšœ˜Jšœœ˜7Jšœœœ&˜HJšœ ˜$Jšœ˜ Jšœ˜—Jšœœœ˜-Jšœ œ˜ šœœ˜Jšœ œ˜(Jšœ)˜)Jšœ˜—Jšœ˜Jšœ ˜ Jšœ˜Jšœ'œ œœ˜]Jšœ˜Jšœœ˜#Jšœœ˜—šžœœ˜$Jš œœœœœDœ˜rJšœœ œœ˜7Jšœ œ˜ Jšœœ œœ˜SJšœGœ˜[Jšœ ˜ Jšœœ˜#Jšœœ˜—šž œœ˜'Jš œœœœœDœ˜rJšœœ œ%œ ˜MJšœ'œ œœ˜]Jšœ@œ˜RJšœ ˜ Jšœ˜Jšœœ˜—šž œ˜!Jšœœ œœ˜MJšœ˜—Jšœœ˜šž œ˜"J˜Jšœ œ˜J˜!Jšœœœ˜Jšœ œ8˜GJšœœ œœ˜KJšœ œœ˜BJšœ{˜{šœ˜Jšœ1œ ˜@Jšœ/˜/Jšœ-˜-Jšœ!œœ˜BJšœœ˜—Jšœ˜—š ž œœ œœ"œœ˜ZJšœ2œ˜:Jšœœ˜.šœ)™)J™—Jšœœ˜Jšœœ˜ š žœœœœœœ˜/šœ˜šœœœ˜0Jšœœ>˜OJšœB˜F—Jšœœœœœœœœœ˜[šœœœ>˜OJšœB˜F—Jšœœœ(˜9Jšœœ˜—Jšœ ˜ Jšœ˜—Jšœœ œœ˜5Jšœ=˜=JšœQ˜QJšœM˜MJšœ˜—š ž œœ œœœ˜;š ž œœœœœ˜4Jšœœ œœ˜#Jšœ˜—Jšœ3˜9Jšœ˜—š žœœ œœœ˜=š ž œœœœœ˜4Jšœœ œœ˜#Jšœ˜—Jšœ;˜AJšœ˜—šž œœœœ˜)Jšœ œ-˜Jšœ œœ˜.Jšœ˜—Jšœœœœ œAœœ˜†šœ˜Jšœ œ˜)Jšœ œ˜)Jš œœ œAœœ˜mJšœ˜—Jšœ˜—š ž œœ œœœ˜8š œœœœ˜-Jšœœ˜Jšœœ˜Jšœ'œœœ˜Jšœœœœ,˜Lšœœœœœœœ˜QJšœ œ ˜Jšœ*œœ˜IJšœ˜—Jšœ˜—Jšœ˜—šœ˜Jšœœ˜6Jš œ œœœœ˜>Jšœœœœ˜!Jšœœ˜šœœœ˜BJšœ+˜+—JšœQ˜Qšœœ˜Jšœœ œœ˜SJšœ˜—šœ˜šœœœœœœœ˜QJšœœ ˜Jšœœ˜Jšœ8˜8Jšœ$œœ˜CJš˜—Jšœ˜—Jšœ˜—Jšœ˜—Jšœœ-œœ˜QJšœ˜—Jšœ˜—Jšœœœ˜IJšœ˜—šž œ˜(Jšœœ˜Jšœœœ ˜Jšœ œœœ,˜DJšœœ˜Jšœœ˜ š ž œœœœ˜3Jš œœœœœœ˜%Jšœ ˜J˜—Jšœ)˜)šœœœ œ˜?JšœX˜XJšœ˜Jšœ˜—šœœ˜)Jšœ œ!˜0Jšœ œ˜%Jšœœœ˜Jšœ.˜.šœœœœ˜LJšœLœ ˜[Jšœ˜—šœ˜JšœœA˜SJšœœœœ˜!Jšœœ˜Jš œ œœœœ˜>Jšœ3˜3JšœK˜Kšœœ˜Jšœœ œ˜7Jšœ˜—šœ˜Jšœ˜Jšœ,˜,Jšœ;˜;Jšœœœ.œ ˜Xšœœœ˜.Jšœ œ˜&Jšœ#˜#Jšœœ œ ˜;J˜0šœœœ˜Jšœ œ ˜Jšœ˜Jšœ+œœ˜ZJšœ˜Jšœ˜Jšœ˜—Jšœ˜—Jšœ˜—J˜—Jšœ˜—Jšœœœ˜IJšœ˜—šž œœœœœœœœœ˜IJšœX™XJšœQ™QJšœ™Jšœœœ ˜!Jš œ œœ œœœ˜0JšœœÏc.˜Bš œœ œœœ œœ˜7JšœQ™QJšœ<™