2
* Copyright 2009 Google Inc.
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
8
* http://www.apache.org/licenses/LICENSE-2.0
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.
18
import("sqlbase.sqlbase");
19
import("sqlbase.sqlcommon");
20
import("sqlbase.sqlobj");
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");
35
jimport("java.lang.System.out.println");
37
jimport("java.util.concurrent.ConcurrentHashMap");
38
jimport("net.appjet.oui.GlobalSynchronizer");
39
jimport("net.appjet.oui.exceptionlog");
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();
55
var _JSON_CACHE_SIZE = 10000;
57
// to clear: appjet.cache.padmodel.modelcache.map.clear()
58
function _getModelCache() {
59
return syncedWithCache('padmodel.modelcache', function(cache) {
61
cache.map = new LimitedSizeMapping(_JSON_CACHE_SIZE);
67
function cleanText(txt) {
68
return txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n').replace(/\t/g, ' ').replace(/\xa0/g, ' ');
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.
78
* Note: padId is a GLOBAL id.
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);
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);
88
var mode = (rwMode || "rw").toLowerCase();
90
if (! appjet.requestCache.padsAccessing) {
91
appjet.requestCache.padsAccessing = {};
93
if (appjet.requestCache.padsAccessing[padId]) {
94
// nested access to same pad
95
var p = appjet.requestCache.padsAccessing[padId];
97
if (m && mode != "r") {
98
m.status.lastAccess = +new Date();
99
m.status.dirty = true;
104
return doWithPadLock(padId, function() {
105
return sqlcommon.inTransaction(function() {
106
var meta = _getPadMetaData(padId); // null if pad doesn't exist yet
108
if (meta && ! meta.status) {
109
meta.status = { validated: false };
112
if (meta && mode != "r") {
113
meta.status.lastAccess = +new Date();
116
function getCurrentAText() {
117
var tempObj = pad.tempObj();
118
if (! tempObj.atext) {
119
tempObj.atext = pad.getInternalRevisionAText(meta.head);
121
return tempObj.atext;
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!
128
var newRev = ++meta.head;
130
var revs = _getPadStringArray(padId, "revs");
131
revs.setEntry(newRev, theChangeset);
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;
139
revmeta.setJSONEntry(newRev, thisRevMeta);
141
updateCoarseChangesets(true);
143
function getNumForAuthor(author, dontAddIfAbsent) {
144
return pad.pool().putAttrib(['author',author||''], dontAddIfAbsent);
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') {
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.
161
if (! meta.coarseHeads) {
166
meta.coarseHeads = {10:-1, 100:-1, 1000:-1};
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)
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];
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]) {
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),
199
coarseArray.setEntry(x, cs);
200
meta.coarseHeads[level] = x;
204
meta.status.dirty = true;
208
/////////////////// "Public" API starts here (functions used by collab_server or other modules)
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) {
217
meta.head = -1; // incremented below by addRevision
218
pad.tempObj().atext = Changeset.makeAText("\n");
220
meta.keyRevInterval = 100;
221
meta.numChatMessages = 0;
223
meta.status = { validated: true };
224
meta.status.lastAccess = t;
225
meta.status.dirty = true;
226
meta.supportsTimeSlider = true;
228
var firstChangeset = Changeset.makeSplice("\n", 0, 0,
229
cleanText(optText || ''));
230
addRevision(firstChangeset, '');
232
_insertPadMetaData(padId, meta);
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
238
padevents.onNewPad(pad);
240
destroy: function() { // you may want to collab_server.bootAllUsers first
241
padevents.onDestroyPad(pad);
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 });
255
writeToDB: function() {
257
for(var k in meta) meta2[k] = meta[k];
259
sqlbase.putJSON("PAD_META", padId, meta2);
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());
270
var props = { headRev: meta.head, lastWriteTime: new Date() };
271
_writePadSqlMeta(padId, props);
274
return _getPadAPool(padId);
276
getHeadRevisionNumber: function() { return meta.head; },
277
getRevisionAuthor: function(r) {
278
var n = _getPadStringArray(padId, "revmeta").getJSONEntry(r).a;
279
return getAuthorForNum(Number(n));
281
getRevisionChangeset: function(r) {
282
return _getPadStringArray(padId, "revs").getEntry(r);
284
tempObj: function() { return _getPadTemp(padId); },
285
getKeyRevisionNumber: function(r) {
286
return Math.floor(r / meta.keyRevInterval) * meta.keyRevInterval;
288
getInternalRevisionAText: function(r) {
289
var cacheKey = "atext/C/"+r+"/"+padId;
290
var modelCache = _getModelCache();
291
var cachedValue = modelCache.get(cacheKey);
293
modelCache.touch(cacheKey);
294
//java.lang.System.out.println("HIT! "+cacheKey);
295
return Changeset.cloneAText(cachedValue);
297
//java.lang.System.out.println("MISS! "+cacheKey);
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;
305
var apool = pad.pool();
306
while (curRev < targetRev) {
308
var cs = pad.getRevisionChangeset(curRev);
309
atext = Changeset.applyToAText(cs, atext, apool);
311
modelCache.put(cacheKey, Changeset.cloneAText(atext));
314
getInternalRevisionText: function(r, optInfoObj) {
315
var atext = pad.getInternalRevisionAText(r);
316
var text = atext.text;
318
if (text.slice(-1) != "\n") {
319
optInfoObj.badLastChar = text.slice(-1);
324
getRevisionText: function(r, optInfoObj) {
325
var internalText = pad.getInternalRevisionText(r, optInfoObj);
326
return internalText.slice(0, -1);
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);
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
338
appendRevision: function(theChangeset, author, optDatestamp) {
339
addRevision(theChangeset, author || '', optDatestamp);
341
appendChatMessage: function(obj) {
342
var index = meta.numChatMessages;
343
meta.numChatMessages++;
344
var chat = _getPadStringArray(padId, "chat");
345
chat.setJSONEntry(index, obj);
347
getNumChatMessages: function() {
348
return meta.numChatMessages;
350
getChatMessage: function(i) {
351
var chat = _getPadStringArray(padId, "chat");
352
return chat.getJSONEntry(i);
354
getPadOptionsObj: function() {
355
var data = pad.getDataRoot();
356
if (! data.padOptions) {
357
data.padOptions = {};
359
if ((! data.padOptions.guestPolicy) ||
360
(data.padOptions.guestPolicy == 'ask')) {
361
data.padOptions.guestPolicy = 'deny';
363
return data.padOptions;
365
getGuestPolicy: function() {
367
return pad.getPadOptionsObj().guestPolicy;
369
setGuestPolicy: function(policy) {
370
pad.getPadOptionsObj().guestPolicy = policy;
372
getDataRoot: function() {
373
var dataRoot = meta.dataRoot;
376
meta.dataRoot = dataRoot;
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);
389
return authors.getJSONEntry(n);
392
setAuthorData: function(author, data) {
393
var authors = _getPadStringArray(padId, "authors");
394
var n = getNumForAuthor(author);
395
authors.setJSONEntry(n, data);
397
adoptChangesetAttribs: function(cs, oldPool) {
398
return Changeset.moveOpsToNewPool(cs, oldPool, pad.pool());
400
eachATextAuthor: function(atext, func) {
402
Changeset.eachAttribNumber(atext.attribs, function(n) {
405
var author = getAuthorForNum(n);
412
getCoarseChangeset: function(start, numChangesets) {
413
updateCoarseChangesets();
415
if (!(numChangesets == 10 || numChangesets == 100 ||
416
numChangesets == 1000)) {
419
var level = numChangesets;
420
var x = Math.floor(start / level);
421
if (!(x >= 0 && x*level == start)) {
425
var cs = _getPadStringArray(padId, "revs"+level).getEntry(x);
433
getSupportsTimeSlider: function() {
434
if (! ('supportsTimeSlider' in meta)) {
435
if (padutils.isProPadId(padId)) {
443
return !! meta.supportsTimeSlider;
446
setSupportsTimeSlider: function(v) {
447
meta.supportsTimeSlider = v;
449
get _meta() { return meta; }
453
padutils.setCurrentPad(padId);
454
appjet.requestCache.padsAccessing[padId] = pad;
458
padutils.clearCurrentPad();
459
delete appjet.requestCache.padsAccessing[padId];
462
meta.status.dirty = true;
464
if (meta.status.dirty) {
465
dbwriter.notifyPadDirty(padId);
474
* Call an arbitrary function with no arguments inside an exclusive
475
* lock on a padId, and return the result.
477
function doWithPadLock(padId, func) {
478
var lockName = "document/"+padId;
479
return sync.doWithStringLock(lockName, func);
482
function isPadLockHeld(padId) {
483
var lockName = "document/"+padId;
484
return GlobalSynchronizer.isHeld(lockName);
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.
492
function _getPadMetaData(padId) {
493
var padMeta = appjet.cache.pads.meta.get(padId);
496
padMeta = sqlbase.getJSON("PAD_META", padId);
502
appjet.cache.pads.meta.put(padId, padMeta);
509
* Sets a pad's meta-data object, such as when creating
510
* a pad for the first time. Requires pad lock.
512
function _insertPadMetaData(padId, obj) {
513
appjet.cache.pads.meta.put(padId, obj);
517
* Removes a pad's meta data, writing through to the database.
518
* Used for the rare case of deleting a pad.
520
function _removePadMetaData(padId) {
521
appjet.cache.pads.meta.remove(padId);
522
sqlbase.deleteJSON("PAD_META", padId);
525
function _getPadAPool(padId) {
526
var padAPool = appjet.cache.pads.apool.get(padId);
529
padAPool = new AttribPool();
530
padAPoolJson = sqlbase.getJSON("PAD_APOOL", padId);
533
padAPool.fromJsonable(padAPoolJson);
535
appjet.cache.pads.apool.put(padId, padAPool);
541
* Removes a pad's apool data, writing through to the database.
542
* Used for the rare case of deleting a pad.
544
function _removePadAPool(padId) {
545
appjet.cache.pads.apool.remove(padId);
546
sqlbase.deleteJSON("PAD_APOOL", padId);
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.
554
function _getPadTemp(padId) {
555
var padTemp = appjet.cache.pads.temp.get(padId);
558
appjet.cache.pads.temp.put(padId, padTemp);
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.
568
function _getPadStringArray(padId, name) {
569
var padFoo = appjet.cache.pads[name].get(padId);
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);
578
var tableName = "PAD_"+name.toUpperCase();
580
getEntry: function(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
587
setEntry: function(idx, value) {
589
var v = String(value);
590
padFoo.writeCache[n] = v;
592
getJSONEntry: function(idx) {
593
var result = self.getEntry(idx);
594
if (! result) return result;
595
return fastJSON.parse(String(result));
597
setJSONEntry: function(idx, valueObj) {
598
self.setEntry(idx, fastJSON.stringify(valueObj));
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];
608
padFoo.writeCache = {};
615
* Destroy a string array; writes through to the database. Must be
616
* called within a pad lock.
618
function _destroyPadStringArray(padId, name) {
619
appjet.cache.pads[name].remove(padId);
620
var tableName = "PAD_"+name.toUpperCase();
621
sqlbase.clearStringArray(tableName, padId);
625
* SELECT the row of PAD_SQLMETA for the given pad. Requires pad lock.
627
function _getPadSqlMeta(padId) {
628
return sqlobj.selectSingle("PAD_SQLMETA", { id: padId });
631
function _writePadSqlMeta(padId, updates) {
632
sqlobj.update("PAD_SQLMETA", { id: padId }, updates);
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);