1/*
2 * Copyright (C) 2012 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31/**
32 * @constructor
33 * @extends {WebInspector.View}
34 * @param {WebInspector.IndexedDBModel.Database} database
35 */
36WebInspector.IDBDatabaseView = function(database)
37{
38    WebInspector.View.call(this);
39    this.registerRequiredCSS("indexedDBViews.css");
40
41    this.element.addStyleClass("fill");
42    this.element.addStyleClass("indexed-db-database-view");
43
44    this._headersListElement = this.element.createChild("ol", "outline-disclosure");
45    this._headersTreeOutline = new TreeOutline(this._headersListElement);
46    this._headersTreeOutline.expandTreeElementsWhenArrowing = true;
47
48    this._securityOriginTreeElement = new TreeElement("", null, false);
49    this._securityOriginTreeElement.selectable = false;
50    this._headersTreeOutline.appendChild(this._securityOriginTreeElement);
51
52    this._nameTreeElement = new TreeElement("", null, false);
53    this._nameTreeElement.selectable = false;
54    this._headersTreeOutline.appendChild(this._nameTreeElement);
55
56    this._intVersionTreeElement = new TreeElement("", null, false);
57    this._intVersionTreeElement.selectable = false;
58    this._headersTreeOutline.appendChild(this._intVersionTreeElement);
59
60    this._stringVersionTreeElement = new TreeElement("", null, false);
61    this._stringVersionTreeElement.selectable = false;
62    this._headersTreeOutline.appendChild(this._stringVersionTreeElement);
63
64    this.update(database);
65}
66
67WebInspector.IDBDatabaseView.prototype = {
68    /**
69     * @param {string} name
70     * @param {string} value
71     */
72    _formatHeader: function(name, value)
73    {
74        var fragment = document.createDocumentFragment();
75        fragment.createChild("div", "attribute-name").textContent = name + ":";
76        fragment.createChild("div", "attribute-value source-code").textContent = value;
77
78        return fragment;
79    },
80
81    _refreshDatabase: function()
82    {
83        this._securityOriginTreeElement.title = this._formatHeader(WebInspector.UIString("Security origin"), this._database.databaseId.securityOrigin);
84        this._nameTreeElement.title = this._formatHeader(WebInspector.UIString("Name"), this._database.databaseId.name);
85        this._stringVersionTreeElement.title = this._formatHeader(WebInspector.UIString("String Version"), this._database.version);
86        this._intVersionTreeElement.title = this._formatHeader(WebInspector.UIString("Integer Version"), this._database.intVersion);
87    },
88
89    /**
90     * @param {WebInspector.IndexedDBModel.Database} database
91     */
92    update: function(database)
93    {
94        this._database = database;
95        this._refreshDatabase();
96    },
97
98    __proto__: WebInspector.View.prototype
99}
100
101
102/**
103 * @constructor
104 * @extends {WebInspector.View}
105 * @param {WebInspector.IndexedDBModel} model
106 * @param {WebInspector.IndexedDBModel.DatabaseId} databaseId
107 * @param {WebInspector.IndexedDBModel.ObjectStore} objectStore
108 * @param {WebInspector.IndexedDBModel.Index} index
109 */
110WebInspector.IDBDataView = function(model, databaseId, objectStore, index)
111{
112    WebInspector.View.call(this);
113    this.registerRequiredCSS("indexedDBViews.css");
114
115    this._model = model;
116    this._databaseId = databaseId;
117    this._isIndex = !!index;
118
119    this.element.addStyleClass("indexed-db-data-view");
120
121    var editorToolbar = this._createEditorToolbar();
122    this.element.appendChild(editorToolbar);
123
124    this._dataGridContainer = this.element.createChild("div", "fill");
125    this._dataGridContainer.addStyleClass("data-grid-container");
126
127    this._refreshButton = new WebInspector.StatusBarButton(WebInspector.UIString("Refresh"), "refresh-storage-status-bar-item");
128    this._refreshButton.addEventListener("click", this._refreshButtonClicked, this);
129
130    this._clearButton = new WebInspector.StatusBarButton(WebInspector.UIString("Clear object store"), "clear-storage-status-bar-item");
131    this._clearButton.addEventListener("click", this._clearButtonClicked, this);
132
133    this._pageSize = 50;
134    this._skipCount = 0;
135
136    this.update(objectStore, index);
137    this._entries = [];
138}
139
140WebInspector.IDBDataView.prototype = {
141    /**
142     * @return {WebInspector.DataGrid}
143     */
144    _createDataGrid: function()
145    {
146        var keyPath = this._isIndex ? this._index.keyPath : this._objectStore.keyPath;
147
148        var columns = [];
149        columns.push({id: "number", title: WebInspector.UIString("#"), width: "50px"});
150        columns.push({id: "key", titleDOMFragment: this._keyColumnHeaderFragment(WebInspector.UIString("Key"), keyPath)});
151        if (this._isIndex)
152            columns.push({id: "primaryKey", titleDOMFragment: this._keyColumnHeaderFragment(WebInspector.UIString("Primary key"), this._objectStore.keyPath)});
153        columns.push({id: "value", title: WebInspector.UIString("Value")});
154
155        var dataGrid = new WebInspector.DataGrid(columns);
156        return dataGrid;
157    },
158
159    /**
160     * @param {string} prefix
161     * @param {*} keyPath
162     * @return {DocumentFragment}
163     */
164    _keyColumnHeaderFragment: function(prefix, keyPath)
165    {
166        var keyColumnHeaderFragment = document.createDocumentFragment();
167        keyColumnHeaderFragment.appendChild(document.createTextNode(prefix));
168        if (keyPath === null)
169            return keyColumnHeaderFragment;
170
171        keyColumnHeaderFragment.appendChild(document.createTextNode(" (" + WebInspector.UIString("Key path: ")));
172        if (keyPath instanceof Array) {
173            keyColumnHeaderFragment.appendChild(document.createTextNode("["));
174            for (var i = 0; i < keyPath.length; ++i) {
175                if (i != 0)
176                    keyColumnHeaderFragment.appendChild(document.createTextNode(", "));
177                keyColumnHeaderFragment.appendChild(this._keyPathStringFragment(keyPath[i]));
178            }
179            keyColumnHeaderFragment.appendChild(document.createTextNode("]"));
180        } else {
181            var keyPathString = /** @type {string} */ (keyPath);
182            keyColumnHeaderFragment.appendChild(this._keyPathStringFragment(keyPathString));
183        }
184        keyColumnHeaderFragment.appendChild(document.createTextNode(")"));
185        return keyColumnHeaderFragment;
186    },
187
188    /**
189     * @param {string} keyPathString
190     * @return {DocumentFragment}
191     */
192    _keyPathStringFragment: function(keyPathString)
193    {
194        var keyPathStringFragment = document.createDocumentFragment();
195        keyPathStringFragment.appendChild(document.createTextNode("\""));
196        var keyPathSpan = keyPathStringFragment.createChild("span", "source-code console-formatted-string");
197        keyPathSpan.textContent = keyPathString;
198        keyPathStringFragment.appendChild(document.createTextNode("\""));
199        return keyPathStringFragment;
200    },
201
202    /**
203     * @return {Element}
204     */
205    _createEditorToolbar: function()
206    {
207        var editorToolbar = document.createElement("div");
208        editorToolbar.addStyleClass("status-bar");
209        editorToolbar.addStyleClass("data-view-toolbar");
210
211        this._pageBackButton = editorToolbar.createChild("button", "back-button");
212        this._pageBackButton.addStyleClass("status-bar-item");
213        this._pageBackButton.title = WebInspector.UIString("Show previous page.");
214        this._pageBackButton.disabled = true;
215        this._pageBackButton.appendChild(document.createElement("img"));
216        this._pageBackButton.addEventListener("click", this._pageBackButtonClicked.bind(this), false);
217        editorToolbar.appendChild(this._pageBackButton);
218
219        this._pageForwardButton = editorToolbar.createChild("button", "forward-button");
220        this._pageForwardButton.addStyleClass("status-bar-item");
221        this._pageForwardButton.title = WebInspector.UIString("Show next page.");
222        this._pageForwardButton.disabled = true;
223        this._pageForwardButton.appendChild(document.createElement("img"));
224        this._pageForwardButton.addEventListener("click", this._pageForwardButtonClicked.bind(this), false);
225        editorToolbar.appendChild(this._pageForwardButton);
226
227        this._keyInputElement = editorToolbar.createChild("input", "key-input");
228        this._keyInputElement.placeholder = WebInspector.UIString("Start from key");
229        this._keyInputElement.addEventListener("paste", this._keyInputChanged.bind(this));
230        this._keyInputElement.addEventListener("cut", this._keyInputChanged.bind(this));
231        this._keyInputElement.addEventListener("keypress", this._keyInputChanged.bind(this));
232        this._keyInputElement.addEventListener("keydown", this._keyInputChanged.bind(this));
233
234        return editorToolbar;
235    },
236
237    _pageBackButtonClicked: function()
238    {
239        this._skipCount = Math.max(0, this._skipCount - this._pageSize);
240        this._updateData(false);
241    },
242
243    _pageForwardButtonClicked: function()
244    {
245        this._skipCount = this._skipCount + this._pageSize;
246        this._updateData(false);
247    },
248
249    _keyInputChanged: function()
250    {
251        window.setTimeout(this._updateData.bind(this, false), 0);
252    },
253
254    /**
255     * @param {WebInspector.IndexedDBModel.ObjectStore} objectStore
256     * @param {WebInspector.IndexedDBModel.Index} index
257     */
258    update: function(objectStore, index)
259    {
260        this._objectStore = objectStore;
261        this._index = index;
262
263        if (this._dataGrid)
264            this._dataGrid.detach();
265        this._dataGrid = this._createDataGrid();
266        this._dataGrid.show(this._dataGridContainer);
267
268        this._skipCount = 0;
269        this._updateData(true);
270    },
271
272    /**
273     * @param {string} keyString
274     */
275    _parseKey: function(keyString)
276    {
277        var result;
278        try {
279            result = JSON.parse(keyString);
280        } catch (e) {
281            result = keyString;
282        }
283        return result;
284    },
285
286    /**
287     * @return {string}
288     */
289    _stringifyKey: function(key)
290    {
291        if (typeof(key) === "string")
292            return key;
293        return JSON.stringify(key);
294    },
295
296    /**
297     * @param {boolean} force
298     */
299    _updateData: function(force)
300    {
301        var key = this._parseKey(this._keyInputElement.value);
302        var pageSize = this._pageSize;
303        var skipCount = this._skipCount;
304        this._refreshButton.setEnabled(false);
305        this._clearButton.setEnabled(!this._isIndex);
306
307        if (!force && this._lastKey === key && this._lastPageSize === pageSize && this._lastSkipCount === skipCount)
308            return;
309
310        if (this._lastKey !== key || this._lastPageSize !== pageSize) {
311            skipCount = 0;
312            this._skipCount = 0;
313        }
314        this._lastKey = key;
315        this._lastPageSize = pageSize;
316        this._lastSkipCount = skipCount;
317
318        /**
319         * @param {Array.<WebInspector.IndexedDBModel.Entry>} entries
320         * @param {boolean} hasMore
321         */
322        function callback(entries, hasMore)
323        {
324            this._refreshButton.setEnabled(true);
325            this.clear();
326            this._entries = entries;
327            for (var i = 0; i < entries.length; ++i) {
328                var data = {};
329                data["number"] = i + skipCount;
330                data["key"] = entries[i].key;
331                data["primaryKey"] = entries[i].primaryKey;
332                data["value"] = entries[i].value;
333
334                var primaryKey = JSON.stringify(this._isIndex ? entries[i].primaryKey : entries[i].key);
335                var node = new WebInspector.IDBDataGridNode(data);
336                this._dataGrid.rootNode().appendChild(node);
337            }
338
339            this._pageBackButton.disabled = skipCount === 0;
340            this._pageForwardButton.disabled = !hasMore;
341        }
342
343        var idbKeyRange = key ? window.webkitIDBKeyRange.lowerBound(key) : null;
344        if (this._isIndex)
345            this._model.loadIndexData(this._databaseId, this._objectStore.name, this._index.name, idbKeyRange, skipCount, pageSize, callback.bind(this));
346        else
347            this._model.loadObjectStoreData(this._databaseId, this._objectStore.name, idbKeyRange, skipCount, pageSize, callback.bind(this));
348    },
349
350    _refreshButtonClicked: function(event)
351    {
352        this._updateData(true);
353    },
354
355    _clearButtonClicked: function(event)
356    {
357        function cleared() {
358            this._clearButton.setEnabled(true);
359            this._updateData(true);
360        }
361        this._clearButton.setEnabled(false);
362        this._model.clearObjectStore(this._databaseId, this._objectStore.name, cleared.bind(this));
363    },
364
365    statusBarItems: function()
366    {
367        return [this._refreshButton.element, this._clearButton.element];
368    },
369
370    clear: function()
371    {
372        this._dataGrid.rootNode().removeChildren();
373        for (var i = 0; i < this._entries.length; ++i) {
374            this._entries[i].key.release();
375            this._entries[i].primaryKey.release();
376            this._entries[i].value.release();
377        }
378        this._entries = [];
379    },
380
381    __proto__: WebInspector.View.prototype
382}
383
384/**
385 * @constructor
386 * @extends {WebInspector.DataGridNode}
387 * @param {*} data
388 */
389WebInspector.IDBDataGridNode = function(data)
390{
391    WebInspector.DataGridNode.call(this, data, false);
392    this.selectable = false;
393}
394
395WebInspector.IDBDataGridNode.prototype = {
396    /**
397     * @return {Element}
398     */
399    createCell: function(columnIdentifier)
400    {
401        var cell = WebInspector.DataGridNode.prototype.createCell.call(this, columnIdentifier);
402        var value = this.data[columnIdentifier];
403
404        switch (columnIdentifier) {
405        case "value":
406        case "key":
407        case "primaryKey":
408            cell.removeChildren();
409            this._formatValue(cell, value);
410            break;
411        default:
412        }
413
414        return cell;
415    },
416
417    _formatValue: function(cell, value)
418    {
419        var type = value.subtype || value.type;
420        var contents = cell.createChild("div", "source-code console-formatted-" + type);
421
422        switch (type) {
423        case "object":
424        case "array":
425            var section = new WebInspector.ObjectPropertiesSection(value, value.description)
426            section.editable = false;
427            section.skipProto = true;
428            contents.appendChild(section.element);
429            break;
430        case "string":
431            contents.addStyleClass("primitive-value");
432            contents.appendChild(document.createTextNode("\"" + value.description + "\""));
433            break;
434        default:
435            contents.addStyleClass("primitive-value");
436            contents.appendChild(document.createTextNode(value.description));
437        }
438    },
439
440    __proto__: WebInspector.DataGridNode.prototype
441}
442
443