Example of a File System Server in Java
This page presents the source code for a Java server that implements our file system and communicates with the client we wrote earlier. The code is fully functional, apart from the required interlocking for threads.
Note that the server is remarkably free of code that relates to distribution: most of the server code is simply application logic that would be present just the same for a non-distributed version. Again, this is one of the major advantages of Ice: distribution concerns are kept away from application code so that you can concentrate on developing application logic instead of networking infrastructure.
On this page:
Implementing a File System Server in Java
We have now seen enough of the server-side Java mapping to implement a server for our file system. (You may find it useful to review these Slice definitions before studying the source code.)
Our server is composed of three source files:
Server.java
This file contains the server main program.NodeI.java
This file contains the implementation for theNode
servants.
DirectoryI.java
This file contains the implementation for theDirectory
servants.
FileI.java
This file contains the implementation for theFile
servants.
Server Main Program in Java
Our server main program, in the file Server.java
, consists of two static methods, main
and run
. main
creates and destroys an Ice communicator, and run
uses this communicator instantiate our file system objects:
import com.zeroc.demos.Manual.simpleFilesystem.Filesystem.*; public class Server { public static void main(String[] args) { int status = 0; java.util.List<String> extraArgs = new java.util.ArrayList<String>(); // // Try with resources block - communicator is automatically destroyed // at the end of this try block // try(com.zeroc.Ice.Communicator communicator = com.zeroc.Ice.Util.initialize(args, extraArgs)) { communicator.getProperties().setProperty("Ice.Default.Package", "com.zeroc.demos.Manual.simpleFilesystem"); // // Install shutdown hook to (also) destroy communicator during JVM shutdown. // This ensures the communicator gets destroyed when the user interrupts the application with Ctrl-C. // Runtime.getRuntime().addShutdownHook(new Thread(() -> communicator.destroy())); if(!extraArgs.isEmpty()) { System.err.println("too many arguments"); status = 1; } else { status = run(communicator); } } System.exit(status); } private static int run(com.zeroc.Ice.Communicator communicator) { // // Create an object adapter. // com.zeroc.Ice.ObjectAdapter adapter = communicator.createObjectAdapterWithEndpoints("SimpleFilesystem", "default -h localhost -p 10000"); // // Create the root directory (with name "/" and no parent) // DirectoryI root = new DirectoryI("/", null); root.activate(adapter); // // Create a file called "README" in the root directory // FileI file = new FileI("README", root); String[] text; text = new String[]{ "This file system contains a collection of poetry." }; try { file.write(text, null); } catch(GenericError e) { System.err.println(e.reason); } file.activate(adapter); // // Create a directory called "Coleridge" in the root directory // DirectoryI coleridge = new DirectoryI("Coleridge", root); coleridge.activate(adapter); // // Create a file called "Kubla_Khan" in the Coleridge directory // file = new FileI("Kubla_Khan", coleridge); text = new String[]{ "In Xanadu did Kubla Khan", "A stately pleasure-dome decree:", "Where Alph, the sacred river, ran", "Through caverns measureless to man", "Down to a sunless sea." }; try { file.write(text, null); } catch(GenericError e) { System.err.println(e.reason); } file.activate(adapter); // // All objects are created, allow client requests now // adapter.activate(); // // Wait until we are done // communicator.waitForShutdown(); return 0; } }
The code imports the contents of the Filesystem
package. This avoids having to continuously use fully-qualified identifiers with a Filesystem.
prefix.
The next part of the source code is the definition of the Server
class. Much of this code is boiler plate: we create a communicator, then an object adapter, and, towards the end, activate the object adapter and call waitForShutdown
.
The interesting part of the code follows the adapter creation: here, the server instantiates a few nodes for our file system to create the structure shown below:
A small file system.
As we will see shortly, the servants for our directories and files are of type DirectoryI
and FileI
, respectively. The constructor for either type of servant accepts two parameters, the name of the directory or file to be created and a reference to the servant for the parent directory. (For the root directory, which has no parent, we pass a null parent.) Thus, the statement
DirectoryI root = new DirectoryI("/", null);
creates the root directory, with the name "/"
and no parent directory.
Here is the code that establishes the structure in the above illustration:
// // Create the root directory (with name "/" and no parent) // DirectoryI root = new DirectoryI("/", null); root.activate(adapter); // // Create a file called "README" in the root directory // FileI file = new FileI("README", root); String[] text; text = new String[]{ "This file system contains a collection of poetry." }; try { file.write(text, null); } catch(GenericError e) { System.err.println(e.reason); } file.activate(adapter); // // Create a directory called "Coleridge" in the root directory // DirectoryI coleridge = new DirectoryI("Coleridge", root); coleridge.activate(adapter); // // Create a file called "Kubla_Khan" in the Coleridge directory // file = new FileI("Kubla_Khan", coleridge); text = new String[]{ "In Xanadu did Kubla Khan", "A stately pleasure-dome decree:", "Where Alph, the sacred river, ran", "Through caverns measureless to man", "Down to a sunless sea." }; try { file.write(text, null); } catch(GenericError e) { System.err.println(e.reason); } file.activate(adapter);
We first create the root directory and a file README
within the root directory. (Note that we pass a reference to the root directory as the parent when we create the new node of type FileI
.)
The next step is to fill the file with text:
String[] text; text = new String[] { "This file system contains a collection of poetry." }; try { file.write(text, null); } catch(GenericError e) { System.err.println(e.reason); }
Recall that Slice sequences by default map to Java arrays. The Slice type Lines
is simply an array of strings; we add a line of text to our README
file by initializing the text
array to contain one element.
Finally, we call the Slice write
operation on our FileI
servant by writing:
file.write(text, null);
This statement is interesting: the server code invokes an operation on one of its own servants. Because the call happens via a reference to the servant (of type FileI
) and not via a proxy (of type FilePrx
), the Ice run time does not know that this call is even taking place — such a direct call into a servant is not mediated by the Ice run time in any way and is dispatched as an ordinary Java function call.
In similar fashion, the remainder of the code creates a subdirectory called Coleridge
and, within that directory, a file called Kubla_Khan
to complete the structure in the illustration listed above.
NodeI
Servant Class in Java
Our NodeI
servant class has the following basic structure:
class NodeI implements Node { // Constructor and methods here... private String _name; private DirectoryI _parent; }
The class has two data members:
_name
This member stores the name of the file incarnated by the servant.
_parent
This member stores the reference to the servant for the node's parent directory.
The _name
and _parent
data members are initialized by the constructor.
NodeI
also provides the implementation for the Slice operation name and an activate helper method:
// NodeI constructor NodeI(String name, DirectoryI parent) { _name = name; _parent = parent; } // Slice Node::name() operation @Override public String name(com.zeroc.Ice.Current current) { return _name; } void activate(com.zeroc.Ice.ObjectAdapter a) { // // Create an identity // com.zeroc.Ice.Identity id = new com.zeroc.Ice.Identity(); id.name = _parent == null ? "RootDir" : java.util.UUID.randomUUID().toString(); NodePrx thisNode = NodePrx.uncheckedCast(a.add(this, id)); if(_parent != null) { _parent.addChild(thisNode); } }
activate
generates an identity for the node: "RootDir" when parent is null, and a random ID otherwise. activate
then adds this
(a servant) to the provided object adapter. And finally, when the node has a parent directory, activate
registers a proxy to this
with the parent directory.
FileI
Servant Class in Java
Our FileI
servant class simply reuses the NodeI implementation and implements read
and write
:
import com.zeroc.demos.Manual.simpleFilesystem.Filesystem.*; final class FileI extends NodeI implements File { // FileI constructor FileI(String name, DirectoryI parent) { super(name, parent); } // Slice File::read() operation @Override public String[] read(com.zeroc.Ice.Current current) { return _lines; } // Slice File::write() operation @Override public void write(String[] text, com.zeroc.Ice.Current current) throws GenericError { _lines = text; } private String[] _lines; }
DirectoryI
Servant Class in Java
The DirectoryI
class reuses NodeI and implements list
:
import com.zeroc.demos.Manual.simpleFilesystem.Filesystem.*; final class DirectoryI extends NodeI implements Directory { // DirectoryI constructor DirectoryI(String name, DirectoryI parent) { super(name, parent); } // Slice Directory::list() operation @Override public NodePrx[] list(com.zeroc.Ice.Current current) { NodePrx[] result = new NodePrx[_contents.size()]; _contents.toArray(result); return result; } // addChild is called by the child in order to add // itself to the _contents member of the parent void addChild(NodePrx child) { _contents.add(child); } private java.util.List<NodePrx> _contents = new java.util.ArrayList<>(); }
Note that the _contents
member is of type java.util.ArrayList<NodePrx>
, which is convenient for the implementation of the addChild
method. However, this requires us to convert the list into a Java array in order to return it from the list
operation.