-- Transport Mechanism - mailbox and R-Server site cache --

-- [Indigo]<Grapevine>MS>SiteCache.mesa

-- Andrew Birrell   4-Mar-82 15:24:41 --
-- Mike Schroeder   26-Jan-83 10:05:32 --

DIRECTORY
BodyDefs	USING[ maxRNameLength, oldestTime, RName, Timestamp],
BTreeDefs	USING[ BTreeHandle, Call, CreateAndInitializeBTree,
		       Delete, EnumerateFrom, Insert, KeyNotFound, Lookup,
		       TestKeys ],
HeapDefs	USING[ HeapEndRead ],
NameInfoDefs	USING[ CheckStamp, Close, Enumerate, Expand, ExpandInfo ],
ServerDefs	USING[ ServerHandle, ServerIsLocal, GetServer, ServerUp ],
SiteCacheDefs	USING[ ForeignDL, RecipientInfo, RemailInfo],
VMDefs		USING[ OpenFile ];

SiteCache: MONITOR
   IMPORTS BTreeDefs, HeapDefs, NameInfoDefs, ServerDefs, SiteCacheDefs,
           VMDefs
   EXPORTS SiteCacheDefs =
BEGIN


tree: BTreeDefs.BTreeHandle ← LOOPHOLE[NIL];


-- Management of Btree entries --

LowerCase: PROCEDURE[c: CHARACTER] RETURNS[CHARACTER] = INLINE
   { RETURN[IF c IN ['A..'Z] THEN c - 'A + 'a ELSE c] };

IsFirstGE: BTreeDefs.TestKeys =
   BEGIN
   -- parameters a,b: DESC FOR ARRAY OF WORD returns[ BOOLEAN]--
   aC: POINTER TO PACKED ARRAY OF CHARACTER = LOOPHOLE[BASE[a]];
   bC: POINTER TO PACKED ARRAY OF CHARACTER = LOOPHOLE[BASE[b]];
   FOR i:CARDINAL IN [0..2*MIN[LENGTH[a],LENGTH[b]])
   DO IF LowerCase[aC[i]] < LowerCase[bC[i]] THEN RETURN[FALSE];
      IF LowerCase[aC[i]] > LowerCase[bC[i]] THEN RETURN[TRUE];
   ENDLOOP;
   RETURN[LENGTH[a] >= LENGTH[b]];
   END;

AreTheyEq: BTreeDefs.TestKeys =
   BEGIN
   aC: POINTER TO PACKED ARRAY OF CHARACTER =LOOPHOLE[BASE[a]];
   bC: POINTER TO PACKED ARRAY OF CHARACTER = LOOPHOLE[BASE[b]];
   IF LENGTH[a] = LENGTH[b]
   THEN FOR i:CARDINAL IN [0..2*LENGTH[a])
        DO IF LowerCase[aC[i]] # LowerCase[bC[i]] THEN EXIT;
        REPEAT FINISHED => RETURN[TRUE];
        ENDLOOP;
   RETURN[FALSE];
   END;

RNameDesc: PROCEDURE[ name: BodyDefs.RName ]
             RETURNS[ DESCRIPTOR FOR ARRAY OF WORD ] =
   BEGIN
   IF name.length MOD 2 # 0 THEN name[name.length] ← '@;
   RETURN[ DESCRIPTOR[ @(name.text), (1+name.length)/2 ] ]
   END;

TreeData: TYPE =
   RECORD[ visibility: {visible, gone} --internal to BTree--,
           primary: BOOLEAN,
           var: SELECT type:* FROM
                  found =>     [ server: ServerDefs.ServerHandle ],
                  allDown =>   [ stamp: BodyDefs.Timestamp ],
                  valid =>     NULL --name ok, but we don't know details--,
                  notInTree => NULL --doesn't appear in BTree--,
                ENDCASE ];

FindInTree: ENTRY PROCEDURE[ who: BodyDefs.RName ]
                    RETURNS[ data: TreeData ] =
   BEGIN
   desc: DESCRIPTOR FOR ARRAY OF WORD = RNameDesc[who];
   length:CARDINAL = BTreeDefs.Lookup[tree, desc,
                                      DESCRIPTOR[@data,SIZE[TreeData]] ];
   IF length # BTreeDefs.KeyNotFound
   THEN IF data.visibility = visible
        THEN RETURN
        ELSE BEGIN
             BTreeDefs.Delete[tree, desc ];
             RETURN[ [visible, TRUE, notInTree[]] ]
             END
   ELSE RETURN[ [visible, TRUE, notInTree[]] ]
   END;

ChangeInTree: ENTRY PROCEDURE[ who: BodyDefs.RName, data: TreeData ] =
   BEGIN
   prev: TreeData;
   desc: DESCRIPTOR FOR ARRAY OF WORD = RNameDesc[who];
   IF BTreeDefs.Lookup[tree, desc, DESCRIPTOR[@prev,SIZE[TreeData]] ]
          # BTreeDefs.KeyNotFound
   THEN BTreeDefs.Delete[tree, desc ];
   IF data.type # notInTree
   THEN BEGIN
        data.visibility ← visible;
        BTreeDefs.Insert[tree, desc,
            DESCRIPTOR[@data, SELECT data.type FROM
                         found => SIZE[found TreeData],
                         allDown => SIZE[allDown TreeData],
                         valid => SIZE[valid TreeData],
                       ENDCASE => ERROR] ];
        END;
   END;

SingleFlush: PUBLIC PROCEDURE[ who: BodyDefs.RName ] =
   BEGIN
   foreign: BodyDefs.RName = [BodyDefs.maxRNameLength];
   ChangeInTree[who, [,,notInTree[]]];
   [] ← MakeRNameBeForeign[who, foreign];
   ChangeInTree[foreign, [,,notInTree[]]];
   END;

SelectiveFlush: PUBLIC ENTRY PROCEDURE[ given: ServerDefs.ServerHandle ] =
   BEGIN
   -- Flushes from the site cache those entries which are non-primary and
   -- indicate a server other than the given one.  That is, the only entries
   -- not flushed are those which are primary and those which are for the
   -- given server.  This is called by UpServer.
   Action: BTreeDefs.Call =
      BEGIN
      record: POINTER TO TreeData = LOOPHOLE[ BASE[v] ];
      more ← TRUE; dirty ← FALSE;
      IF LENGTH[k] = 0 THEN RETURN;
      WITH record SELECT FROM
        found => IF server # given
                    AND NOT primary
                    AND record.visibility = visible
                 THEN { record.visibility ← gone; dirty ← TRUE };
        allDown, valid => IF record.visibility = visible
                   THEN { record.visibility ← gone; dirty ← TRUE };
      ENDCASE => ERROR;
      END;
   BTreeDefs.EnumerateFrom[tree, DESCRIPTOR[NIL,0], Action];
   END;



-- R-Name lookup  --

SubMBXSite: PROCEDURE[ who, really: BodyDefs.RName ]
              RETURNS[ SiteCacheDefs.RecipientInfo ] =
   BEGIN
   -- Finds MBX site or distribution list contents, using cache if possible --
   -- BEWARE: If "who" describes a distribution list, this procedure starts
   -- a reader for it.  In particular, to discriminate on the variant record
   -- returned, do not write "WITH FindMBXSite[....] SELECT", since each
   -- access to the opened fields of the record causes "FindMBXSite[....]" to
   -- be re-evaluated, causing another reader to be started!
   found:     BOOLEAN ← FALSE;
   isLocal:   BOOLEAN ← FALSE;
   primary:   BOOLEAN ← TRUE;
   server:    ServerDefs.ServerHandle;
   treeData:  TreeData = FindInTree[who];
   stamp:     BodyDefs.Timestamp ← BodyDefs.oldestTime;
   WITH t: treeData SELECT FROM
     found =>
       BEGIN
       isLocal ← ServerDefs.ServerIsLocal[server ← t.server];
       found ← isLocal OR ServerDefs.ServerUp[server];
       END;
     allDown =>
       stamp ← t.stamp;
     notInTree, valid =>
       NULL;
   ENDCASE => ERROR; 
   IF NOT found
   THEN -- ask registration server --
      BEGIN
      info: NameInfoDefs.ExpandInfo =
         -- should just call Expand, but at present that always causes a
         -- reader to be opened in the R-Server, so use CheckStamp first.
         IF treeData.type = allDown
         AND  NameInfoDefs.CheckStamp[who,stamp] = noChange
         THEN [noChange[]]
         ELSE NameInfoDefs.Expand[who, stamp];
      WITH i: info SELECT FROM
        notFound =>
           BEGIN
           IF treeData.type # notInTree
           THEN ChangeInTree[who, [,,notInTree[]] ];
           RETURN[ [notFound[]] ];
           END;
        allDown, noChange =>
           NULL;
        group =>
           BEGIN
           IF treeData.type # valid
           THEN ChangeInTree[who, [,,valid[]] ];
           RETURN[ [dl[i.members]] ];
           END;
        individual =>
           BEGIN
           --scan list of mailbox sites--
           hasNoSites: BOOLEAN ← TRUE;
           Work: PROC[site: BodyDefs.RName] RETURNS[done: BOOLEAN] =
              BEGIN
              hasNoSites ← FALSE;
              server ← ServerDefs.GetServer[[rName[site]],mail];
              isLocal ← ServerDefs.ServerIsLocal[server];
              --If network is broken, ServerIsLocal may call DownServer--
              --So we get more predictable results by calling it early--
              IF ServerDefs.ServerUp[server]
              THEN { found ← done ← TRUE; }
              ELSE { primary ← FALSE; done ← FALSE };
              END;
           stamp ← i.stamp;
           NameInfoDefs.Enumerate[i.sites, Work];
           NameInfoDefs.Close[i.sites];
           IF hasNoSites THEN RETURN[ [notFound[]] ];
           IF found THEN ChangeInTree[who, [,primary,found[server]] ];
           END;
      ENDCASE => ERROR;
      END;
   IF found
   THEN BEGIN
        -- the server may, of course, go down after we find it --
        IF who # really
        THEN ChangeInTree[really, [,primary,found[server]] ];
        RETURN[ IF isLocal THEN [local[]] ELSE [found[server]] ]
        END
   ELSE BEGIN
        IF treeData.type # allDown
        THEN ChangeInTree[who, [,,allDown[stamp]] ];
        RETURN[ [allDown[]] ]
        END
   END;

NeedToRemail: PUBLIC PROCEDURE[ who: BodyDefs.RName ]
                      RETURNS[ SiteCacheDefs.RemailInfo ] =
   BEGIN
   info: NameInfoDefs.ExpandInfo ← NameInfoDefs.Expand[who];
   WITH i: info SELECT FROM
        notFound =>
           BEGIN
           -- Note that we always remail if foreign registry exists --
           foreign: BodyDefs.RName = [BodyDefs.maxRNameLength];
           [] ← MakeRNameBeForeign[who, foreign];
           info ← NameInfoDefs.Expand[foreign];
           WITH i: info SELECT FROM
              notFound => RETURN [invalid];
              allDown, noChange => NULL;
              group => NameInfoDefs.Close[i.members];
              individual => NameInfoDefs.Close[i.sites];
           ENDCASE => ERROR;
           RETURN[remail];
           END;
        allDown, noChange =>
           RETURN[remail];
        group =>
           {NameInfoDefs.Close[i.members]; RETURN[remail]};
        individual =>
           BEGIN
           --scan list of mailbox sites--
           rI: SiteCacheDefs.RemailInfo ← invalid;
           Work: PROC[site: BodyDefs.RName] RETURNS[done: BOOLEAN] =
              BEGIN
              server: ServerDefs.ServerHandle =
                ServerDefs.GetServer[[rName[site]],mail];
              IF ServerDefs.ServerIsLocal[server]
              THEN {rI ← stillLocal; done ← TRUE}
              ELSE {rI ← remail; done ← FALSE};
              END;
           NameInfoDefs.Enumerate[i.sites, Work];
           NameInfoDefs.Close[i.sites];
           RETURN[rI];
           END;
   ENDCASE => ERROR;
   END;

FindMBXSite: PUBLIC PROCEDURE[ who: BodyDefs.RName ]
                      RETURNS[ info: SiteCacheDefs.RecipientInfo ] =
   BEGIN
   -- this layer is for the MTP recipient kludge --
   info ← SubMBXSite[who, who];
   IF info.type = notFound
   THEN BEGIN
        foreign: BodyDefs.RName = [BodyDefs.maxRNameLength];
        upArrow: BOOLEAN = MakeRNameBeForeign[who, foreign];
        info ← SubMBXSite[foreign, IF upArrow THEN foreign ELSE who];
        IF upArrow AND info.type = found
        THEN info ← SiteCacheDefs.ForeignDL[who, info];
        END;
   END;

MakeRNameBeForeign: PROC[sname, destination: BodyDefs.RName]
                 RETURNS[ upArrow: BOOLEAN ] =
   { RETURN[ MakeRNameHaveNA["Foreign"L, sname, destination] ] };

MakeRNameHaveNA: PROC[nname, sname, destination: BodyDefs.RName]
                 RETURNS[ upArrow: BOOLEAN ] =
   BEGIN
   -- nname is of the form "NA" or "NA.something"
   -- sname is of form "something.SN" or "SN"
   -- assumes that SN and NA do not contain '.
   -- constructs "SN.NA", truncating NA if needed
   SNstart: CARDINAL;
   sep: CHARACTER = '.;
   destination.length ← 0;
   SNstart ← sname.length;
   UNTIL SNstart = 0 OR sname[SNstart-1] = sep
   DO SNstart ← SNstart - 1; ENDLOOP;
   upArrow ← (SNstart > 1 AND sname[SNstart-2] = '↑);
   FOR index: CARDINAL IN [SNstart..sname.length)
   DO IF destination.length = destination.maxlength THEN ERROR;
      destination[destination.length] ← sname[index];
      destination.length ← destination.length + 1;
   ENDLOOP;
   IF destination.length = destination.maxlength THEN RETURN;
   destination[destination.length] ← sep;
   destination.length ← destination.length + 1;
   FOR index: CARDINAL IN [0..nname.length)
   WHILE nname[index] # sep
   DO IF destination.length = destination.maxlength THEN EXIT;
      destination[destination.length] ← nname[index];
      destination.length ← destination.length + 1;
   ENDLOOP;
   END;

ValidateRName: PUBLIC PROCEDURE[ who: BodyDefs.RName ]
                        RETURNS[ ok: BOOLEAN ] =
   BEGIN
   treeData: TreeData = FindInTree[who];
   IF treeData.type # notInTree
   THEN ok ← TRUE
   ELSE SELECT NameInfoDefs.CheckStamp[who] FROM
          notFound =>
             BEGIN
             foreign: BodyDefs.RName = [BodyDefs.maxRNameLength];
             upArrow: BOOLEAN = MakeRNameBeForeign[who, foreign];
             fInfo: SiteCacheDefs.RecipientInfo =
                SubMBXSite[foreign, IF upArrow THEN foreign ELSE who];
             ok ← fInfo.type # notFound;
             IF ok AND upArrow THEN ChangeInTree[who, [,,valid[]] ];
             END;
          group =>
             BEGIN
             ok ← TRUE; ChangeInTree[who, [,,valid[]] ];
             END;
        ENDCASE => -- includes "individual" and silly replies --
          BEGIN
          info: SiteCacheDefs.RecipientInfo = SubMBXSite[who, who];
          WITH i:info SELECT FROM
            allDown => ok ← TRUE;
            notFound => ok ← FALSE;
            dl => { ok ← TRUE; NameInfoDefs.Close[i.members] };
            foreign => { ok ← TRUE; HeapDefs.HeapEndRead[i.members] };
            local, found => ok ← TRUE;
          ENDCASE => ERROR;
          END; 
   END;


CheckPrimary: PUBLIC PROC[who: BodyDefs.RName,
                          server: ServerDefs.ServerHandle]
              RETURNS[ BOOLEAN ] =
   BEGIN
   -- in case of doubt, it's safe to return FALSE --
   SingleFlush[who];
   IF ValidateRName[who]
   THEN BEGIN
        treeData: TreeData = FindInTree[who];
        WITH t:treeData SELECT FROM
          found => RETURN[t.primary AND t.server = server];
        ENDCASE--may possibly include notInTree!-- => RETURN[FALSE]
        END
   ELSE RETURN[FALSE]
   END;


Init: ENTRY PROCEDURE =
   BEGIN
   tree ← BTreeDefs.CreateAndInitializeBTree [
        fileH: LOOPHOLE[VMDefs.OpenFile[options:oldOrNew,
                           name:"SiteCache.BTree", cacheFraction:10] ],
        initializeFile: TRUE, useDefaultOrderingRoutines: FALSE,
        isFirstGreaterOrEqual: IsFirstGE,
        areTheyEqual: AreTheyEq ];
   END;


Init[];


END.