View Source Front Page How-To Articles JavaScript Apostle DHTML Demystified Human Interface Online Feature Articles Case Studies Editor's Corner Third-Party News

Connecting JavaScript to Java with LiveConnect

By Gary Smith


Send comments and questions about this article to View Source.

In Navigator 3.0, Netscape introduced LiveConnect as a technology that allows JavaScript code to interact and communicate with Java applets and plug-ins. In this article, I'll show how you can write pure JavaScript code that will have Java-like capabilities without your having to write any Java applets or plug-ins. I'll provide several examples of using LiveConnect that show you how to add Java- or server-like functionality to your client-side web applications, essentially connecting the JavaScript and Java languages. In this way you can extend your programming skills and improve your web applications by adding advanced JavaScript and Java features, such as the new capabilities of JavaScript 1.2 and Java's privilege-based security model.

You need to know at least the basics of the Java and JavaScript languages to understand what's going on in LiveConnect. I'm assuming in this article that you're familiar with the basic syntax of each language as well as their fundamental differences. I also assume that you're familiar with the functionality of the common JDK classes and methods.

Note that you must enable JavaScript to run the examples in this article.

BASIC SYNTAX FOR LIVECONNECT

The basic syntax for applying LiveConnect to JavaScript is not very different from the way you write regular JavaScript code. The LiveConnect architecture runs silently in the background as a part of the Navigator engine, so you don't need to know everything that's going on inside Navigator to write code for LiveConnect. I'm not going to discuss the architecture details of the Navigator engine, since it would make this article considerably longer. If you want detailed information, there are several great books and online resources that discuss LiveConnect architecture and technology in Navigator and Netscape Enterprise Server. From these resources, you can learn more about JavaScript JavaPackage objects, JavaClass objects, and JavaObject objects, including exactly how they interact with the standard Java system classes built into Navigator and how LiveConnect handles the data type conversion.

The following is a very basic LiveConnect code example:

<SCRIPT>
var myPrintln = java.lang.System.out.println;
</SCRIPT>

As you can see, I simply created a JavaScript variable reference named myPrintln referring to the Java static class method java.lang.System.out.println() that's built into Navigator; this method prints a line to the Java console. I can now call this variable in my code as if I were actually calling the java.lang.System.out.println() method in Navigator's built-in Java classes. Try the following HTML form button, which does just that:

You should see "print to the Java Console" in the Java console; to open this console in Communicator, choose Java Console from the Communicator menu (the one labeled with the Communicator icon). This is what the code for the Form button looks like:

<FORM>

<INPUT TYPE="button" VALUE="call myPrintln()" onClick="myPrintln('print to the Java Console');">
</FORM>

In this same way, you can refer to all of the classes in the version of the Java langauge that's built into Navigator, because every Navigator window object contains the java, sun, and netscape properties, which are references to the associated Java classes. (Actually, they're references to the associated JavaScript JavaPackage objects representing the related Java classes that are represented by the JavaClass object -- but again, I'm not going to get into these architecture details.)

As an alternative syntax, you can also call Java's println() method more directly in LiveConnect, without creating variable references, as in the following example:

<SCRIPT>
java.lang.System.out.println('print any string');
</SCRIPT>
You can add this type of code in-line, with a link like the following:

The equiv with special characters inserted should look like:

<A HREF="javascript:java.lang.System.out.println('print any string'); ">java.lang.System.out.println('print any string');</A>

SAMPLE DEBUGGING TECHNIQUE

Referencing and calling println() is not only easy, it's also a very useful technique for debugging or tracking the state in your scripts during development. This technique should sound familiar to those of you who use the built-in JavaScript alert() method for debugging. For example, suppose we write a script that increments a variable every time we call a certain function, such as the following script:

<SCRIPT LANGUAGE="JavaScript">

<!--
function myFunc() {
    foo++;
    alert("foo=" + foo);
}
//-->
</SCRIPT>
Now we call the function, with this button:

We used the alert() method to print the value of the variable foo in an alert dialog box, and it reflects our mistake (of not first defining foo) by printing "foo=NaN" in the dialog.

This technique is a quick and easy way to debug a script; however, opening and closing alert dialogs can get pretty annoying, especially if you're trying to debug something that calls your alert debugger several times in a row, such as in a for or while loop. So, in this next example, we'll do the same thing using the println() method rather than alert().

<SCRIPT LANGUAGE="JavaScript">

<!--
function myFunc2() {
    foo++;
    java.lang.System.out.println("foo=" + foo);
}
//-->
</SCRIPT>
Call the function from the following button:

The println() method prints the value of the variable foo to the Java console. You can click this button as many times as you want, and it will keep printing as did the alert() method, except you don't have to bother with closing the alert dialog every time.

Now we'll fix the bug by properly defining the variable foo, like so:

<SCRIPT LANGUAGE="JavaScript">

<!--
var foo = 0;
function myFunc3() {
    foo++;
    java.lang.System.out.println("foo=" + foo);
}
//-->
</SCRIPT>
Call this function as many times as you like:

Looking in the Java console, you should see a value printed for foo, properly incremented.

REFERRING TO JAVA AWT CLASSES

Now we'll take LiveConnect a bit further by illustrating how you can refer to other Java classes that are built into Navigator: specifically, how you can refer to Java AWT classes with LiveConnect. We'll connect JavaScript to Java by creating a new instance of a Java class using the JavaScript new keyword.

Example 1 shows a function named browse() that opens a native "file dialog" using the built-in java.awt.Frame and java.awt.FileDialog classes. This example was actually taken from the code that I wrote inside Visual DHTML for saving files.


Example 1
<SCRIPT LANGUAGE="JavaScript" ID="Ex1">

<!--
function browse() {
    var Frame = new java.awt.Frame();
    var fd = new java.awt.FileDialog(Frame, "Browse File", 
java.awt.FileDialog.LOAD);
    fd.toFront();
    fd.show();
    var getDirectory = new java.awt.FileDialog(Frame);
    var filename = fd.getDirectory() + fd.getFile();
    alert('You selected: ' + filename);
}
//-->
</SCRIPT>

The following button calls the browse() function:

The function opens the file dialog and then, after a file is selected, calls the alert() method to display the selected file name and path.

As you can see from the example JavaScript code, I created a variable named Frame that invokes an instance of the java.awt.Frame class using the new keyword, and a variable named fd (for "file dialog") that invokes an instance of java.awt.FileDialog. If you're familiar with these common Java classes, this code should be a piece of cake to understand. And, with the flexibility of the JavaScript and Java languages, you should be able to rewrite this code in various ways for different web applications.

ACCESSING USER PREFERENCES WITH SIGNED OBJECTS

This section introduces some of the new power and flexibility of LiveConnect in JavaScript 1.2 inside Netscape Navigator 4.0 or Communicator. Communicator brings LiveConnect to a whole new level, by introducing a policy for JavaScript security based on the new Java security model, Object Signing. To make use of the new policy in JavaScript, you must use the Java security classes and then digitally sign your JavaScript scripts. For more information on signed scripts, see JavaScript Security in Communicator 4.x.

Example 2 shows how to read "user preferences" -- something many JavaScript developers have long wanted to do but couldn't because of the previous security restrictions. Reading or writing user preferences was purposely not allowed in Navigator for protection and privacy reasons. Now it is allowed, with the new interactive security model in Communicator: users are prompted with the Java Security dialog, which provides the option to either grant or deny the requested privilege at run time.


Example 2
<SCRIPT LANGUAGE="JavaScript1.2" ID="Ex2">

<!--
function readPref(prefName) {
    netscape.security.PrivilegeManager.enablePrivilege("UniversalPreferencesRead");
    alert(prefName + "=" + navigator.preference(prefName));
}
//-->
</SCRIPT>

The first line of code in the readPref() function calls the public static enablePrivilege() method of the Java security PrivilegeManager class, which is contained in the netscape.security package. When the enablePrivilege() method is called, the digital signature is verified and, if the signature is valid, the Java Security dialog box opens. If the signature is not valid, the script will not be executed and an alert dialog will display a warning message that an uncaught ForbiddenTargetException Java exception error occurred. If you want to trap that exception in JavaScript, see the section on error handling.

This example shows how you generally request expanded privileges from the new Java security model. In this case I passed the UniversalPreferencesRead parameter, which is the system target that's required for reading user preferences. You must grant the requested privilege for Navigator to continue executing the script.

Now, call the readPref() function from the following button:

If you grant this requested privilege, the function will display an alert dialog containing the value of the prefName parameter. In this case the value of prefName is the value of the mail.identity.useremail property, which refers to the user's e-mail address as set by the user. In the prefs.js file in your Communicator user profile directory, you'll find this user preference property, along with all the other user preference (user_prefs) properties, listed according to how you set them. (Your user profile directory is usually C:\Program Files\Netscape\Communicator\Users\your name\, depending on your system and where you installed Communicator.)

Note that I've added ID attributes to the script and button tags, because signing inline and event handler JavaScript scripts requires ID attributes for generating inline signatures.

You not only can read the user preferences that are kept in Communicator's prefs.js file, you also can read Java system properties, as illustrated in Example 3.


Example 3
<SCRIPT LANGUAGE="JavaScript1.2" ID="Ex3">

<!--
function readProp(propName) {
    netscape.security.PrivilegeManager.enablePrivilege("UniversalPropertyRead");
    alert(propName + "=" + java.lang.System.getProperty(propName));
}
//-->
</SCRIPT>

You can call the readProp() function from the following button:

Notice how similar the readProp() function is to the readPref() function in Example 2. This time we passed a different system target parameter: UniversalPropertyRead instead of UniversalPreferencesRead. Because we're requesting a different system target, the PrivilegeManager will open another Java Security dialog box for this function; to avoid this, you should use macro targets whenever possible. For more information on security privilege targets, see Netscape System Targets.

If you're concerned about signing your scripts, note that Communicator can recognize codebases as principals. You can enable codebase principal support in your client by adding the following user preference to your prefs.js file:

user_pref("signed.applets.codebase_principal_support", true);

See Introduction to the Capabilities Classes for more information.

READING AND WRITING FILES

This next example also illustrates accessing Java system properties: specifically, those related to reading and writing files. We'll look at how to write JavaScript code using LiveConnect to read and write file data by accessing the File class in Java's I/O library. This technique can be very useful in web applications for storing and retrieving user-specific information, such as saving the state between user sessions and processes or saving user preferences.

Example 4 is based on a function used in Visual DHTML for saving files. It shows a JavaScript function, writeFile(), that uses LiveConnect to create a stream in order to write to a file, using an instance of Java's DataOutputStream class.


Example 4
<SCRIPT LANGUAGE="JavaScript1.2" ID="Ex4">

<!--
function writeFile(fileName, data) {
    // The following calls require Object Signing
    netscape.security.PrivilegeManager.enablePrivilege("UniversalPropertyRead");
    var userHome = java.lang.System.getProperty("user.home");
    var fileSeparator = java.lang.System.getProperty("file.separator");
    var fileName = userHome + fileSeparator + (fileName || 'myFile.txt');
    
    netscape.security.PrivilegeManager.enablePrivilege("UniversalFileAccess");
    var raf = new java.io.RandomAccessFile(fileName, "rw");
    raf.seek(raf.length());
    raf.writeUTF(Date() + "::" + data + "\n");
    raf.close();
    java.lang.System.out.println(Date() + "::" + data + "\n");
}
//-->
</SCRIPT>

In the first four lines of this function, I chose to find and write the file in the user.home directory, getting the value from the Java system property as in the previous section. This way I stay in my user profile directory. I also used the file.separator Java system property to make this script work better across platforms, since different platforms use different file separators.

The rest of the code in writeFile() requires a different privilege: that of the UniversalFileAccess target, for permission to create the stream for writing to the file. In LiveConnect, this can be done by setting a JavaScript variable to refer to an instance of java.io.DataOutputStream.

You can call writeFile() with this button:

Look in the Java console for feedback, and also in your user profile directory, where you'll find a new file named myFile.txt that was created by the writeFile() button and function. I've defined writeFile() to have two parameters -- the file name and an arbitrary string to be written out -- just to illustrate that you can create a generic function for writing different things to different files, based on the function parameters, for a variety of web applications.

Example 5 shows how to read the same file we just wrote to. (If you know the Java language really well and are getting the hang of this LiveConnect stuff, you might want to skip to the next section, since this is very much like the writeFile() function.) Note that you will probably want to do more error handling here in order to catch potential I/O exceptions, especially for certain platforms that don't let you dynamically create files.


Example 5
<SCRIPT LANGUAGE="JavaScript1.2" ID="Ex5">

<!--
function readFile(fileName) {
    netscape.security.PrivilegeManager.enablePrivilege("UniversalPropertyRead");
    var userHome = java.lang.System.getProperty("user.home");
    var fileSeparator = java.lang.System.getProperty("file.separator");
    var fileName = userHome + fileSeparator + 'myFile.txt';

    netscape.security.PrivilegeManager.enablePrivilege("UniversalFileAccess");
    var fis  = new java.io.FileInputStream(fileName);
    var ds   = new java.io.DataInputStream(fis);
    var fileData = "";
    var line    = "";
    while ((line = ds.readLine()) != null) { 
        line.toString();
        fileData += line.substring(2, line.length()) + "\n"; 
    }
    fis.close();
    alert(fileData);
}
//-->
</SCRIPT>

The main differences between readFile() and writeFile() are that readFile() reads lines of data from the file using Java's DataInputStream class and returns the file data.

Call readFile() from the following button:

This function opens an alert dialog displaying the file data that was read via a LiveConnect instance of java.io.DataInputStream.

CREATING A CLIENT-SIDE DATABASE

Now that we've seen how to access files using LiveConnect, we'll use JavaScript accessor functions to create a very simple client-side (flat-file) database for reading, writing, and modifying records.

The setDatabaseRecord() function in Example 6 is similar to the writeFile() function in Example 4, except that setDatabaseRecord() reads a database file, looking for the record name. Each record is kept on a single line. If the record is found, the function replaces that record with the value passed as an argument; otherwise, it appends the name and value parameters to the database as a new record.


Example 6
<SCRIPT LANGUAGE="JavaScript1.2" ID="Ex6">

<!--
function setDatabaseRecord(name, value) {
    netscape.security.PrivilegeManager.enablePrivilege("UniversalPropertyRead");
    var userHome = java.lang.System.getProperty("user.home");
    var fileSeparator = java.lang.System.getProperty("file.separator");
    var fileName = userHome + fileSeparator + 'myDatabase.txt';

    
netscape.security.PrivilegeManager.enablePrivilege("UniversalFileAccess");
    var raf = new java.io.RandomAccessFile(fileName, "rw");
    var fis = new java.io.FileInputStream(fileName);
    var ds  = new java.io.DataInputStream(fis);
    var data = new Array();
    var line   = "";
    var record = true;
    while ((line = ds.readLine()) != null) {
        if (line.startsWith(name + "=")) {
            line = name + "=" + value;
            record = false;
            java.lang.System.err.println("replacing: " + line);
        }
        data[data.length] = line;
    }
    fis.close();
    if (record) {
        data[data.length] = name + "=" + value;
    }
    var fps  = new java.io.PrintStream(new 
java.io.FileOutputStream(fileName));
    for (var i in data) {
        fps.println(data[i]);
        java.lang.System.err.println("data[" + i + "]=" + data[i]);
    }
    fps.close();
}
//-->
</SCRIPT>

You can call the setDatabaseRecord() function from the following links to see how this works:

After calling the function from these links, you should find these name and value parameters recorded in the myDatabase.txt file that was created in your user profile directory. You can also look in the Java console and see the records being printed, because I added the java.lang.System.err.println() method for debugging purposes.

Example 7 shows the getDatabaseRecord() function, for reading the value of an individual record.


Example 7
<SCRIPT LANGUAGE="JavaScript1.2" ID="Ex7">

<!--
function getDatabaseRecord(name) {
    netscape.security.PrivilegeManager.enablePrivilege("UniversalPropertyRead");
    var userHome = java.lang.System.getProperty("user.home");
    var fileSeparator = java.lang.System.getProperty("file.separator");
    var fileName = userHome + fileSeparator + 'myDatabase.txt';

    netscape.security.PrivilegeManager.enablePrivilege("UniversalFileAccess");
    var raf = new java.io.RandomAccessFile(fileName, "rw");
    var fis = new java.io.FileInputStream(fileName);
    var ds  = new java.io.DataInputStream(fis);
    var value;
    var line  = "";
    while ((line = ds.readLine()) != null) {
        if (line.startsWith(name + "=")) {
            value = line.substring(line.indexOf("=") +1);
            java.lang.System.err.println("found=" + value);
        }
    }
    fis.close();
    java.lang.System.err.println("value=" + value);
    return value;
}
//-->
</SCRIPT>

This function searches the database file looking for the record name, similar to the setDatabaseRecord() function. If the record is found, it gets and returns the value of the record; otherwise, it returns the value as null.

You can call getDatabaseRecord() from the following links:

Did you find this LiveConnect code simple, powerful, and potentially useful? You can do a lot more for your web applications using JavaScript-based LiveConnect, especially for offline usage, hit tracking, personalization, and other traditionally server-side functionality -- all on the client side.

MAKING SOCKET CONNECTIONS

In this section we'll see how to make secure socket connections using LiveConnect, enabling your web applications to send client-side data to a remote destination. You can now make the same socket connections with JavaScript that you can with Java.

Example 8 shows how to use LiveConnect to make a JavaScript socket connection for sending an e-mail message to users in HTML format. Notice that accessing sockets is an expanded security privilege that involves requesting the UniversalConnect target.


Example 8
<SCRIPT LANGUAGE="JavaScript1.2" ID="Ex8">

<!--
function sendMail(subject, body) {
    netscape.security.PrivilegeManager.enablePrivilege("UniversalPreferencesRead");
    var recipient = navigator.preference("mail.identity.useremail");
    var smtp_server = navigator.preference("network.hosts.smtp_server");

    netscape.security.PrivilegeManager.enablePrivilege("UniversalConnect");
    var response = java.lang.System.out.println;
    var smtp = new java.net.Socket(smtp_server, 25);
    var smtpIn = new java.io.DataInputStream(smtp.getInputStream());
    var smtpOut = new java.io.PrintStream(smtp.getOutputStream());

    smtpOut.println("HELO " + smtp_server);
    response(smtpIn.readLine());
    response(smtpIn.readLine());
    smtpOut.println("MAIL FROM: " + recipient);
    response(smtpIn.readLine());
    smtpOut.println("RCPT TO: " + recipient);
    response(smtpIn.readLine());
    smtpOut.println("DATA");
    response(smtpIn.readLine());
    smtpOut.println("Subject: " + subject);
    smtpOut.println("X-Mailer: LiveConnect");
    smtpOut.println("MIME-Version: 1.0");
    smtpOut.println("Content-Type: text/html");
    smtpOut.println("");
    smtpOut.println(body);
    smtpOut.println(".");
    smtpOut.println("QUIT");
    response(smtpIn.readLine());
}
//-->
</SCRIPT>

The first three lines of the sendMail() function should look familiar if you've read the previous sections of this article. We find the user's e-mail address for the recipient variable by getting the value of the navigator.preference property, as in the earlier user preferences example, and we get the SMTP server address from the network.hosts.smtp_server property.

The rest of the code requires the UniversalConnect target privilege to create the socket to connect to the SMTP server. If you're familiar with sockets, you probably don't need further explanation, especially if you know SMTP. In a nutshell:

With the Java console open in Communicator, call the sendMail() function from the following button:

I've defined sendMail() to have two parameters -- the subject and the body for the email message. You should see the results from the SMTP server printed to the Java console, because I created the var println reference in the code to print the server response after each SMTP command. Note that Example 8 doesn't do any error handling; feel free to improve it to suit your needs.

Example 9 shows how to write LiveConnect code that uses the java.net.URL class for making a socket connection to fetch the "source" of a web page. You can do this for the purpose of dynamically parsing or reading certain strings or values contained in the source, or for modifying the source of the web page. In this case, I'm simply counting the lines of the web page and printing the line numbers and lines of the source to the Java console. Notice that the code also looks for the <SCRIPT LANGUAGE='JavaScript'> tag.


Example 9
<SCRIPT LANGUAGE="JavaScript1.2" ID="Ex9">

<!--
function fetchWebpage(url) {
    netscape.security.PrivilegeManager.enablePrivilege("UniversalConnect");
    var dest = new java.net.URL(url);
    var dis = new java.io.DataInputStream(dest.openStream());
    var tag = "SCRIPT LANGUAGE='JavaScript'>";
    var hasTag;
    var i = 0;
    while ((line = dis.readLine()) != null) {
        java.lang.System.out.println("line "+ i +": " + line);
        if (line.indexOf(tag) != -1) {
           hasTag = i;
        }
        i++;
    }
    dis.close();

    var str = "Total line were: " + i;
    if (hasTag) {
        alert(str + " and a " + tag + " tag was found on line " + hasTag); 
    } else {
        alert(str + " and no " + tag + " tag was found.");
    }
}
//-->
</SCRIPT>

With the Java console open, call the fetchWebpage() function from the following button:

Notice that the script uses java.io.DataInputStream, opening a stream to read the lines of the web page. At the end of the web page, when there are no more lines to read, the script closes the stream and uses an alert dialog to display the results of whether the web page contained the <SCRIPT LANGUAGE='JavaScript'> tag. You can improve and modify this code according to your needs.

If you like, you can make more complicated socket connections that request the POST method or connect to various servers on different ports or protocols, just as in Java code, since LiveConnect can also access all of the other classes contained in the java.net package built into Navigator. Note, however, that you have to be careful when making socket connections in JavaScript code, because you don't want to write code that will expose a password or anything else that you don't want users to find by viewing the source code, unless you're writing it for use within a secure intranet. It's best only to make connections that prompt the user for that information whenever possible, or make connections that don't require having confidential information contained in the source code.

ERROR HANDLING

As you may have noticed by now, if you click the Deny button to deny a privilege, or if the digital signature is not valid for a script requesting a privilege, the script will not be executed, and an alert dialog will display a warning message that an uncaught ForbiddenTargetException Java exception error occurred. If you want to trap that exception in JavaScript, you'll need to add an onError handler. I've included a very simple event handler for this page called handleErrors(), but you'll probably want to add your own error handlers to trap specific exceptions, especially for environments that you're less sure about or platforms that implement stricter security.

You can create your own event handler for displaying messages before the JavaScript error dialog appears by adding the following line of code:

window.onError = myErrorHandler;
You can also directly override Navigator's built-in onError() method, by adding your own onError() function, like this:
<SCRIPT LANGUAGE="JavaScript1.2">

<!--
function onError(e, url, line) {
    if (e.indexOf('ForbiddenTargetException') != -1) {
        // User didn't grant privilege
        //...
    } else if (e.indexOf('IOException') != -1) {
        // For IOException errors
        //...
    } else {
        // etc.
        //...
    }
}
//-->
</SCRIPT>
Note that Netscape plans on implementing exception handling in JavaScript 1.3 to make this much easier.

APPLET AND PLUG-IN COMMUNICATION

Although applets and plug-ins are not the focus of this article, I'll briefly mention that using LiveConnect can significantly improve your existing web applications that contain Java applets or plug-ins. This article has discussed LiveConnect for JavaScript-to-Java communication; on the other side of the coin, you can do the opposite with LiveConnect to achieve Java-to-JavaScript communication via your Java applet code, as well as achieve Java-to-plug-in and plug-in-to-JavaScript communication using plug-ins.

For example, JavaScript code can access the public methods and control your Java applet, just as it can access other Java classes. LiveConnect also uses the netscape.javascript.JSObject class built into Navigator, which you can add to your applet for Java-to-JavaScript communication. These kinds of possibilities allow you to provide many of the JavaScript features in your Java applets and in your web applications that are not exposed to Java.

For more sources of information on applet and plug-in communication, see the resources section at the end of this article.

ENDLESS POSSIBILITIES

The possibilities LiveConnect offers JavaScript programmers are endless. There are already several very innovative things being done today with JavaScript 1.2 and LiveConnect. I hope the code samples in the article help you with using LiveConnect for creating and improving your web applications.


FURTHER RESOURCES


View Source wants your feedback!
Write to us and let us know
what you think of this article.


Many thanks to Paul Dreyfus for the opportunity to write this article, to Caroline Rose for the fantastic editing job, and to Norris Boyd for reviewing the security issues.

Gary Smith is a Netscape engineer, creator of Visual DHTML, Dialog Widget, and Webtop Widget. He works on new technology projects involving LiveConnect, JavaScript, Java, CGI, XML/RDF, and Web-crawling technologies, such as Netcaster and Channel Finder. Occasionally he writes sample code and documentation for DevEdge Online.

(4.98)

For the latest technical information on Sun-Netscape Alliance products, go to: http://developer.iplanet.com

For more Internet development resources, try Netscape TechSearch.


Copyright © 1999 Netscape Communications Corporation.
This site powered by: Netscape Enterprise Server and Netscape Compass Server.