Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to preserve text form (formatting, etc.) of registry data. #84

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions core/src/main/scala/org/labrad/Proxies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,14 @@ trait RegistryServer extends Requester {
case None => call("get", Str(key), Str(pat))
}
}
def getAsString(key: String, pat: String = "?", default: Option[(Boolean, Data)] = None): Future[(Data, String)] = {
default match {
case Some((set, default)) => call[(Data, String)]("get as string", Str(key), Str(pat), Bool(set), default)
case None => call[(Data, String)]("get as string", Str(key), Str(pat))
}
}
def set(key: String, value: Data): Future[Unit] = callUnit("set", Str(key), value)
def setAsString(key: String, text: String): Future[Unit] = callUnit("set as string", Str(key), Str(text))
def del(key: String): Future[Unit] = callUnit("del", Str(key))

def notifyOnChange(id: Long, enable: Boolean): Future[Unit] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class MultiheadServer(name: String, registry: RegistryStore, server: LocalServer
dir = registry.child(dir, name, create = true)
dir = registry.child(dir, "Multihead", create = true)
val default = DataBuilder("*(sws)").array(0).result()
val result = registry.getValue(dir, "Managers", default = Some((true, default)))
val (result, textOpt) = registry.getValue(dir, "Managers", default = Some((true, default)))
val configs = result.get[Seq[(String, Long, String)]].map {
case (host, port, pw) =>
externalConfig.copy(
Expand Down
6 changes: 3 additions & 3 deletions manager/src/main/scala/org/labrad/registry/DelphiStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ class DelphiFileStore(rootDir: File) extends FileStore(rootDir) {
/**
* Encode and decode data for storage in individual key files.
*/
override def encodeData(data: Data): Array[Byte] = {
override def encodeData(data: Data, textOpt: Option[String]): Array[Byte] = {
DelphiFormat.dataToString(data).getBytes(UTF_8)
}

override def decodeData(bytes: Array[Byte]): Data = {
DelphiFormat.stringToData(new String(bytes, UTF_8))
override def decodeData(bytes: Array[Byte]): (Data, Option[String]) = {
(DelphiFormat.stringToData(new String(bytes, UTF_8)), None)
}
}

Expand Down
38 changes: 26 additions & 12 deletions manager/src/main/scala/org/labrad/registry/FileStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ abstract class FileStore(rootDir: File) extends RegistryStore {
/**
* Encode and decode data for storage in individual key files.
*/
def encodeData(data: Data): Array[Byte]
def decodeData(bytes: Array[Byte]): Data
def encodeData(data: Data, textOpt: Option[String]): Array[Byte]
def decodeData(bytes: Array[Byte]): (Data, Option[String])

/**
* Convert the given directory into a registry path.
Expand Down Expand Up @@ -82,7 +82,7 @@ abstract class FileStore(rootDir: File) extends RegistryStore {
if (!ok) sys.error(s"failed to remove directory: $name")
}

def getValue(dir: File, key: String, default: Option[(Boolean, Data)]): Data = {
def getValue(dir: File, key: String, default: Option[(Boolean, Data)]): (Data, Option[String]) = {
val path = keyFile(dir, key)
if (path.exists) {
val bytes = readFile(path)
Expand All @@ -91,15 +91,15 @@ abstract class FileStore(rootDir: File) extends RegistryStore {
default match {
case None => sys.error(s"key does not exist: $key")
case Some((set, default)) =>
if (set) setValue(dir, key, default)
default
if (set) setValue(dir, key, default, None)
(default, None)
}
}
}

def setValueImpl(dir: File, key: String, value: Data): Unit = {
def setValueImpl(dir: File, key: String, value: Data, textOpt: Option[String]): Unit = {
val path = keyFile(dir, key)
val bytes = encodeData(value)
val bytes = encodeData(value, textOpt)
writeFile(path, bytes)
}

Expand Down Expand Up @@ -166,12 +166,26 @@ class BinaryFileStore(rootDir: File) extends FileStore(rootDir) {
/**
* Encode and decode data for storage in individual key files.
*/
override def encodeData(data: Data): Array[Byte] = {
Cluster(Str(data.t.toString), Bytes(data.toBytes)).toBytes
override def encodeData(data: Data, textOpt: Option[String]): Array[Byte] = {
val b = DataBuilder()
b.clusterStart()
b.string(data.t.toString)
b.bytes(data.toBytes)
for (text <- textOpt) {
b.string(text)
}
b.clusterEnd()
b.result.toBytes
}

override def decodeData(bytes: Array[Byte]): Data = {
val (typ, data) = Data.fromBytes(Type("sy"), bytes).get[(String, Array[Byte])]
Data.fromBytes(Type(typ), data)
override def decodeData(bytes: Array[Byte]): (Data, Option[String]) = {
try {
val (typ, data, text) = Data.fromBytes(Type("sys"), bytes).get[(String, Array[Byte], String)]
(Data.fromBytes(Type(typ), data), Some(text))
} catch {
case _: Exception =>
val (typ, data) = Data.fromBytes(Type("sy"), bytes).get[(String, Array[Byte])]
(Data.fromBytes(Type(typ), data), None)
}
}
}
6 changes: 3 additions & 3 deletions manager/src/main/scala/org/labrad/registry/Migrate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ object Migrate {
}

/**
* SQLite registry format on disk (source or sink).
* Registry on local disk (source or sink).
*/
class LocalRegistry(store: RegistryStore) extends Registry {
private def find(path: Seq[String], create: Boolean = false): store.Dir = {
Expand All @@ -219,7 +219,7 @@ object Migrate {

val values = Map.newBuilder[String, Data]
for (key <- keys) {
val data = store.getValue(loc, key, default = None)
val (data, textOpt) = store.getValue(loc, key, default = None)
values += key -> data
}
(dirs, values.result)
Expand All @@ -228,7 +228,7 @@ object Migrate {
def set(path: Seq[String], keys: Map[String, Data]): Unit = {
val loc = find(path, create = true)
for ((key, value) <- keys) {
store.setValue(loc, key, value)
store.setValue(loc, key, value, None)
}
}
}
Expand Down
43 changes: 37 additions & 6 deletions manager/src/main/scala/org/labrad/registry/Registry.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ trait RegistryStore {
}
def rmDirImpl(dir: Dir, name: String): Unit

def getValue(dir: Dir, key: String, default: Option[(Boolean, Data)]): Data
def getValue(dir: Dir, key: String, default: Option[(Boolean, Data)]): (Data, Option[String])

def setValue(dir: Dir, key: String, value: Data): Unit = {
setValueImpl(dir, key, value)
def setValue(dir: Dir, key: String, value: Data, textOpt: Option[String]): Unit = {
setValueImpl(dir, key, value, textOpt)
notifyListener(dir, key, isDir=false, addOrChange=true)
}
def setValueImpl(dir: Dir, key: String, value: Data): Unit
def setValueImpl(dir: Dir, key: String, value: Data, textOpt: Option[String]): Unit

def delete(dir: Dir, key: String): Unit = {
deleteImpl(dir, key)
Expand Down Expand Up @@ -235,17 +235,48 @@ extends LocalServer with Logging {
def getValue(key: String, pat: String, set: Boolean, default: Data): Data = _getValue(key, pat, Some((set, default)))

def _getValue(key: String, pat: String = "?", default: Option[(Boolean, Data)] = None): Data = {
val data = store.getValue(curDir, key, default)
val (data, textOpt) = store.getValue(curDir, key, default)
val pattern = Pattern(pat)
data.convertTo(pattern)
}

@Setting(id=21,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this setting needs some docs explaining how it works. In our discussion yesterday it took a while before it came out that the string can only be stored if it can be parsed to LabRAD data. This might seem obvious, but it's an important feature. It's probably also worth explicitly noting that the point of this setting is to use the user as a pretty-printer and that really only editors (i.e. scalabrad-web) should use it.

name="get as string",
doc="""Get the content of the given key in the current directory as a string.""")
def getAsString(key: String): (Data, String) = _getAsString(key)
def getAsString(key: String, pat: String): (Data, String) = _getAsString(key, pat)
def getAsString(key: String, set: Boolean, default: Data): (Data, String) = _getAsString(key, default = Some((set, default)))
def getAsString(key: String, pat: String, set: Boolean, default: Data): (Data, String) = _getAsString(key, pat, Some((set, default)))

def _getAsString(key: String, pat: String = "?", default: Option[(Boolean, Data)] = None): (Data, String) = {
val (data, textOpt) = store.getValue(curDir, key, default)
val pattern = Pattern(pat)
val converted = data.convertTo(pattern)
val text = textOpt match {
case None =>
converted.toString

case Some(text) =>
if (converted == data) text else converted.toString
}
(converted, text)
}

@Setting(id = 30,
name = "set",
doc = """Set the given key in the current directory to the given value.""")
def setValue(key: String, value: Data): Unit = {
require(key.nonEmpty, "Cannot create a key with empty name")
store.setValue(curDir, key, value)
store.setValue(curDir, key, value, None)
}

@Setting(id=31,
name="set as string",
doc="""Set the content of the given key in the current directory to the given data in text form.""")
def setAsString(key: String, text: String): Unit = {
require(key.nonEmpty, "Cannot create a key with empty name")
val value = Data.parse(text)
store.setValue(curDir, key, value, Some(text))
}

@Setting(id = 40,
Expand Down
43 changes: 35 additions & 8 deletions manager/src/main/scala/org/labrad/registry/RemoteStore.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.labrad.registry

import org.labrad.{Client, Credential, RegistryServerPacket, RegistryServerProxy, TlsMode}
import org.labrad.{Client, Credential, ManagerServerProxy, RegistryServerPacket,
RegistryServerProxy, TlsMode}
import org.labrad.data.{Data, Message}
import org.labrad.types.Type
import scala.concurrent.{Await, Future}
Expand All @@ -26,7 +27,9 @@ extends RegistryStore {
val root = Seq("")

private val lock = new Object
private var cxn: Client = null
private var reg: RegistryServerProxy = null
private var hasGetAsString: Boolean = false

/**
* Connect to remote registry if we are not currently connected,
Expand Down Expand Up @@ -56,7 +59,11 @@ extends RegistryStore {
cxn.connect()
val registry = new RegistryServerProxy(cxn)
Await.result(registry.streamChanges(id, true), 5.seconds)
reg = registry
val manager = new ManagerServerProxy(cxn)
val registrySettings = Await.result(manager.settings(Registry.NAME), 5.seconds)
this.cxn = cxn
this.reg = registry
this.hasGetAsString = registrySettings.map(_._2).toSet.contains("get as string")
}
}
}
Expand Down Expand Up @@ -99,12 +106,32 @@ extends RegistryStore {
call(dir) { _.rmDir(name) }
}

def getValue(dir: Dir, key: String, default: Option[(Boolean, Data)]): Data = {
call(dir) { _.get(key, default = default) }
def getValue(dir: Dir, key: String, default: Option[(Boolean, Data)]): (Data, Option[String]) = {
call(dir) { p =>
// We need an execution context to map over futures; use our client's execution context.
implicit val executionContext = cxn.executionContext
if (hasGetAsString) {
p.getAsString(key, default = default).map { case (data, text) => (data, Some(text)) }
} else {
p.get(key, default = default).map { case data => (data, None) }
}
}
}

def setValueImpl(dir: Dir, key: String, value: Data): Unit = {
call(dir) { _.set(key, value) }
def setValueImpl(dir: Dir, key: String, value: Data, textOpt: Option[String]): Unit = {
textOpt match {
case Some(text) =>
call(dir) { p =>
if (hasGetAsString) {
p.setAsString(key, text)
} else {
p.set(key, value)
}
}

case None =>
call(dir) { _.set(key, value) }
}
}

def deleteImpl(dir: Dir, key: String): Unit = {
Expand All @@ -125,8 +152,8 @@ extends RegistryStore {
rmDirImpl(dir, name)
}

override def setValue(dir: Dir, key: String, value: Data): Unit = {
setValueImpl(dir, key, value)
override def setValue(dir: Dir, key: String, value: Data, textOpt: Option[String]): Unit = {
setValueImpl(dir, key, value, textOpt)
}

override def delete(dir: Dir, key: String): Unit = {
Expand Down
36 changes: 26 additions & 10 deletions manager/src/main/scala/org/labrad/registry/SQLiteStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ object SQLiteStore {
)
""".execute()

// add data_str column if needed
val keyColumns = SQL"""PRAGMA table_info(keys)""".as(str("name").*)
if (!keyColumns.contains("data_str")) {
SQL"""
ALTER TABLE keys ADD COLUMN data_str TEXT
""".execute()
}

SQL"""
CREATE INDEX IF NOT EXISTS idx_list_keys ON keys(dir_id)
""".execute()
Expand Down Expand Up @@ -129,35 +137,43 @@ class SQLiteStore(cxn: Connection) extends RegistryStore {
SQL" DELETE FROM dirs WHERE parent_id = ${dir.id} AND name = $name ".execute()
}

def getValue(dir: SqlDir, key: String, default: Option[(Boolean, Data)]): Data = {
def getValue(dir: SqlDir, key: String, default: Option[(Boolean, Data)]): (Data, Option[String]) = {
// SQLite return NULL for empty BLOBs, so we have to treat the data
// column as nullable, even though we specified it as NOT NULL
val tdOpt = SQL"""
SELECT type, data FROM keys WHERE dir_id = ${dir.id} AND name = $key
""".as((str("type") ~ byteArray("data").?).singleOpt)
SELECT type, data, data_str FROM keys WHERE dir_id = ${dir.id} AND name = $key
""".as((str("type") ~ byteArray("data").? ~ str("data_str").?).singleOpt)

tdOpt match {
case Some(typ ~ data) =>
Data.fromBytes(Type(typ), data.getOrElse(Array.empty))
case Some(typ ~ bytes ~ textOpt) =>
val data = Data.fromBytes(Type(typ), bytes.getOrElse(Array.empty))
(data, textOpt)

case None =>
default match {
case None => sys.error(s"key does not exist: $key")
case Some((set, default)) =>
if (set) setValue(dir, key, default)
default
if (set) setValue(dir, key, default, None)
(default, None)
}
}
}

def setValueImpl(dir: SqlDir, key: String, value: Data): Unit = {
def setValueImpl(dir: SqlDir, key: String, value: Data, textOpt: Option[String]): Unit = {
val typ = value.t.toString
val data = value.toBytes

val n = SQL" UPDATE keys SET type = $typ, data = $data WHERE dir_id = ${dir.id} AND name = $key ".executeUpdate()
val n = SQL"""
UPDATE keys
SET type = $typ, data = $data, data_str = $textOpt
WHERE dir_id = ${dir.id} AND name = $key
""".executeUpdate()

if (n == 0) {
SQL" INSERT INTO keys(dir_id, name, type, data) VALUES (${dir.id}, $key, $typ, $data) ".executeInsert()
SQL"""
INSERT INTO keys(dir_id, name, type, data, data_str)
VALUES (${dir.id}, $key, $typ, $data, $textOpt)
""".executeInsert()
}
}

Expand Down
Loading