From 7a9889b18cad4b96081b9158d0022b337b6af4c8 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Sat, 31 Oct 2009 17:40:16 -0400 Subject: [PATCH] Add IO#File. New review: owner: dougkearns I like this for the most part, except that it has to go to lengths to wrap the original nsIFile correctly, an that it can't be passed directly to other XPCOM components. It makes most operations on files a lot cleaner, though. --- common/content/browser.js | 2 +- common/content/buffer.js | 6 +- common/content/editor.js | 8 +- common/content/events.js | 21 +- common/content/io.js | 410 +++++++++++++++++++----------------- common/content/liberator.js | 8 +- common/content/services.js | 1 + common/content/style.js | 16 +- common/content/util.js | 4 +- 9 files changed, 244 insertions(+), 232 deletions(-) diff --git a/common/content/browser.js b/common/content/browser.js index f522cbcc..842aa185 100644 --- a/common/content/browser.js +++ b/common/content/browser.js @@ -177,7 +177,7 @@ function Browser() //{{{ { if (/^file:\/|^\//.test(url)) { - let file = io.getFile(url); + let file = io.File(url); return file.exists() && file.isDirectory(); } else diff --git a/common/content/buffer.js b/common/content/buffer.js index 95606569..3a435dc3 100644 --- a/common/content/buffer.js +++ b/common/content/buffer.js @@ -181,7 +181,7 @@ function Buffer() //{{{ function openUploadPrompt(elem) { commandline.input("Upload file: ", function (path) { - let file = io.getFile(path); + let file = io.File(path); if (!file.exists()) return void liberator.beep(); @@ -541,7 +541,7 @@ function Buffer() //{{{ if (arg) { options.setPref("print.print_to_file", "true"); - options.setPref("print.print_to_filename", io.getFile(arg.substr(1)).path); + options.setPref("print.print_to_filename", io.File(arg.substr(1)).path); liberator.echomsg("Printing to file: " + arg.substr(1)); } else @@ -617,7 +617,7 @@ function Buffer() //{{{ if (filename) { - let file = io.getFile(filename); + let file = io.File(filename); if (file.exists() && !args.bang) return void liberator.echoerr("E13: File exists (add ! to override)"); diff --git a/common/content/editor.js b/common/content/editor.js index fe0c26dc..3bd5bfe1 100644 --- a/common/content/editor.js +++ b/common/content/editor.js @@ -892,16 +892,16 @@ function Editor() //{{{ textBox.style.backgroundColor = "#bbbbbb"; } - if (!io.writeFile(tmpfile, text)) - throw "Input contains characters not valid in the current " + - "file encoding"; + if (!tmpfile.write(text)) + throw Error("Input contains characters not valid in the current " + + "file encoding"); this.editFileExternally(tmpfile.path); if (textBox) textBox.removeAttribute("readonly"); - let val = io.readFile(tmpfile); + let val = tmpfile.read(); if (textBox) textBox.value = val; else diff --git a/common/content/events.js b/common/content/events.js index 0ea683f2..bf80129c 100644 --- a/common/content/events.js +++ b/common/content/events.js @@ -563,21 +563,20 @@ function Events() //{{{ for (let [, dir] in Iterator(dirs)) { liberator.echomsg('Searching for "macros/*" in "' + dir.path + '"', 2); - liberator.log("Sourcing macros directory: " + dir.path + "...", 3); let files = io.readDirectory(dir.path); + for (let file in dir.iterDirectory()) + { + if (file.exists() && !file.isDirectory() && file.isReadable() && + /^[\w_-]+(\.vimp)?$/i.test(file.leafName)) + { + let name = file.leafName.replace(/\.vimp$/i, ""); + macros.set(name, file.read().split("\n")[0]); - files.forEach(function (file) { - if (!file.exists() || file.isDirectory() || - !file.isReadable() || !/^[\w_-]+(\.vimp)?$/i.test(file.leafName)) - return; - - let name = file.leafName.replace(/\.vimp$/i, ""); - macros.set(name, io.readFile(file).split("\n")[0]); - - liberator.log("Macro " + name + " added: " + macros.get(name), 5); - }); + liberator.log("Macro " + name + " added: " + macros.get(name), 5); + } + } } } else diff --git a/common/content/io.js b/common/content/io.js index e3ba52a0..b9e626f8 100644 --- a/common/content/io.js +++ b/common/content/io.js @@ -77,17 +77,16 @@ function IO() //{{{ { if (!list) return []; - else - // empty list item means the current directory - return list.replace(/,$/, "").split(",") - .map(function (dir) dir == "" ? io.getCurrentDirectory().path : dir); + // empty list item means the current directory + return list.replace(/,$/, "").split(",") + .map(function (dir) dir == "" ? io.getCurrentDirectory().path : dir); } function replacePathSep(path) path.replace("/", IO.PATH_SEP, "g"); function joinPaths(head, tail) { - let path = self.getFile(head); + let path = self.File(head); try { path.appendRelativePath(self.expandPath(tail, true)); // FIXME: should only expand env vars and normalise path separators @@ -257,7 +256,7 @@ function IO() //{{{ return void liberator.echoerr("E172: Only one file name allowed"); let filename = args[0] || io.getRCFile(null, true).path; - let file = io.getFile(filename); + let file = io.File(filename); if (file.exists() && !args.bang) return void liberator.echoerr("E189: \"" + filename + "\" exists (add ! to override)"); @@ -281,7 +280,7 @@ function IO() //{{{ try { - io.writeFile(file, lines.join("\n")); + file.write(lines.join("\n")); } catch (e) { @@ -372,7 +371,7 @@ function IO() //{{{ /////////////////////////////////////////////////////////////////////////////{{{ liberator.registerObserver("load_completion", function () { - completion.setFunctionCompleter([self.getFile, self.expandPath], + completion.setFunctionCompleter([self.File, self.expandPath], [function (context, obj, args) { context.quote[2] = ""; completion.file(context, true); @@ -451,10 +450,10 @@ function IO() //{{{ for (let [, dirName] in Iterator(dirNames)) { - let dir = io.getFile(dirName); + let dir = io.File(dirName); if (dir.exists() && dir.isDirectory()) { - commands.push([[file.leafName, dir.path] for ([i, file] in Iterator(io.readDirectory(dir))) + commands.push([[file.leafName, dir.path] for (file in dir.iterDirectory()) if (file.isFile() && file.isExecutable())]); } } @@ -466,11 +465,201 @@ function IO() //{{{ completion.addUrlCompleter("f", "Local files", completion.file); }); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// File //////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + /** + * @class File A class to wrap nsIFile objects and simplify operations + * thereon. + * + * @param {nsIFile|string} path Expanded according to {@link IO#expandPath} + * @param {boolean} checkPWD Whether to allow expansion relative to the + * current directory. @default true + */ + function File(path, checkPWD) + { + let self = { __proto__: File.prototype } + if (arguments.length < 2) + checkPWD = true; + + self.file = services.create("file"); + + if (path instanceof Ci.nsIFile) + self.file = path; + else if (/file:\/\//.test(path)) + self.file = services.create("file:").getFileFromURLSpec(path); + else + { + let expandedPath = io.expandPath(path); + + if (!isAbsolutePath(expandedPath) && checkPWD) + self = joinPaths(io.getCurrentDirectory().path, expandedPath); + else + self.file.initWithPath(expandedPath); + } + self.wrappedNative = self.file; + return self; + } + File.prototype = { + __noSuchMethod__: function (meth, args) + { + return this.wrappedNative[meth].apply(this.wrappedNative, + args.map(function (a) a instanceof File ? a.wrappedNative : a)); + }, + + /** + * Iterates over the objects in this directory. + */ + iterDirectory: function () + { + if (!this.file.isDirectory()) + throw Error("Not a directory"); + let entries = this.file.directoryEntries; + while (entries.hasMoreElements()) + yield File(entries.getNext().QueryInterface(Ci.nsIFile)); + }, + /** + * Returns the list of files in this directory. + * + * @param {boolean} sort Whether to sort the returned directory + * entries. + * @returns {nsIFile[]} + */ + readDirectory: function (sort) + { + if (!this.file.isDirectory()) + throw Error("Not a directory"); + + let array = [e for (e in this.iterDirectory())]; + if (sort) + array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path)); + return array; + }, + + /** + * Reads this file's entire contents in "text" mode and returns the + * content as a string. + * + * @param {string} encoding The encoding from which to decode the file. + * @default options["fileencoding"] + * @returns {string} + */ + read: function (encoding) + { + let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + let icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream); + + if (!encoding) + encoding = options["fileencoding"]; + + ifstream.init(this.file, -1, 0, 0); + icstream.init(ifstream, encoding, 4096, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); // 4096 bytes buffering + + let buffer = []; + let str = {}; + while (icstream.readString(4096, str) != 0) + buffer.push(str.value); + + icstream.close(); + ifstream.close(); + return buffer.join(""); + }, + + /** + * Writes the string buf to this file. + * + * @param {string} buf The file content. + * @param {string|number} mode The file access mode, a bitwise OR of + * the following flags: + * {@link #MODE_RDONLY}: 0x01 + * {@link #MODE_WRONLY}: 0x02 + * {@link #MODE_RDWR}: 0x04 + * {@link #MODE_CREATE}: 0x08 + * {@link #MODE_APPEND}: 0x10 + * {@link #MODE_TRUNCATE}: 0x20 + * {@link #MODE_SYNC}: 0x40 + * Alternatively, the following abbreviations may be used: + * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE} + * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND} + * @default ">" + * @param {number} perms The file mode bits of the created file. This + * is only used when creating a new file and does not change + * permissions if the file exists. + * @default 0644 + * @param {string} encoding The encoding to used to write the file. + * @default options["fileencoding"] + */ + write: function (buf, mode, perms, encoding) + { + let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); + function getStream(defaultChar) + { + let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream); + stream.init(ofstream, encoding, 0, defaultChar); + return stream; + } + + if (!encoding) + encoding = options["fileencoding"]; + + if (mode == ">>") + mode = self.MODE_WRONLY | self.MODE_CREATE | self.MODE_APPEND; + else if (!mode || mode == ">") + mode = self.MODE_WRONLY | self.MODE_CREATE | self.MODE_TRUNCATE; + + if (!perms) + perms = 0644; + + ofstream.init(this.file, mode, perms, 0); + let ocstream = getStream(0); + try + { + ocstream.writeString(buf); + } + catch (e) + { + liberator.dump(e); + if (e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) + { + ocstream = getStream("?".charCodeAt(0)); + ocstream.writeString(buf); + return false; + } + else + throw e; + } + finally + { + try + { + ocstream.close(); + } + catch (e) {} + ofstream.close(); + } + return true; + }, + }; + /* It would be nice if there were a simpler way to do this. */ + ("leafName nativeLeafName permissions permissionsOfLink lastModifiedTime " + + "lastModifiedTimeOfLink fileSize fileSizeOfLink target nativeTarget path " + + "nativePath parent directoryEntries").split(" ").forEach(function (p) { + File.prototype.__defineGetter__(p, function () this.file[p]); + File.prototype.__defineSetter__(p, function (val) this.file[p] = val); + }); + + /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PUBLIC SECTION ////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ const self = { + /** + * @property {function} File class. + * @final + */ + File: File, /** * @property {number} Open for reading only. @@ -562,7 +751,7 @@ function IO() //{{{ */ getCurrentDirectory: function () { - let dir = self.getFile(cwd.path); + let dir = self.File(cwd.path); // NOTE: the directory could have been deleted underneath us so // fallback to the process's CWD @@ -586,7 +775,7 @@ function IO() //{{{ [cwd, oldcwd] = [oldcwd, this.getCurrentDirectory()]; else { - let dir = self.getFile(newDir); + let dir = self.File(newDir); if (!dir.exists() || !dir.isDirectory()) { @@ -612,7 +801,6 @@ function IO() //{{{ dirs = dirs.map(function (dir) joinPaths(dir, name)) .filter(function (dir) dir.exists() && dir.isDirectory() && dir.isReadable()); - return dirs; }, @@ -644,44 +832,11 @@ function IO() //{{{ return null; }, - // return a nsILocalFile for path where you can call isDirectory(), etc. on - // caller must check with .exists() if the returned file really exists - // also expands relative paths - /** - * Returns an nsIFile object for path, which is expanded - * according to {@link #expandPath}. - * - * @param {string} path The path used to create the file object. - * @param {boolean} noCheckPWD Whether to allow a relative path. - * @returns {nsIFile} - */ - getFile: function (path, noCheckPWD) - { - let file = services.create("file"); - - if (/file:\/\//.test(path)) - { - file = Cc["@mozilla.org/network/protocol;1?name=file"].createInstance(Ci.nsIFileProtocolHandler) - .getFileFromURLSpec(path); - } - else - { - let expandedPath = self.expandPath(path); - - if (!isAbsolutePath(expandedPath) && !noCheckPWD) - file = joinPaths(self.getCurrentDirectory().path, expandedPath); - else - file.initWithPath(expandedPath); - } - - return file; - }, - // TODO: make secure /** * Creates a temporary file. * - * @returns {nsIFile} + * @returns {File} */ createTempFile: function () { @@ -690,148 +845,9 @@ function IO() //{{{ file.append(config.tempFile); file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); - return file; + return self.File(file); }, - /** - * Returns the list of files in dir. - * - * @param {nsIFile|string} dir The directory to read, either a full - * pathname or an instance of nsIFile. - * @param {boolean} sort Whether to sort the returned directory - * entries. - * @returns {nsIFile[]} - */ - readDirectory: function (dir, sort) - { - if (typeof dir == "string") - dir = self.getFile(dir); - - if (dir.isDirectory()) - { - let entries = dir.directoryEntries; - let array = []; - while (entries.hasMoreElements()) - { - let entry = entries.getNext(); - array.push(entry.QueryInterface(Ci.nsIFile)); - } - if (sort) - array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path)); - return array; - } - else - return []; // XXX: or should it throw an error, probably yes? - // Yes, though frankly this should be a precondition so... --djk - }, - - /** - * Reads a file in "text" mode and returns the content as a string. - * - * @param {nsIFile|string} file The file to read, either a full - * pathname or an instance of nsIFile. - * @returns {string} - */ - readFile: function (file, encoding) - { - let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); - let icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream); - - if (!encoding) - encoding = options["fileencoding"]; - if (typeof file == "string") - file = self.getFile(file); - - ifstream.init(file, -1, 0, 0); - icstream.init(ifstream, encoding, 4096, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); // 4096 bytes buffering - - let buffer = ""; - let str = {}; - while (icstream.readString(4096, str) != 0) - buffer += str.value; - - icstream.close(); - ifstream.close(); - - return buffer; - }, - - /** - * Writes the string buf to a file. - * - * @param {nsIFile|string} file The file to write, either a full - * pathname or an instance of nsIFile. - * @param {string} buf The file content. - * @param {string|number} mode The file access mode, a bitwise OR of - * the following flags: - * {@link #MODE_RDONLY}: 0x01 - * {@link #MODE_WRONLY}: 0x02 - * {@link #MODE_RDWR}: 0x04 - * {@link #MODE_CREATE}: 0x08 - * {@link #MODE_APPEND}: 0x10 - * {@link #MODE_TRUNCATE}: 0x20 - * {@link #MODE_SYNC}: 0x40 - * Alternatively, the following abbreviations may be used: - * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE} - * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND} - * @default ">" - * @param {number} perms The file mode bits of the created file. This - * is only used when creating a new file and does not change - * permissions if the file exists. - * @default 0644 - */ - writeFile: function (file, buf, mode, perms, encoding) - { - let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); - function getStream(defaultChar) - { - let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream); - stream.init(ofstream, encoding, 0, defaultChar); - return stream; - } - - if (!encoding) - encoding = options["fileencoding"]; - if (typeof file == "string") - file = self.getFile(file); - - if (mode == ">>") - mode = self.MODE_WRONLY | self.MODE_CREATE | self.MODE_APPEND; - else if (!mode || mode == ">") - mode = self.MODE_WRONLY | self.MODE_CREATE | self.MODE_TRUNCATE; - - if (!perms) - perms = 0644; - - ofstream.init(file, mode, perms, 0); - let ocstream = getStream(0); - try - { - ocstream.writeString(buf); - } - catch (e) - { - liberator.dump(e); - if (e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) - { - ocstream = getStream("?".charCodeAt(0)); - ocstream.writeString(buf); - return false; - } - else - throw e; - } - finally - { - try - { - ocstream.close(); - } - catch (e) {} - ofstream.close(); - } - return true; - }, /** * Runs an external program. @@ -849,7 +865,7 @@ function IO() //{{{ let file; if (isAbsolutePath(program)) - file = self.getFile(program, true); + file = self.File(program, true); else { let dirs = services.get("environment").get("PATH").split(WINDOWS ? ";" : ":"); @@ -891,7 +907,7 @@ lookup: let process = services.create("process"); - process.init(file); + process.init(file.wrappedNative); process.run(blocking, args.map(String), args.length); return process.exitValue; @@ -952,7 +968,7 @@ lookup: let wasSourcing = self.sourcing; try { - var file = self.getFile(filename); + var file = self.File(filename); self.sourcing = { file: file.path, line: 0 @@ -975,8 +991,8 @@ lookup: liberator.echomsg("sourcing \"" + filename + "\"", 2); - let str = self.readFile(file); - let uri = services.get("io").newFileURI(file); + let str = file.read(); + let uri = services.get("io").newFileURI(file.wrappedNative); // handle pure JavaScript files specially if (/\.js$/.test(filename)) @@ -990,7 +1006,7 @@ lookup: let err = new Error(); for (let [k, v] in Iterator(e)) err[k] = v; - err.echoerr = file.path + ":" + e.lineNumber + ": " + e; + err.echoerr = <>{file.path}:{e.lineNumber}: {e}; throw err; } } @@ -1076,8 +1092,8 @@ lookup: } catch (e) { - let message = "Sourcing file: " + (e.echoerr || file.path + ": " + e); liberator.reportError(e); + let message = "Sourcing file: " + (e.echoerr || file.path + ": " + e); if (!silent) liberator.echoerr(message); } @@ -1105,7 +1121,7 @@ lookup: return this.withTempFiles(function (stdin, stdout, cmd) { if (input) - this.writeFile(stdin, input); + stdin.write(input); // TODO: implement 'shellredir' if (WINDOWS) @@ -1115,13 +1131,13 @@ lookup: } else { - this.writeFile(cmd, "cd " + escape(cwd.path) + "\n" + + cmd.write("cd " + escape(cwd.path) + "\n" + ["exec", ">" + escape(stdout.path), "2>&1", "<" + escape(stdin.path), escape(options["shell"]), options["shellcmdflag"], escape(command)].join(" ")); res = this.run("/bin/sh", ["-e", cmd.path], true); } - let output = self.readFile(stdout); + let output = stdout.read(); if (res > 0) output += "\nshell returned " + res; // if there is only one \n at the end, chop it off diff --git a/common/content/liberator.js b/common/content/liberator.js index 475d03d6..f4f4df7f 100644 --- a/common/content/liberator.js +++ b/common/content/liberator.js @@ -402,10 +402,10 @@ const liberator = (function () //{{{ "Install an extension", function (args) { - let file = io.getFile(args[0]); + let file = io.File(args[0]); if (file.exists() && file.isReadable() && file.isFile()) - services.get("extensionManager").installItemFromFile(file, "app-profile"); + services.get("extensionManager").installItemFromFile(file.file, "app-profile"); else { if (file.exists() && file.isDirectory()) @@ -1483,7 +1483,7 @@ const liberator = (function () //{{{ return void liberator.echoerr("E484: Can't open file " + dir.path); liberator.log("Sourcing plugin directory: " + dir.path + "...", 3); - io.readDirectory(dir.path, true).forEach(function (file) { + dir.readDirectory(true).forEach(function (file) { if (file.isFile() && /\.(js|vimp)$/i.test(file.path) && !(file.path in liberator.pluginFiles)) { try @@ -1879,7 +1879,7 @@ const liberator = (function () //{{{ { let filename = liberator.commandLineOptions.rcFile; if (!/^(NONE|NORC)$/.test(filename)) - io.source(io.getFile(filename).path, false); // let io.source handle any read failure like Vim + io.source(io.File(filename).path, false); // let io.source handle any read failure like Vim } else { diff --git a/common/content/services.js b/common/content/services.js index 47d0058e..9cc54026 100644 --- a/common/content/services.js +++ b/common/content/services.js @@ -114,6 +114,7 @@ function Services() self.add("xulAppInfo", "@mozilla.org/xre/app-info;1", Ci.nsIXULAppInfo); self.addClass("file", "@mozilla.org/file/local;1", Ci.nsILocalFile); + self.addClass("file:", "@mozilla.org/network/protocol;1?name=file", Ci.nsIFileProtocolHandler); self.addClass("find", "@mozilla.org/embedcomp/rangefind;1", Ci.nsIFind); self.addClass("process", "@mozilla.org/process/util;1", Ci.nsIProcess); diff --git a/common/content/style.js b/common/content/style.js index 1c09ab2c..a28afc2f 100644 --- a/common/content/style.js +++ b/common/content/style.js @@ -825,17 +825,13 @@ liberator.registerObserver("load_completion", function () { ]); completion.colorScheme = function colorScheme(context) { - let colors = []; - - io.getRuntimeDirectories("colors").forEach(function (dir) { - io.readDirectory(dir).forEach(function (file) { - if (/\.vimp$/.test(file.leafName) && !colors.some(function (c) c.leafName == file.leafName)) - colors.push(file); - }); - }); - context.title = ["Color Scheme", "Runtime Path"]; - context.completions = [[c.leafName.replace(/\.vimp$/, ""), c.parent.path] for ([, c] in Iterator(colors))] + context.keys = { text: function (f) f.leafName.replace(/\.vimp$/, ""), description: "parent.path" }; + context.completions = util.Array.flatten( + io.getRuntimeDirectories("colors").map( + function (dir) dir.readDirectory().filter( + function (file) /\.vimp$/.test(file.leafName) && !colors.some(function (c) c.leafName == file.leafName)))) + }; completion.highlightGroup = function highlightGroup(context) { diff --git a/common/content/util.js b/common/content/util.js index 07530b7c..f79a8905 100644 --- a/common/content/util.js +++ b/common/content/util.js @@ -688,9 +688,9 @@ const util = { //{{{ try { // Try to find a matching file. - let file = io.getFile(url); + let file = io.File(url); if (file.exists() && file.isReadable()) - return services.get("io").newFileURI(file).spec; + return services.get("io").newFileURI(file.wrappedNative).spec; } catch (e) {}