We can use a Freeze map to add persistence to the file system server, and we present C++ and Java implementations in this section. However, as we describe in
Section 39.3, a Freeze evictor is often a better choice for applications (such as the file system server) in which the persistent value is an Ice object.
2.
If no suitable key or value types are found, define new (possibly derived) types that capture your persistent state requirements. Consider placing these definitions in a separate file: these types are only used by the server for persistence, and therefore do not need to appear in the “public” definitions required by clients. Also consider placing your persistent types in a separate module to avoid name clashes.
Our goal is to implement the file system using Freeze maps for all persistent storage, including files and their contents. There are various options for how to implement the server. For this example, the server is stateless; whenever a client invokes an operation, the server accesses the database to satisfy the request. Implementing the server in this way has the advantage that it scales very well: we do not need a separate servant for each node; instead two default servants (see
Section 32.8), one for directories and one for files, are sufficient. This keeps the memory requirements of the server to a minimum and also allows us to rely on the database for transactions and locking. (This is a very common implementation technique for servers that act as a front end to a database: the server is a simple facade that implements each operation by accessing the database.)
Our first step is to select the Slice types we will use for the key and value types for our maps. For each file, we need to store the name of the file, its parent directory, and the contents of the file. For directories, we also store the name and parent directory, as well as a dictionary that keeps track of the subdirectories and files in that directory. This leads to Slice definitions (in file
FilesystemDB.ice) as follows:
#include <Filesystem.ice>
#include <Ice/Identity.ice>
module FilesystemDB {
struct FileEntry {
string name;
Ice::Identity parent;
Filesystem::Lines text;
};
dictionary<string, Filesystem::NodeDesc> StringNodeDescDict;
struct DirectoryEntry {
string name;
Ice::Identity parent;
StringNodeDescDict nodes;
};
};
Note that the definitions are placed into a separate module, so they do not affect the existing definitions of the non-persistent version of the application. For reference, here is the definition of
NodeDesc once more:
module Filesystem {
// ...
enum NodeType { DirType, FileType };
struct NodeDesc {
string name;
NodeType type;
Node* proxy;
};
// ...
};
To store the persistent state for the file system, we use two Freeze maps: one map for files and one map for directories. For files, we map the identity of the file to its corresponding
FileEntry structure and, similarly, for directories, we map the identity of the directory to its corresponding
DirectoryEntry structure.
When a request arrives from a client, the object identity is available in the server. The server uses the identity to retrieve the state of the target node for the request from the database and act on that state accordingly.
$ slice2freeze ‑I$(ICE_HOME)/slice ‑I. ‑‑ice ‑‑dict \
FilesystemDB::IdentityFileEntryMap,Ice::Identity,\
FilesystemDB::FileEntry \
IdentityFileEntryMap FilesystemDB.ice \
$(ICE_HOME)/slice/Ice/Identity.ice
$
slice2freeze ‑I$(ICE_HOME)/slice ‑I. ‑‑ice ‑‑dict \
FilesystemDB::IdentityDirectoryEntryMap,Ice::Identity,\
FilesystemDB::DirectoryEntry \
IdentityDirectoryEntryMap FilesystemDB.ice \
$(ICE_HOME)/slice/Ice/Identity.ice
The resulting map classes are named IdentityFileEntryMap and
IdentityDirectoryEntryMap.
The server’s main program is very simple:
#include <FilesystemI.h>
#include <IdentityFileEntryMap.h>
#include <IdentityDirectoryEntryMap.h>
#include <Ice/Application.h>
#include <Freeze/Freeze.h>
using namespace std;
using namespace Filesystem;
using namespace FilesystemDB;
class FilesystemApp : public virtual Ice::Application
{
public:
FilesystemApp(const string& envName)
: _envName(envName)
{
}
virtual int run(int, char*[])
{
shutdownOnInterrupt();
Ice::ObjectAdapterPtr adapter =
communicator()‑>createObjectAdapter("MapFilesystem");
const Freeze::ConnectionPtr connection(
Freeze::createConnection(communicator(), _envName));
const IdentityFileEntryMap fileDB(
connection, FileI::filesDB());
const IdentityDirectoryEntryMap dirDB(
connection,
DirectoryI::directoriesDB());
adapter‑>addDefaultServant(
new FileI(communicator(), _envName), "file");
adapter‑>addDefaultServant(
new DirectoryI(communicator(), _envName), "");
adapter‑>activate();
communicator()‑>waitForShutdown();
if(interrupted())
cerr << appName()
<< ": received signal, shutting down" << endl;
return 0;
}
private:
string _envName;
};
int
main(int argc, char* argv[])
{
FilesystemApp app("db");
return app.main(argc, argv, "config.server");
}
Let us examine the code in detail. First, we are now including IdentityFileEntry.h and
IdentityDirectoryEntry.h. These header files includes all of the other Freeze (and Ice) header files we need.
Next, we define the class FilesystemApp as a subclass of
Ice::Application, and provide a constructor taking a string argument:
FilesystemApp(const string& envName)
: _envName(envName) {}
The interesting part of run are the few lines of code that create the database connection and the two maps that store files and directories, plus the code to add the two default servants:
const Freeze::ConnectionPtr connection(
Freeze::createConnection(communicator(), _envName));
const IdentityFileEntryMap fileDB(
connection, FileI::filesDB());
const IdentityDirectoryEntryMap dirDB(
connection,
DirectoryI::directoriesDB());
adapter‑>addDefaultServant(
new FileI(communicator(), _envName), "file");
adapter‑>addDefaultServant(
new DirectoryI(communicator(), _envName), "");
run keeps the database connection open for the duration of the program for performance reasons. As we will see shortly, individual operation implementations will use their own connections; however, it is substantially cheaper to create second (and subsequent connections) than it is to create the first connection.
For the default servants, we use file as the category for files. For directories, we use the empty default category.
namespace Filesystem {
class FileI : public File {
public:
FileI(const Ice::CommunicatorPtr& communicator,
const std::string& envName);
// Slice operations...
static std::string filesDB();
private:
void halt(const Freeze::DatabaseException& ex) const;
const Ice::CommunicatorPtr _communicator;
const std::string _envName;
};
}
The FileI class stores the communicator and the environment name. These members are initialized by the constructor. The
filesDB static member function returns the name of the file map, and the
halt member function is used to stop the server if it encounters a catastrophic error.
The DirectoryI class looks very much the same, also storing the communicator and environment name. The
directoriesDB static member function returns the name of the directory map.
namespace Filesystem {
class DirectoryI : public Directory {
public:
DirectoryI(const Ice::CommunicatorPtr& communicator,
const std::string& envName);
// Slice operations...
static std::string directoriesDB();
private:
void halt(const Freeze::DatabaseException& ex) const;
const Ice::CommunicatorPtr _communicator;
const std::string _envName;
};
}
The FileI constructor and the
filesDB and
halt member functions have trivial implementations:
FileI::FileI(const Ice::CommunicatorPtr& communicator,
const string& envName)
: _communicator(communicator), _envName(envName)
{
}
string
FileI::filesDB()
{
return "files";
}
void
FileI::halt(const Freeze::DatabaseException& ex) const
{
Ice::Error error(_communicator‑>getLogger());
error << "fatal exception: " << ex
<< "\n*** Aborting application ***";
abort();
}
The Slice operations all follow the same implementation strategy: we create a database connection and the file map and place the body of the operation into an infinite loop:
string
FileI::someOperation(/* ... */ const Ice::Current& c)
{
const Freeze::ConnectionPtr connection(
Freeze::createConnection(_communicator, _envName));
IdentityFileEntryMap fileDB(connection, filesDB());
for (;;) {
try {
// Operation implementation here...
} catch (const Freeze::DeadlockException&) {
continue;
} catch (const Freeze::DatabaseException& ex) {
halt(ex);
}
}
}
Each operation creates its own database connection and map for concurrency reasons: the database takes care of all the necessary locking, so there is no need for any other synchronization in the server. If the database detects a deadlock, the code handles the corresponding
DeadlockException and simply tries again until the operation eventually succeeds; any other database exception indicates that something has gone seriously wrong and terminates the server.
string
FileI::name(const Ice::Current& c)
{
const Freeze::ConnectionPtr connection(
Freeze::createConnection(_communicator, _envName));
IdentityFileEntryMap fileDB(connection, filesDB());
for (;;) {
try {
IdentityFileEntryMap::iterator p = fileDB.find(c.id);
if (p == fileDB.end()) {
throw Ice::ObjectNotExistException(__FILE__,
__LINE__);
}
return p‑>second.name;
} catch (const Freeze::DeadlockException&) {
continue;
} catch (const Freeze::DatabaseException& ex) {
halt(ex);
}
}
}
The implementation could hardly be simpler: the default servant uses the identity in the
Current object to index into the file map. If a record with this identity exists, it returns the name of the file as stored in the
FileEntry structure in the map. Otherwise, if no such entry exists, it throws
ObjectNotExistException. This happens if the file existed at some time in the past but has since been destroyed.
The read implementation is almost identical. It returns the text that is stored by the
FileEntry:
Lines
FileI::read(const Ice::Current& c)
{
const Freeze::ConnectionPtr connection(
Freeze::createConnection(_communicator, _envName));
IdentityFileEntryMap fileDB(connection, filesDB());
for (;;) {
try {
IdentityFileEntryMap::iterator p = fileDB.find(c.id);
if (p == fileDB.end()) {
throw Ice::ObjectNotExistException(__FILE__,
__LINE__);
}
return p‑>second.text;
} catch (const Freeze::DeadlockException&) {
continue;
} catch (const Freeze::DatabaseException& ex) {
halt(ex);
}
}
}
The write implementation updates the file contents and calls
set on the iterator to update the map with the new contents:
void
FileI::write(const Filesystem::Lines& text, const Ice::Current& c)
{
const Freeze::ConnectionPtr connection(
Freeze::createConnection(_communicator, _envName));
IdentityFileEntryMap fileDB(connection, filesDB());
for (;;) {
try {
IdentityFileEntryMap::iterator p = fileDB.find(c.id);
if (p == fileDB.end()) {
throw Ice::ObjectNotExistException(__FILE__,
__LINE__);
}
FileEntry entry = p‑>second;
entry.text = text;
p.set(entry);
break;
} catch (const Freeze::DeadlockException&) {
continue;
} catch (const Freeze::DatabaseException& ex) {
halt(ex);
}
}
}
Finally, the destroy implementation for files must update two maps: it needs to remove its own entry in the file map as well as update the
nodes map in the parent to remove itself from the parent’s map of children. This raises a potential problem: if one update succeeds but the other one fails, we end up with an inconsistent file system: either the parent still has an entry to a non-existent file, or the parent lacks an entry to a file that still exists.
void
FileI::destroy(const Ice::Current& c)
{
const Freeze::ConnectionPtr connection(
Freeze::createConnection(_communicator, _envName));
IdentityFileEntryMap fileDB(connection, filesDB());
IdentityDirectoryEntryMap dirDB(connection,
DirectoryI::directoriesDB());
for (;;) {
try {
Freeze::TransactionHolder txn(connection);
IdentityFileEntryMap::iterator p = fileDB.find(c.id);
if (p == fileDB.end()) {
throw Ice::ObjectNotExistException(__FILE__,
__LINE__);
}
FileEntry entry = p‑>second;
IdentityDirectoryEntryMap::iterator pp =
dirDB.find(entry.parent);
if (pp == dirDB.end()) {
halt(Freeze::DatabaseException(
__FILE__, __LINE__,
"consistency error: file without parent"));
}
DirectoryEntry dirEntry = pp‑>second;
dirEntry.nodes.erase(entry.name);
pp.set(dirEntry);
fileDB.erase(p);
txn.commit();
break;
} catch (const Freeze::DeadlockException&) {
continue;
} catch (const Freeze::DatabaseException& ex) {
halt(ex);
}
}
}
As you can see, the code first establishes a transaction and then locates the file in the parent directory’s map of nodes. After removing the file from the parent, the code updates the parent’s persistent state by calling
set on the parent iterator and then removes the file from the file map before committing the transaction.
The DirectoryI::directoriesDB implementation returns the string
directories, and the
halt implementation is the same as for
FileI, so we do not show them here.
This means that the root directory (which must always exist) may or may not be present in the database. Accordingly, the constructor looks for the root directory (with the fixed identity
RootDir); if the root directory does not exist in the database, it creates it:
DirectoryI::DirectoryI(const Ice::CommunicatorPtr& communicator,
const string& envName)
: _communicator(communicator), _envName(envName)
{
const Freeze::ConnectionPtr connection =
Freeze::createConnection(_communicator, _envName);
IdentityDirectoryEntryMap dirDB(connection, directoriesDB());
for (;;) {
try {
Ice::Identity rootId;
rootId.name = "RootDir";
IdentityDirectoryEntryMap::const_iterator p =
dirDB.find(rootId);
if (p == dirDB.end()) {
DirectoryEntry d;
d.name = "/";
dirDB.put(make_pair(rootId, d));
}
break;
} catch (const Freeze::DeadlockException&) {
continue;
} catch (const Freeze::DatabaseException& ex) {
halt(ex);
}
}
}
Next, let us examine the implementation of createDirectory. Similar to the
FileI::destroy operation,
createDirectory must update both the parent’s nodes map and create a new entry in the directory map. These updates must happen atomically, so we perform them in a separate transaction:
DirectoryPrx
DirectoryI::createDirectory(const string& name,
const Ice::Current& c)
{
const Freeze::ConnectionPtr connection(
Freeze::createConnection(_communicator, _envName));
IdentityDirectoryEntryMap directoryDB(connection,
directoriesDB());
for (;;) {
try {
Freeze::TransactionHolder txn(connection);
IdentityDirectoryEntryMap::iterator p =
directoryDB.find(c.id);
if (p == directoryDB.end()) {
throw Ice::ObjectNotExistException(__FILE__,
__LINE__);
}
DirectoryEntry entry = p‑>second;
if (name.empty()
|| entry.nodes.find(name) != entry.nodes.end()) {
throw NameInUse(name);
}
DirectoryEntry d;
d.name = name;
d.parent = c.id;
Ice::Identity id;
id.name = IceUtil::generateUUID();
DirectoryPrx proxy = DirectoryPrx::uncheckedCast(
c.adapter‑>createProxy(id));
NodeDesc nd;
nd.name = name;
nd.type = DirType;
nd.proxy = proxy;
entry.nodes.insert(make_pair(name, nd));
p.set(entry);
directoryDB.put(make_pair(id, d));
txn.commit();
return proxy;
} catch (const Freeze::DeadlockException&) {
continue;
} catch (const Freeze::DatabaseException& ex) {
halt(ex);
}
}
}
After establishing the transaction, the code ensures that the directory does not already contain an entry with the same name and then initializes a new
DirectoryEntry, setting the name to the name of the new directory, and the parent to its own identity. The identity of the new directory is a UUID, which ensures that all directories have unique identities. In addition, the UUID prevents the “accidental rebirth” of a file or directory in the future (see
Section 34.8).
The code then initializes a new NodeDesc structure with the details of the new directory and, finally, updates its own map of children as well as adding the new directory to the map of directories before committing the transaction.
The createFile implementation is almost identical, so we do not show it here. Similarly, the
name and
destroy implementations are almost identical to the ones for
FileI, so let us move to
list:
NodeDescSeq
DirectoryI::list(const Ice::Current& c)
{
const Freeze::ConnectionPtr connection(
Freeze::createConnection(_communicator, _envName));
IdentityDirectoryEntryMap directoryDB(connection,
directoriesDB());
for (;;) {
try {
IdentityDirectoryEntryMap::iterator p =
directoryDB.find(c.id);
if (p == directoryDB.end()) {
throw Ice::ObjectNotExistException(__FILE__,
__LINE__);
}
NodeDescSeq result;
for (StringNodeDescDict::const_iterator q =
p‑>second.nodes.begin();
q != p‑>second.nodes.end();
++q) {
result.push_back(q‑>second);
}
return result;
} catch (const Freeze::DeadlockException&) {
continue;
} catch (const Freeze::DatabaseException& ex) {
halt(ex);
}
}
}
Again, the code is very simple: it iterates over the nodes map, adding each
NodeDesc structure to the returned sequence.
The find implementation is even simpler, so we do not show it here.
$ slice2freezej ‑I$(ICE_HOME)/slice ‑I. ‑‑ice ‑‑dict \
FilesystemDB.IdentityFileEntryMap,Ice.Identity,\
FilesystemDB.FileEntry \
IdentityFileEntryMap FilesystemDB.ice \
$(ICE_HOME)/slice/Ice/Identity.ice
$
slice2freezej ‑I$(ICE_HOME)/slice ‑I. ‑‑ice ‑‑dict \
FilesystemDB.IdentityDirectoryEntryMap,Ice.Identity,\
FilesystemDB.DirectoryEntry \
IdentityDirectoryEntryMap FilesystemDB.ice \
$(ICE_HOME)/slice/Ice/Identity.ice
The resulting map classes are named IdentityFileEntryMap and
IdentityDirectoryEntryMap.
import Filesystem.*;
import FilesystemDB.*;
public class Server extends Ice.Application
{
public
Server(String envName)
{
_envName = envName;
}
public int
run(String[] args)
{
Ice.ObjectAdapter adapter =
communicator().createObjectAdapter("MapFilesystem");
Freeze.Connection connection = null;
try {
connection =
Freeze.Util.createConnection(communicator(),
_envName);
IdentityFileEntryMap fileDB =
new IdentityFileEntryMap(
connection, FileI.filesDB(), true);
IdentityDirectoryEntryMap dirDB =
new IdentityDirectoryEntryMap(
connection, DirectoryI.directoriesDB(), true);
adapter.addDefaultServant(
new FileI(communicator(), _envName), "file");
adapter.addDefaultServant(
new DirectoryI(communicator(), _envName), "");
adapter.activate();
communicator().waitForShutdown();
} finally {
connection.close();
}
return 0;
}
public static void
main(String[] args)
{
Server app = new Server("db");
app.main("MapServer", args, "config.server");
System.exit(0);
}
private String _envName;
}
First, we import the Filesystem and
FilesystemDB packages.
Next, we define the class FilesystemApp as a subclass of
Ice.Application, and provide a constructor taking a string argument:
FilesystemApp(const string& envName)
: _envName(envName) {}
The interesting part of run are the few lines of code that create the database connection and the two maps that store files and directories, plus the code to add the two default servants:
connection =
Freeze.Util.createConnection(communicator(),
_envName);
IdentityFileEntryMap fileDB =
new IdentityFileEntryMap(
connection, FileI.filesDB(), true);
IdentityDirectoryEntryMap dirDB =
new IdentityDirectoryEntryMap(
connection, DirectoryI.directoriesDB(), true);
adapter.addDefaultServant(
new FileI(communicator(), _envName), "file");
adapter.addDefaultServant(
new DirectoryI(communicator(), _envName), "");
run keeps the database connection open for the duration of the program for performance reasons. As we will see shortly, individual operation implementations will use their own connections; however, it is substantially cheaper to create second (and subsequent connections) than it is to create the first connection.
For the default servants, we use file as the category for files. For directories, we use the empty default category.
public class FileI extends _FileDisp
{
public
FileI(Ice.Communicator communicator,
String envName)
{
_communicator = communicator;
_envName = envName;
}
// Slice operations...
public static String
filesDB()
{
return "files";
}
private void
halt(Freeze.DatabaseException e)
{
java.io.StringWriter sw = new java.io.StringWriter();
java.io.PrintWriter pw = new java.io.PrintWriter(sw);
e.printStackTrace(pw);
pw.flush();
_communicator.getLogger().error(
"fatal database error\n" + sw.toString() +
"\n*** Halting JVM ***");
Runtime.getRuntime().halt(1);
}
private Ice.Communicator _communicator;
private String _envName;
}
The FileI class stores the communicator and the environment name. These members are initialized by the constructor. The
filesDB static method returns the name of the file map, and the
halt member function is used to stop the server if it encounters a catastrophic error.
The Slice operations all follow the same implementation strategy: we create a database connection and the file map and place the body of the operation into an infinite loop:
public String
someOperation(/* ... */ Ice.Current c)
{
Freeze.Connection connection =
Freeze.Util.createConnection(_communicator, _envName);
try {
IdentityFileEntryMap fileDB =
new IdentityFileEntryMap(connection, filesDB());
for (;;) {
try {
// Operation implementation here...
} catch (Freeze.DeadlockException ex) {
continue;
} catch (Freeze.DatabaseException ex) {
halt(ex);
}
}
} finally {
connection.close();
}
}
Each operation creates its own database connection and map for concurrency reasons: the database takes care of all the necessary locking, so there is no need for any other synchronization in the server. If the database detects a deadlock, the code handles the corresponding
DeadlockException and simply tries again until the operation eventually succeeds; any other database exception indicates that something has gone seriously wrong and terminates the server.
public String
name(Ice.Current c)
{
Freeze.Connection connection =
Freeze.Util.createConnection(_communicator, _envName);
try
{
IdentityFileEntryMap fileDB =
new IdentityFileEntryMap(connection, filesDB());
for (;;) {
try {
FileEntry entry = fileDB.get(c.id);
if (entry == null) {
throw new Ice.ObjectNotExistException();
}
return entry.name;
} catch (Freeze.DeadlockException ex) {
continue;
} catch (Freeze.DatabaseException ex) {
halt(ex);
}
}
} finally {
connection.close();
}
}
The implementation could hardly be simpler: the default servant uses the identity in the
Current object to index into the file map. If a record with this identity exists, it returns the name of the file as stored in the
FileEntry structure in the map. Otherwise, if no such entry exists, it throws
ObjectNotExistException. This happens if the file existed at some time in the past but has since been destroyed.
The read implementation is almost identical. It returns the text that is stored by the
FileEntry:
public String[]
read(Ice.Current c)
{
Freeze.Connection connection =
Freeze.Util.createConnection(_communicator, _envName);
try {
IdentityFileEntryMap fileDB = new
IdentityFileEntryMap(connection, filesDB());
for (;;) {
try {
FileEntry entry = fileDB.get(c.id);
if (entry == null) {
throw new Ice.ObjectNotExistException();
}
return entry.text;
} catch (Freeze.DeadlockException ex) {
continue;
} catch (Freeze.DatabaseException ex) {
halt(ex);
}
}
} finally {
connection.close();
}
}
The write implementation updates the file contents and calls
put on the iterator to update the map with the new contents:
public void
write(String[] text, Ice.Current c)
throws GenericError
{
Freeze.Connection connection =
Freeze.Util.createConnection(_communicator, _envName);
try {
IdentityFileEntryMap fileDB =
new IdentityFileEntryMap(connection, filesDB());
for (;;) {
try {
FileEntry entry = fileDB.get(c.id);
if (entry == null) {
throw new Ice.ObjectNotExistException();
}
entry.text = text;
fileDB.put(c.id, entry);
break;
} catch (Freeze.DeadlockException ex) {
continue;
} catch (Freeze.DatabaseException ex) {
halt(ex);
}
}
} finally {
connection.close();
}
}
Finally, the destroy implementation for files must update two maps: it needs to remove its own entry in the file map as well as update the
nodes map in the parent to remove itself from the parent’s map of children. This raises a potential problem: if one update succeeds but the other one fails, we end up with an inconsistent file system: either the parent still has an entry to a non-existent file, or the parent lacks an entry to a file that still exists.
public void
destroy(Ice.Current c)
throws PermissionDenied
{
Freeze.Connection connection =
Freeze.Util.createConnection(_communicator, _envName);
try {
IdentityFileEntryMap fileDB =
new IdentityFileEntryMap(connection, filesDB());
IdentityDirectoryEntryMap dirDB =
new IdentityDirectoryEntryMap(
connection, DirectoryI.directoriesDB());
for (;;) {
Freeze.Transaction txn = null;
try {
txn = connection.beginTransaction();
FileEntry entry = fileDB.get(c.id);
if (entry == null) {
throw new Ice.ObjectNotExistException();
}
DirectoryEntry dirEntry =
(DirectoryEntry)dirDB.get(entry.parent);
if (dirEntry == null) {
halt(new Freeze.DatabaseException(
"consistency error: " +
"file without parent"));
}
dirEntry.nodes.remove(entry.name);
dirDB.put(entry.parent, dirEntry);
fileDB.remove(c.id);
txn.commit();
txn = null;
break;
} catch (Freeze.DeadlockException ex) {
continue;
} catch (Freeze.DatabaseException ex) {
halt(ex);
} finally {
if (txn != null) {
txn.rollback();
}
}
}
}
finally
{
connection.close();
}
}
As you can see, the code first establishes a transaction and then locates the file in the parent directory’s map of nodes. After removing the file from the parent, the code updates the parent’s persistent state by calling
put on the parent iterator and then removes the file from the file map before committing the transaction.
The DirectoryI.directoriesDB implementation returns the string
directories, and the
halt implementation is the same as for
FileI, so we do not show them here.
This means that the root directory (which must always exist) may or may not be present in the database. Accordingly, the constructor looks for the root directory (with the fixed identity
RootDir); if the root directory does not exist in the database, it creates it:
public
DirectoryI(Ice.Communicator communicator, String envName)
{
_communicator = communicator;
_envName = envName;
Freeze.Connection connection =
Freeze.Util.createConnection(_communicator, _envName);
try {
IdentityDirectoryEntryMap dirDB =
new IdentityDirectoryEntryMap(connection,
directoriesDB());
for (;;) {
try {
Ice.Identity rootId =
new Ice.Identity("RootDir", "");
DirectoryEntry entry = dirDB.get(rootId);
if (entry == null) {
dirDB.put(rootId,
new DirectoryEntry("/",
new Ice.Identity("", ""), null));
}
break;
} catch (Freeze.DeadlockException ex) {
continue;
} catch (Freeze.DatabaseException ex) {
halt(ex);
}
}
} finally {
connection.close();
}
}
Next, let us examine the implementation of createDirectory. Similar to the
FileI::destroy operation,
createDirectory must update both the parent’s nodes map and create a new entry in the directory map. These updates must happen atomically, so we perform them in a separate transaction:
public DirectoryPrx
createDirectory(String name, Ice.Current c)
throws NameInUse
{
Freeze.Connection connection =
Freeze.Util.createConnection(_communicator, _envName);
try {
IdentityDirectoryEntryMap dirDB =
new IdentityDirectoryEntryMap(connection,
directoriesDB());
for (;;) {
Freeze.Transaction txn = null;
try {
txn = connection.beginTransaction();
DirectoryEntry entry = dirDB.get(c.id);
if (entry == null) {
throw new Ice.ObjectNotExistException();
}
if(name.length() == 0
|| entry.nodes.get(name) != null) {
throw new NameInUse(name);
}
DirectoryEntry newEntry =
new DirectoryEntry(name, c.id, null);
Ice.Identity id = new Ice.Identity(
java.util.UUID.randomUUID().toString(),
"");
DirectoryPrx proxy =
DirectoryPrxHelper.uncheckedCast(
c.adapter.createProxy(id));
entry.nodes.put(name,
new NodeDesc(name,
NodeType.DirType,
proxy));
dirDB.put(c.id, entry);
dirDB.put(id, newEntry);
txn.commit();
txn = null;
return proxy;
} catch (Freeze.DeadlockException ex) {
continue;
} catch (Freeze.DatabaseException ex) {
halt(ex);
} finally {
if(txn != null) {
txn.rollback();
}
}
}
} finally {
connection.close();
}
}
After establishing the transaction, the code ensures that the directory does not already contain an entry with the same name and then initializes a new
DirectoryEntry, setting the name to the name of the new directory, and the parent to its own identity. The identity of the new directory is a UUID, which ensures that all directories have unique identities. In addition, the UUID prevents the “accidental rebirth” of a file or directory in the future (see
Section 34.8).
The code then initializes a new NodeDesc structure with the details of the new directory and, finally, updates its own map of children as well as adding the new directory to the map of directories before committing the transaction.
The createFile implementation is almost identical, so we do not show it here. Similarly, the
name and
destroy implementations are almost identical to the ones for
FileI, so let us move to
list:
public NodeDesc[]
list(Ice.Current c)
{
Freeze.Connection connection =
Freeze.Util.createConnection(_communicator, _envName);
try {
IdentityDirectoryEntryMap dirDB =
new IdentityDirectoryEntryMap(connection,
directoriesDB());
for (;;) {
try {
DirectoryEntry entry = dirDB.get(c.id);
if (entry == null) {
throw new Ice.ObjectNotExistException();
}
NodeDesc[] result =
new NodeDesc[entry.nodes.size()];
java.util.Iterator<NodeDesc> p =
entry.nodes.values().iterator();
for (int i = 0; i < entry.nodes.size(); ++i) {
result[i] = p.next();
}
return result;
} catch (Freeze.DeadlockException ex) {
continue;
} catch (Freeze.DatabaseException ex) {
halt(ex);
}
}
} finally {
connection.close();
}
}
Again, the code is very simple: it iterates over the nodes map, adding each
NodeDesc structure to the returned sequence.
The find implementation is even simpler, so we do not show it here.