~mrooney/etherpad/ubuntu

« back to all changes in this revision

Viewing changes to trunk/trunk/etherpad/src/etherpad/pad/model.js

  • Committer: Aaron Iba
  • Date: 2009-12-18 07:40:23 UTC
  • Revision ID: hg-v1:a9f8774a2e00cc15b35857471fecea17f649e3c9
initial code push

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/**
 
2
 * Copyright 2009 Google Inc.
 
3
 * 
 
4
 * Licensed under the Apache License, Version 2.0 (the "License");
 
5
 * you may not use this file except in compliance with the License.
 
6
 * You may obtain a copy of the License at
 
7
 * 
 
8
 *      http://www.apache.org/licenses/LICENSE-2.0
 
9
 * 
 
10
 * Unless required by applicable law or agreed to in writing, software
 
11
 * distributed under the License is distributed on an "AS-IS" BASIS,
 
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
13
 * See the License for the specific language governing permissions and
 
14
 * limitations under the License.
 
15
 */
 
16
 
 
17
import("fastJSON");
 
18
import("sqlbase.sqlbase");
 
19
import("sqlbase.sqlcommon");
 
20
import("sqlbase.sqlobj");
 
21
import("timer");
 
22
import("sync");
 
23
 
 
24
import("etherpad.collab.ace.easysync2.{Changeset,AttribPool}");
 
25
import("etherpad.log");
 
26
import("etherpad.pad.padevents");
 
27
import("etherpad.pad.padutils");
 
28
import("etherpad.pad.dbwriter");
 
29
import("etherpad.pad.pad_migrations");
 
30
import("etherpad.pad.pad_security");
 
31
import("etherpad.collab.collab_server");
 
32
import("cache_utils.syncedWithCache");
 
33
jimport("net.appjet.common.util.LimitedSizeMapping");
 
34
 
 
35
jimport("java.lang.System.out.println");
 
36
 
 
37
jimport("java.util.concurrent.ConcurrentHashMap");
 
38
jimport("net.appjet.oui.GlobalSynchronizer");
 
39
jimport("net.appjet.oui.exceptionlog");
 
40
 
 
41
function onStartup() {
 
42
  appjet.cache.pads = {};
 
43
  appjet.cache.pads.meta = new ConcurrentHashMap();
 
44
  appjet.cache.pads.temp = new ConcurrentHashMap();
 
45
  appjet.cache.pads.revs = new ConcurrentHashMap();
 
46
  appjet.cache.pads.revs10 = new ConcurrentHashMap();
 
47
  appjet.cache.pads.revs100 = new ConcurrentHashMap();
 
48
  appjet.cache.pads.revs1000 = new ConcurrentHashMap();
 
49
  appjet.cache.pads.chat = new ConcurrentHashMap();
 
50
  appjet.cache.pads.revmeta = new ConcurrentHashMap();
 
51
  appjet.cache.pads.authors = new ConcurrentHashMap();
 
52
  appjet.cache.pads.apool = new ConcurrentHashMap();
 
53
}
 
54
 
 
55
var _JSON_CACHE_SIZE = 10000;
 
56
 
 
57
// to clear: appjet.cache.padmodel.modelcache.map.clear()
 
58
function _getModelCache() {
 
59
  return syncedWithCache('padmodel.modelcache', function(cache) {
 
60
    if (! cache.map) {
 
61
      cache.map = new LimitedSizeMapping(_JSON_CACHE_SIZE);
 
62
    }
 
63
    return cache.map;
 
64
  });
 
65
}
 
66
 
 
67
function cleanText(txt) {
 
68
  return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, '        ').replace(/\xa0/g, ' ');
 
69
}
 
70
 
 
71
/**
 
72
 * Access a pad object, which is passed as an argument to
 
73
 * the given padFunc, which is executed inside an exclusive lock,
 
74
 * and return the result.  If the pad doesn't exist, a wrapper
 
75
 * object is still created and passed to padFunc, and it can
 
76
 * be used to check whether the pad exists and create it.
 
77
 *
 
78
 * Note: padId is a GLOBAL id.
 
79
 */
 
80
function accessPadGlobal(padId, padFunc, rwMode) {
 
81
  // this may make a nested call to accessPadGlobal, so do it first
 
82
  pad_security.checkAccessControl(padId, rwMode);
 
83
 
 
84
  // pad is never loaded into memory (made "active") unless it has been migrated.
 
85
  // Migrations do not use accessPad, but instead access the database directly.
 
86
  pad_migrations.ensureMigrated(padId);
 
87
 
 
88
  var mode = (rwMode || "rw").toLowerCase();
 
89
 
 
90
  if (! appjet.requestCache.padsAccessing) {
 
91
    appjet.requestCache.padsAccessing = {};
 
92
  }
 
93
  if (appjet.requestCache.padsAccessing[padId]) {
 
94
    // nested access to same pad
 
95
    var p = appjet.requestCache.padsAccessing[padId];
 
96
    var m = p._meta;
 
97
    if (m && mode != "r") {
 
98
      m.status.lastAccess = +new Date();
 
99
      m.status.dirty = true;
 
100
    }
 
101
    return padFunc(p);
 
102
  }
 
103
 
 
104
  return doWithPadLock(padId, function() {
 
105
    return sqlcommon.inTransaction(function() {
 
106
      var meta = _getPadMetaData(padId); // null if pad doesn't exist yet
 
107
 
 
108
      if (meta && ! meta.status) {
 
109
        meta.status = { validated: false };
 
110
      }
 
111
 
 
112
      if (meta && mode != "r") {
 
113
        meta.status.lastAccess = +new Date();
 
114
      }
 
115
 
 
116
      function getCurrentAText() {
 
117
        var tempObj = pad.tempObj();
 
118
        if (! tempObj.atext) {
 
119
          tempObj.atext = pad.getInternalRevisionAText(meta.head);
 
120
        }
 
121
        return tempObj.atext;
 
122
      }
 
123
      function addRevision(theChangeset, author, optDatestamp) {
 
124
        var atext = getCurrentAText();
 
125
        var newAText = Changeset.applyToAText(theChangeset, atext, pad.pool());
 
126
        Changeset.copyAText(newAText, atext); // updates pad.tempObj().atext!
 
127
 
 
128
        var newRev = ++meta.head;
 
129
 
 
130
        var revs = _getPadStringArray(padId, "revs");
 
131
        revs.setEntry(newRev, theChangeset);
 
132
 
 
133
        var revmeta = _getPadStringArray(padId, "revmeta");
 
134
        var thisRevMeta = {t: (optDatestamp || (+new Date())),
 
135
          a: getNumForAuthor(author)};
 
136
        if ((newRev % meta.keyRevInterval) == 0) {
 
137
          thisRevMeta.atext = atext;
 
138
        }
 
139
        revmeta.setJSONEntry(newRev, thisRevMeta);
 
140
 
 
141
        updateCoarseChangesets(true);
 
142
      }
 
143
      function getNumForAuthor(author, dontAddIfAbsent) {
 
144
        return pad.pool().putAttrib(['author',author||''], dontAddIfAbsent);
 
145
      }
 
146
      function getAuthorForNum(n) {
 
147
        // must return null if n is an attrib number that isn't an author
 
148
        var pair = pad.pool().getAttrib(n);
 
149
        if (pair && pair[0] == 'author') {
 
150
          return pair[1];
 
151
        }
 
152
        return null;
 
153
      }
 
154
 
 
155
      function updateCoarseChangesets(onlyIfPresent) {
 
156
        // this is fast to run if the coarse changesets
 
157
        // are up-to-date or almost up-to-date;
 
158
        // if there's no coarse changeset data,
 
159
        // it may take a while.
 
160
 
 
161
        if (! meta.coarseHeads) {
 
162
          if (onlyIfPresent) {
 
163
            return;
 
164
          }
 
165
          else {
 
166
            meta.coarseHeads = {10:-1, 100:-1, 1000:-1};
 
167
          }
 
168
        }
 
169
        var head = meta.head;
 
170
        // once we reach head==9, coarseHeads[10] moves
 
171
        // from -1 up to 0; at head==19 it moves up to 1
 
172
        var desiredCoarseHeads = {
 
173
          10: Math.floor((head-9)/10),
 
174
          100: Math.floor((head-99)/100),
 
175
          1000: Math.floor((head-999)/1000)
 
176
        };
 
177
        var revs = _getPadStringArray(padId, "revs");
 
178
        var revs10 = _getPadStringArray(padId, "revs10");
 
179
        var revs100 = _getPadStringArray(padId, "revs100");
 
180
        var revs1000 = _getPadStringArray(padId, "revs1000");
 
181
        var fineArrays = [revs, revs10, revs100];
 
182
        var coarseArrays = [revs10, revs100, revs1000];
 
183
        var levels = [10, 100, 1000];
 
184
        var dirty = false;
 
185
        for(var z=0;z<3;z++) {
 
186
          var level = levels[z];
 
187
          var coarseArray = coarseArrays[z];
 
188
          var fineArray = fineArrays[z];
 
189
          while (meta.coarseHeads[level] < desiredCoarseHeads[level]) {
 
190
            dirty = true;
 
191
            // for example, if the current coarse head is -1,
 
192
            // compose 0-9 inclusive of the finer level and call it 0
 
193
            var x = meta.coarseHeads[level] + 1;
 
194
            var cs = fineArray.getEntry(10 * x);
 
195
            for(var i=1;i<=9;i++) {
 
196
              cs = Changeset.compose(cs, fineArray.getEntry(10*x + i),
 
197
                                     pad.pool());
 
198
            }
 
199
            coarseArray.setEntry(x, cs);
 
200
            meta.coarseHeads[level] = x;
 
201
          }
 
202
        }
 
203
        if (dirty) {
 
204
          meta.status.dirty = true;
 
205
        }
 
206
      }
 
207
 
 
208
      /////////////////// "Public" API starts here (functions used by collab_server or other modules)
 
209
      var pad = {
 
210
        // Operations that write to the data structure should
 
211
        // set meta.dirty = true.  Any pad access that isn't
 
212
        // done in "read" mode also sets dirty = true.
 
213
        getId: function() { return padId; },
 
214
        exists: function() { return !!meta; },
 
215
        create: function(optText) {
 
216
          meta = {};
 
217
          meta.head = -1; // incremented below by addRevision
 
218
          pad.tempObj().atext = Changeset.makeAText("\n");
 
219
          meta.padId = padId,
 
220
          meta.keyRevInterval = 100;
 
221
          meta.numChatMessages = 0;
 
222
          var t = +new Date();
 
223
          meta.status = { validated: true };
 
224
          meta.status.lastAccess = t;
 
225
          meta.status.dirty = true;
 
226
          meta.supportsTimeSlider = true;
 
227
 
 
228
          var firstChangeset = Changeset.makeSplice("\n", 0, 0,
 
229
            cleanText(optText || ''));
 
230
          addRevision(firstChangeset, '');
 
231
 
 
232
          _insertPadMetaData(padId, meta);
 
233
 
 
234
          sqlobj.insert("PAD_SQLMETA", {
 
235
            id: padId, version: 2, creationTime: new Date(t), lastWriteTime: new Date(),
 
236
            headRev: meta.head }); // headRev is not authoritative, just for info
 
237
 
 
238
          padevents.onNewPad(pad);
 
239
        },
 
240
        destroy: function() { // you may want to collab_server.bootAllUsers first
 
241
          padevents.onDestroyPad(pad);
 
242
 
 
243
          _destroyPadStringArray(padId, "revs");
 
244
          _destroyPadStringArray(padId, "revs10");
 
245
          _destroyPadStringArray(padId, "revs100");
 
246
          _destroyPadStringArray(padId, "revs1000");
 
247
          _destroyPadStringArray(padId, "revmeta");
 
248
          _destroyPadStringArray(padId, "chat");
 
249
          _destroyPadStringArray(padId, "authors");
 
250
          _removePadMetaData(padId);
 
251
          _removePadAPool(padId);
 
252
          sqlobj.deleteRows("PAD_SQLMETA", { id: padId });
 
253
          meta = null;
 
254
        },
 
255
        writeToDB: function() {
 
256
          var meta2 = {};
 
257
          for(var k in meta) meta2[k] = meta[k];
 
258
          delete meta2.status;
 
259
          sqlbase.putJSON("PAD_META", padId, meta2);
 
260
 
 
261
          _getPadStringArray(padId, "revs").writeToDB();
 
262
          _getPadStringArray(padId, "revs10").writeToDB();
 
263
          _getPadStringArray(padId, "revs100").writeToDB();
 
264
          _getPadStringArray(padId, "revs1000").writeToDB();
 
265
          _getPadStringArray(padId, "revmeta").writeToDB();
 
266
          _getPadStringArray(padId, "chat").writeToDB();
 
267
          _getPadStringArray(padId, "authors").writeToDB();
 
268
          sqlbase.putJSON("PAD_APOOL", padId, pad.pool().toJsonable());
 
269
 
 
270
          var props = { headRev: meta.head, lastWriteTime: new Date() };
 
271
          _writePadSqlMeta(padId, props);
 
272
        },
 
273
        pool: function() {
 
274
          return _getPadAPool(padId);
 
275
        },
 
276
        getHeadRevisionNumber: function() { return meta.head; },
 
277
        getRevisionAuthor: function(r) {
 
278
          var n = _getPadStringArray(padId, "revmeta").getJSONEntry(r).a;
 
279
          return getAuthorForNum(Number(n));
 
280
        },
 
281
        getRevisionChangeset: function(r) {
 
282
          return _getPadStringArray(padId, "revs").getEntry(r);
 
283
        },
 
284
        tempObj: function() { return _getPadTemp(padId); },
 
285
        getKeyRevisionNumber: function(r) {
 
286
          return Math.floor(r / meta.keyRevInterval) * meta.keyRevInterval;
 
287
        },
 
288
        getInternalRevisionAText: function(r) {
 
289
          var cacheKey = "atext/C/"+r+"/"+padId;
 
290
          var modelCache = _getModelCache();
 
291
          var cachedValue = modelCache.get(cacheKey);
 
292
          if (cachedValue) {
 
293
            modelCache.touch(cacheKey);
 
294
            //java.lang.System.out.println("HIT! "+cacheKey);
 
295
            return Changeset.cloneAText(cachedValue);
 
296
          }
 
297
          //java.lang.System.out.println("MISS! "+cacheKey);
 
298
 
 
299
          var revs = _getPadStringArray(padId, "revs");
 
300
          var keyRev = pad.getKeyRevisionNumber(r);
 
301
          var revmeta = _getPadStringArray(padId, "revmeta");
 
302
          var atext = revmeta.getJSONEntry(keyRev).atext;
 
303
          var curRev = keyRev;
 
304
          var targetRev = r;
 
305
          var apool = pad.pool();
 
306
          while (curRev < targetRev) {
 
307
            curRev++;
 
308
            var cs = pad.getRevisionChangeset(curRev);
 
309
            atext = Changeset.applyToAText(cs, atext, apool);
 
310
          }
 
311
          modelCache.put(cacheKey, Changeset.cloneAText(atext));
 
312
          return atext;
 
313
        },
 
314
        getInternalRevisionText: function(r, optInfoObj) {
 
315
          var atext = pad.getInternalRevisionAText(r);
 
316
          var text = atext.text;
 
317
          if (optInfoObj) {
 
318
            if (text.slice(-1) != "\n") {
 
319
              optInfoObj.badLastChar = text.slice(-1);
 
320
            }
 
321
          }
 
322
          return text;
 
323
        },
 
324
        getRevisionText: function(r, optInfoObj) {
 
325
          var internalText = pad.getInternalRevisionText(r, optInfoObj);
 
326
          return internalText.slice(0, -1);
 
327
        },
 
328
        atext: function() { return Changeset.cloneAText(getCurrentAText()); },
 
329
        text: function() { return pad.atext().text; },
 
330
        getRevisionDate: function(r) {
 
331
          var revmeta = _getPadStringArray(padId, "revmeta");
 
332
          return new Date(revmeta.getJSONEntry(r).t);
 
333
        },
 
334
        // note: calls like appendRevision will NOT notify clients of the change!
 
335
        // you must go through collab_server.
 
336
        // Also, be sure to run cleanText() on any text to strip out carriage returns
 
337
        // and other stuff.
 
338
        appendRevision: function(theChangeset, author, optDatestamp) {
 
339
          addRevision(theChangeset, author || '', optDatestamp);
 
340
        },
 
341
        appendChatMessage: function(obj) {
 
342
          var index = meta.numChatMessages;
 
343
          meta.numChatMessages++;
 
344
          var chat = _getPadStringArray(padId, "chat");
 
345
          chat.setJSONEntry(index, obj);
 
346
        },
 
347
        getNumChatMessages: function() {
 
348
          return meta.numChatMessages;
 
349
        },
 
350
        getChatMessage: function(i) {
 
351
          var chat = _getPadStringArray(padId, "chat");
 
352
          return chat.getJSONEntry(i);
 
353
        },
 
354
        getPadOptionsObj: function() {
 
355
          var data = pad.getDataRoot();
 
356
          if (! data.padOptions) {
 
357
            data.padOptions = {};
 
358
          }
 
359
          if ((! data.padOptions.guestPolicy) ||
 
360
            (data.padOptions.guestPolicy == 'ask')) {
 
361
            data.padOptions.guestPolicy = 'deny';
 
362
          }
 
363
          return data.padOptions;
 
364
        },
 
365
        getGuestPolicy: function() {
 
366
          // allow/ask/deny
 
367
          return pad.getPadOptionsObj().guestPolicy;
 
368
        },
 
369
        setGuestPolicy: function(policy) {
 
370
          pad.getPadOptionsObj().guestPolicy = policy;
 
371
        },
 
372
        getDataRoot: function() {
 
373
          var dataRoot = meta.dataRoot;
 
374
          if (! dataRoot) {
 
375
            dataRoot = {};
 
376
            meta.dataRoot = dataRoot;
 
377
          }
 
378
          return dataRoot;
 
379
        },
 
380
        // returns an object, changes to which are not reflected
 
381
        // in the DB;  use setAuthorData for mutation
 
382
        getAuthorData: function(author) {
 
383
          var authors = _getPadStringArray(padId, "authors");
 
384
          var n = getNumForAuthor(author, true);
 
385
          if (n < 0) {
 
386
            return null;
 
387
          }
 
388
          else {
 
389
            return authors.getJSONEntry(n);
 
390
          }
 
391
        },
 
392
        setAuthorData: function(author, data) {
 
393
          var authors = _getPadStringArray(padId, "authors");
 
394
          var n = getNumForAuthor(author);
 
395
          authors.setJSONEntry(n, data);
 
396
        },
 
397
        adoptChangesetAttribs: function(cs, oldPool) {
 
398
          return Changeset.moveOpsToNewPool(cs, oldPool, pad.pool());
 
399
        },
 
400
        eachATextAuthor: function(atext, func) {
 
401
          var seenNums = {};
 
402
          Changeset.eachAttribNumber(atext.attribs, function(n) {
 
403
            if (! seenNums[n]) {
 
404
              seenNums[n] = true;
 
405
              var author = getAuthorForNum(n);
 
406
              if (author) {
 
407
                func(author, n);
 
408
              }
 
409
            }
 
410
          });
 
411
        },
 
412
        getCoarseChangeset: function(start, numChangesets) {
 
413
          updateCoarseChangesets();
 
414
 
 
415
          if (!(numChangesets == 10 || numChangesets == 100 ||
 
416
                numChangesets == 1000)) {
 
417
            return null;
 
418
          }
 
419
          var level = numChangesets;
 
420
          var x = Math.floor(start / level);
 
421
          if (!(x >= 0 && x*level == start)) {
 
422
            return null;
 
423
          }
 
424
 
 
425
          var cs = _getPadStringArray(padId, "revs"+level).getEntry(x);
 
426
 
 
427
          if (! cs) {
 
428
            return null;
 
429
          }
 
430
 
 
431
          return cs;
 
432
        },
 
433
        getSupportsTimeSlider: function() {
 
434
          if (! ('supportsTimeSlider' in meta)) {
 
435
            if (padutils.isProPadId(padId)) {
 
436
              return true;
 
437
            }
 
438
            else {
 
439
              return false;
 
440
            }
 
441
          }
 
442
          else {
 
443
            return !! meta.supportsTimeSlider;
 
444
          }
 
445
        },
 
446
        setSupportsTimeSlider: function(v) {
 
447
          meta.supportsTimeSlider = v;
 
448
        },
 
449
        get _meta() { return meta; }
 
450
      };
 
451
 
 
452
      try {
 
453
        padutils.setCurrentPad(padId);
 
454
        appjet.requestCache.padsAccessing[padId] = pad;
 
455
        return padFunc(pad);
 
456
      }
 
457
      finally {
 
458
        padutils.clearCurrentPad();
 
459
        delete appjet.requestCache.padsAccessing[padId];
 
460
        if (meta) {
 
461
          if (mode != "r") {
 
462
            meta.status.dirty = true;
 
463
          }
 
464
          if (meta.status.dirty) {
 
465
            dbwriter.notifyPadDirty(padId);
 
466
          }
 
467
        }
 
468
      }
 
469
    });
 
470
  });
 
471
}
 
472
 
 
473
/**
 
474
 * Call an arbitrary function with no arguments inside an exclusive
 
475
 * lock on a padId, and return the result.
 
476
 */
 
477
function doWithPadLock(padId, func) {
 
478
  var lockName = "document/"+padId;
 
479
  return sync.doWithStringLock(lockName, func);
 
480
}
 
481
 
 
482
function isPadLockHeld(padId) {
 
483
  var lockName = "document/"+padId;
 
484
  return GlobalSynchronizer.isHeld(lockName);
 
485
}
 
486
 
 
487
/**
 
488
 * Get pad meta-data object, which is stored in SQL as JSON
 
489
 * but cached in appjet.cache.  Returns null if pad doesn't
 
490
 * exist at all (does NOT create it).  Requires pad lock.
 
491
 */
 
492
function _getPadMetaData(padId) {
 
493
  var padMeta = appjet.cache.pads.meta.get(padId);
 
494
  if (! padMeta) {
 
495
    // not in cache
 
496
    padMeta = sqlbase.getJSON("PAD_META", padId);
 
497
    if (! padMeta) {
 
498
      // not in SQL
 
499
      padMeta = null;
 
500
    }
 
501
    else {
 
502
      appjet.cache.pads.meta.put(padId, padMeta);
 
503
    }
 
504
  }
 
505
  return padMeta;
 
506
}
 
507
 
 
508
/**
 
509
 * Sets a pad's meta-data object, such as when creating
 
510
 * a pad for the first time.  Requires pad lock.
 
511
 */
 
512
function _insertPadMetaData(padId, obj) {
 
513
  appjet.cache.pads.meta.put(padId, obj);
 
514
}
 
515
 
 
516
/**
 
517
 * Removes a pad's meta data, writing through to the database.
 
518
 * Used for the rare case of deleting a pad.
 
519
 */
 
520
function _removePadMetaData(padId) {
 
521
  appjet.cache.pads.meta.remove(padId);
 
522
  sqlbase.deleteJSON("PAD_META", padId);
 
523
}
 
524
 
 
525
function _getPadAPool(padId) {
 
526
  var padAPool = appjet.cache.pads.apool.get(padId);
 
527
  if (! padAPool) {
 
528
    // not in cache
 
529
    padAPool = new AttribPool();
 
530
    padAPoolJson = sqlbase.getJSON("PAD_APOOL", padId);
 
531
    if (padAPoolJson) {
 
532
      // in SQL
 
533
      padAPool.fromJsonable(padAPoolJson);
 
534
    }
 
535
    appjet.cache.pads.apool.put(padId, padAPool);
 
536
  }
 
537
  return padAPool;
 
538
}
 
539
 
 
540
/**
 
541
 * Removes a pad's apool data, writing through to the database.
 
542
 * Used for the rare case of deleting a pad.
 
543
 */
 
544
function _removePadAPool(padId) {
 
545
  appjet.cache.pads.apool.remove(padId);
 
546
  sqlbase.deleteJSON("PAD_APOOL", padId);
 
547
}
 
548
 
 
549
/**
 
550
 * Get an object for a pad that's not persisted in storage,
 
551
 * e.g. for tracking open connections.  Creates object
 
552
 * if necessary.  Requires pad lock.
 
553
 */
 
554
function _getPadTemp(padId) {
 
555
  var padTemp = appjet.cache.pads.temp.get(padId);
 
556
  if (! padTemp) {
 
557
    padTemp = {};
 
558
    appjet.cache.pads.temp.put(padId, padTemp);
 
559
  }
 
560
  return padTemp;
 
561
}
 
562
 
 
563
/**
 
564
 * Returns an object with methods for manipulating a string array, where name
 
565
 * is something like "revs" or "chat".  The object must be acquired and used
 
566
 * all within a pad lock.
 
567
 */
 
568
function _getPadStringArray(padId, name) {
 
569
  var padFoo = appjet.cache.pads[name].get(padId);
 
570
  if (! padFoo) {
 
571
    padFoo = {};
 
572
    // writes go into writeCache, which is authoritative for reads;
 
573
    // reads cause pages to be read into readCache
 
574
    padFoo.readCache = {};
 
575
    padFoo.writeCache = {};
 
576
    appjet.cache.pads[name].put(padId, padFoo);
 
577
  }
 
578
  var tableName = "PAD_"+name.toUpperCase();
 
579
  var self = {
 
580
    getEntry: function(idx) {
 
581
      var n = Number(idx);
 
582
      if (padFoo.writeCache[n]) return padFoo.writeCache[n];
 
583
      if (padFoo.readCache[n]) return padFoo.readCache[n];
 
584
      sqlbase.getPageStringArrayElements(tableName, padId, n, padFoo.readCache);
 
585
      return padFoo.readCache[n]; // null if not present in SQL
 
586
    },
 
587
    setEntry: function(idx, value) {
 
588
      var n = Number(idx);
 
589
      var v = String(value);
 
590
      padFoo.writeCache[n] = v;
 
591
    },
 
592
    getJSONEntry: function(idx) {
 
593
      var result = self.getEntry(idx);
 
594
      if (! result) return result;
 
595
      return fastJSON.parse(String(result));
 
596
    },
 
597
    setJSONEntry: function(idx, valueObj) {
 
598
      self.setEntry(idx, fastJSON.stringify(valueObj));
 
599
    },
 
600
    writeToDB: function() {
 
601
      sqlbase.putDictStringArrayElements(tableName, padId, padFoo.writeCache);
 
602
      // copy key-vals of writeCache into readCache
 
603
      var readCache = padFoo.readCache;
 
604
      var writeCache = padFoo.writeCache;
 
605
      for(var p in writeCache) {
 
606
        readCache[p] = writeCache[p];
 
607
      }
 
608
      padFoo.writeCache = {};
 
609
    }
 
610
  };
 
611
  return self;
 
612
}
 
613
 
 
614
/**
 
615
 * Destroy a string array;  writes through to the database.  Must be
 
616
 * called within a pad lock.
 
617
 */
 
618
function _destroyPadStringArray(padId, name) {
 
619
  appjet.cache.pads[name].remove(padId);
 
620
  var tableName = "PAD_"+name.toUpperCase();
 
621
  sqlbase.clearStringArray(tableName, padId);
 
622
}
 
623
 
 
624
/**
 
625
 * SELECT the row of PAD_SQLMETA for the given pad.  Requires pad lock.
 
626
 */
 
627
function _getPadSqlMeta(padId) {
 
628
  return sqlobj.selectSingle("PAD_SQLMETA", { id: padId });
 
629
}
 
630
 
 
631
function _writePadSqlMeta(padId, updates) {
 
632
  sqlobj.update("PAD_SQLMETA", { id: padId }, updates);
 
633
}
 
634
 
 
635
 
 
636
// called from dbwriter
 
637
function removeFromMemory(pad) {
 
638
  // safe to call if all data is written to SQL, otherwise will lose data;
 
639
  var padId = pad.getId();
 
640
  appjet.cache.pads.meta.remove(padId);
 
641
  appjet.cache.pads.revs.remove(padId);
 
642
  appjet.cache.pads.revs10.remove(padId);
 
643
  appjet.cache.pads.revs100.remove(padId);
 
644
  appjet.cache.pads.revs1000.remove(padId);
 
645
  appjet.cache.pads.chat.remove(padId);
 
646
  appjet.cache.pads.revmeta.remove(padId);
 
647
  appjet.cache.pads.apool.remove(padId);
 
648
  collab_server.removeFromMemory(pad);
 
649
}
 
650
 
 
651