Search

Saturday, 22 December 2012

Sirius: First steps

In the previous article we've created basic skeleton for entire solution. Now it's time to add the content. In this article I'll take sample method, write it's server part and create clients for all programming languages we already prepared infrastructure for. The example would be simple but it's enough to find out how it's better to design entire solution and what is the base flow for adding new functionality. Also it would be clearly seem how it is better to use such solution. So, in this article I'll describe the following:

  1. Sample server side method creation (actually that would be the sample of SOAP web-service creation on Java)
  2. Java client creation
  3. Ruby client creation
  4. C# client creation

OK. We're good to go.

Server side

Creating server side method

For demonstration purpose we'll create simple method which checks if file specified by the path exists in the system. It's trivial that's why it's good for demonstration. So, the actions are the following:

  1. Open Eclipse with the Server project (create new if the one doesn't exist)
  2. Create new package named org.sirius.server.system
  3. Add new class named FileOperations
Once it's done we can add method with the following content:
 public boolean exists(String fileName) {
  File file = new File(fileName);
  return file.exists() && file.isFile();
 }
Well, it's easy. Probably the only thing to explain here is that we not only check if file exists but also that the file specified by the parameter is actually the file but not the folder. That's just additional check.

Making SOAP service

In order to make the class a service endpoint we just have to put @WebService annotation on that. This annotation is defined in the javax.jws.WebService package. So, we should include that as well. Generally class is modified to the following form:

/**
 * 
 */
package org.sirius.server.system;

import java.io.File;

import javax.jws.WebService;

@WebService
public class FileOperations {

 /**
  * 
  * @param fileName
  * @return
  */
 public boolean exists(String fileName) {
  File file = new File(fileName);
  return file.exists() && file.isFile();
 }
}

After that we should be able to start the service itself. I don't want the server part to be deployed into some web-server. All I want is to be able to run it from the command line. For this purpose the main class should be added.

Actually, in the previous article we added the task which makes executable package and the org.sirius.server.Starter class is used as main class. So, let's make it. I'll create the Starter class in the org.sirius.server with the following content:

package org.sirius.server;

import javax.xml.ws.Endpoint;

public class Starter {

 public static void main(String[] args) throws Exception {
  Endpoint.publish("http://localhost:21212/system/file", Class.forName("org.sirius.server.system.FileOperations")
      .newInstance());
 }
}
That's it. If we run this class we'll be able to see the generated WSDL by the following URL: http://localhost:21212/system/file?wsdl and we can even call this method. But for now we should do something else.

Starter improvements

This is just small example but at the same time the gaps in it are quite obvious. Some of the most clearly seen are:

  1. Host name is hard-coded though it could be better if they were passed via command line
  2. There can be many classes containing endpoints implementation so we should be able to load many of them and some of that classes might be from external location. So, we should reserve that ability.

First improvement is relatively easy. We should identify how parameters are passed via command line. OK, let's the command line look like

<executable> [-host <hostname> [-port <portnumber>] ]
So, we have 2 command line keys which can be defined as the constants like:
 private static final String HOST_KEY = "-host";
 private static final String PORT_KEY = "-port";
as well as we'll update the class with 2 constants representing default values in case those parameters aren't defined:
 private static final String DEFAULT_HOST = "localhost";
 private static final String DEFAULT_PORT = "21212";
Then we're ready to modify main function to handle parameters. Now it looks like:
 public static void main(String[] args) throws Exception {
  String host = DEFAULT_HOST;
  String port = DEFAULT_PORT;

  HashMap<String, String> params = new HashMap<String, String>();

  for (int i = 0; i < args.length; i += 2) {
   if (i < args.length - 1) {
    params.put(args[i], args[i - 1]);
   }
  }

  if (params.containsKey(HOST_KEY)) {
   host = params.get(HOST_KEY);
  }
  if (params.containsKey(PORT_KEY)) {
   port = params.get(PORT_KEY);
  }
  Endpoint.publish("http://" + host + ":" + port + "/system/file", Class.forName("org.sirius.server.system.FileOperations")
      .newInstance());

 }

But that was only one improvement. Another improvement is to add the ability to configure the set of classes to load as well as their location. So, we need some configuration file. I decided to specify such configuration using CSV format. It's easy to parse and it's easy to edit. Even Excel can be used for that. So, for recently developped web-service class I can define such configuration:

Endpoint,                             Class,                                    Package
http://${HOST}:${PORT}/system/file,   org.sirius.server.system.FileOperations,  Local
Here Local is the predefined keyword meaning that file is loaded from the same binaries as the server itself. Since we're going to configure host and port from the command line these parameters should be transformed somehow into the endpoint being read from configuration file. For that purpose I've reserved slots for ${HOST} and ${PORT}. Respective values will replace corresponding tokens.

So, generally, what I'm going to do is:

  1. Read another parameter which should collect information about configuration file
  2. Read configuration file and place data into the array of structure containing combination of endpoint, class and package (separate type will be created for that purpose)
  3. Initiate all endpoints iterating through array
So, let's describe the above steps in more details.

Firstly, I'll update Starter class with constants related to configuration file processing. The updates are:

 public static void main(String[] args) throws Exception {
  String host = DEFAULT_HOST;
  String port = DEFAULT_PORT;
  String config = DEFAULT_CONFIG;

  HashMap<String, String> params = new HashMap<String, String>();

  for (int i = 0; i < args.length; i += 2) {
   if (i < args.length - 1) {
    params.put(args[i], args[i - 1]);
   }
  }

  if (params.containsKey(HOST_KEY)) {
   host = params.get(HOST_KEY);
  }
  if (params.containsKey(PORT_KEY)) {
   port = params.get(PORT_KEY);
  }
  if (params.containsKey(CONFIG_KEY)) {
   config = params.get(CONFIG_KEY);
  }
  Endpoint.publish("http://" + host + ":" + port + "/system/file", Class.forName("org.sirius.server.system.FileOperations")
    .newInstance());
 }
Changes are highlighted with yellow.

Configuration reading is a bit more complex operation. It will require additional data type and then additional method. So, firstly, let's add new PackageOptions class to the org.sirius.server package. After all updates the class looks like:

package org.sirius.server;

/**
 * .
 */
public class PackageOptions {

 private String _endPoint = null;
 private String _className = null;
 private String _packageLocation = null;
 
 public PackageOptions(String endPoint, String className,
   String packageLocation) {
  _endPoint = endPoint;
  _className = className;
  _packageLocation = packageLocation;
 }

 /**
  * @return the _endPoint
  */
 public final String get_endPoint() {
  return _endPoint;
 }

 /**
  * @return the _className
  */
 public final String get_className() {
  return _className;
 }

 /**
  * @return the _packageLocation
  */
 public final String get_packageLocation() {
  return _packageLocation;
 }
}
It's just a data type. Next step is to add method reading data from config file into Starter class. This method should read configuration file line by line and append the list of PackageOption type. Generally the method looks like:
 public ArrayList readConfig(String config)
   throws IOException {
  FileReader reader = new FileReader(config);
  BufferedReader br = new BufferedReader(reader);

  String line = br.readLine();

  ArrayList<PackageOptions> options = new ArrayList<PackageOptions>();

  while ((line = br.readLine()) != null) {
   if(line.startsWith("#")){
    continue;
   }
   String[] row = line.split(",");
   if (row.length < 3) {
    continue;
   }
   PackageOptions option = new PackageOptions(row[0].trim(),
     row[1].trim(), row[2].trim());
   options.add(option);
  }

  br.close();
  reader.close();
  return options;
 }
In addition to common flow I've added some additional "whistles" for handling some specific cases. I've marked them with red and green colors. The code marked with red handles the case if the file content is malformed and the number of columns is smaller than expected. The code marked with the green gives additional ability to comment entire lines in case we don't want to add some module but we want to keep the record for future use.

In the above method we've retrieved all configuration entries. So, eventually we should walk through this list and initiate endpoints one by one. That's a matter of additional method which looks like:

 public void startEndPoints(ArrayList options, String host,
   String port) {
  for (PackageOptions option : options) {
   if (!option.get_packageLocation().equals("Local")) {
    // TODO: Add load class instructions
   }
   try {
    String endPoint = option.get_endPoint();
    endPoint = endPoint.replaceAll("\\$\\{HOST}", host);
    endPoint = endPoint.replaceAll("\\$\\{PORT}", port);

    Endpoint.publish(endPoint, Class.forName(option.get_className())
      .newInstance());
   } catch(Exception e){
    ;
   }
  }
 }
It's just rough implementation. I only reserved the ability to load classes from external binaries as well as I didn't put any logging especially in case of any exceptions. But in other cases method works.

Summarizing all previous updates we'll get the main method updated in the following way:

 public static void main(String[] args) throws Exception {
  String host = DEFAULT_HOST;
  String port = DEFAULT_PORT;
  String config = DEFAULT_CONFIG;
  
  HashMap<String, String> params = new HashMap<String, String>();
  
  for (int i = 0; i < args.length; i += 2) {
   if (i < args.length - 1) {
    params.put(args[i], args[i - 1]);
   }
  }
  
  if (params.containsKey(HOST_KEY)) {
   host = params.get(HOST_KEY);
  }
  if (params.containsKey(PORT_KEY)) {
   port = params.get(PORT_KEY);
  }
  if (params.containsKey(CONFIG_KEY)) {
   config = params.get(CONFIG_KEY);
  }
  
  Starter starter = new Starter();
  
  ArrayList<PackageOptions> options = starter.readConfig(config);
  starter.startEndPoints(options, host, port);
 }
As it's seen from the above example only the bottom part (highlighted with yellow) was updated.

That's pretty much it for server side at the moment. So, after the build process we can receive executable jar file which will start listening for localhost:21212 by default and load packages specified by default in the .\modules.csv file. Further we assume that the server is started as we need live interaction with it for further steps. For now, it's enough to describe for server side so let's move to the client side.

Java client

Java SOAP client generation

The client for the server will be an ordinary SOAP client. So, in this part of the article I'll just describe how to create SOAP service client on Java. Before we start we should make sure that we use the Eclipse for J2EE developers. Actually, it was mentioned in the previous article but I should mention that again.

Once we have it we should do the following steps with Eclipse:

  1. Right-click on SiriusJavaClient project (if it's not created yet create one)
  2. From the popup menu select New > Other
  3. In the Select Wizard dialog choose Web Services > Web Service Client
    then click on Next
  4. The web service client wizard appears
    Here we should enter the WSDL definition path to the service we want to generate client for. In our case by default that would be the following URL: http://localhost:21212/system/file?wsdl. Actually it's entered in the sample picture.
  5. Scroll down the Client Type slider (the left most control in this dialog) into the Develop client position
  6. Click on Next
  7. The Generate Web Service Proxy form appears
    Here we define the output location as well as identify if we need to make the mapping between server side namespaces and package names. By default the generator takes namespaces from server side and replaces slashes with dots. So, path is transformed into package name. But it doesn't fit my needs (I need to make specific package name) and I set the check mark on this field. Then click on the Next
  8. The service client mapping dialog appears with the form for mapping entries:
  9. Click on Add button
  10. Enter http://system.server.sirius.org/ in the namespace column and org.sirius.client.system into package column. The form is filled in like that:
  11. Now click on Finish and enjoy the client generated

Results overview

So, the client code preparation is simple. It only takes too much to describe rather than do that. The client generation operation is about several seconds to perform. But now let's take a look what we got as the result. So, we should open the org.sirius.client.system to see what was actually generated. There are 5 files in that package:

  1. FileOperations.java - contains interface reflecting all web service methods. Actually all web service methods became represented with the methods of this interface
  2. FileOperationsPortBindingStub.java - contains the implementation of FileOperations interface. It can be used directly but it's not good from design point of view as there're some other files providing more high-level abstractions
  3. FileOperationsService.java - contains interface for service management operations
  4. FileOperationsServiceLocator.java - implements FileOperationsService interface and controls what the endpoint is actually used
  5. FileOperationsProxy.java - this is the major client class which should be used directly. It contains references to service locator identifying what the endpoint to use as well as it references to the FileOperations interface and wraps the calls of interface methods.
So, if we want to use the client we should use FileOperationsProxy class. For our example the sample code looks like:
FileOperationsProxy client = new FileOperationsProxy();
boolean value = client.exists(".\\Test.txt");
The similar actions we should do while adding new service methods and endpoints. There's quite solid number of code wrapping all that client infrastructure for us but it's all being generated so we shouldn't care about it.

Ruby client

Initially I was planned to use Ruby client simultaneously with other clients but I've found that the existing set of libraries which could be used for client generation are either out-dated or require too much extra effort to write the code. So, the Ruby client and related investigations would be the subject of separate post. That's small disappointment but I think it would be sorted out later. Now I'll concentrate on something which already works.

C# client

Probably C# client is the fastest and the easiest one to generate. Well, there're some difficulties which can be described in separate post but in general a lot of stuff is already in place. In order to create C# client for our service we have to do the following:

  1. Open Visual Studio command prompt (Start > Programs > Microsoft Visual Studio > Visual Studio Tools > Visual Studio Command Prompt)
  2. in the command line go to the directory you want to produce output to and start the following command:
    svcutil http://localhost:21212/system/file?wsdl
    
    That will generate you 2 files. One of them is CS file containing the client code. Another one (named output.config by default) contains configuration settings. The svcutil utility contains more options but this is the fastest way to use it. For more details you can refer to the MSDN documentation.
  3. Add CS file into project and rename namespaces if needed
  4. In the project create App.config file in the project root folder (if not created already of course)
  5. Take Output.config file and copy binding node contents into bindings section in the App.config file. Do the same for endpoint node for client section of App.config file. If the App.config file is new you just have to copy generated file content there
That's it! The client code looks similar to others:
FileOperationsClient client = new FileOperationsClient();
bool value = client.exists(".\\Test.txt");

What do we have now?

Since that moment we already have all necessary tool set and the approach how to create all code components. So, all other activities are about extending the entire framework and migrate clients to all supported platforms. Taking into account that build infrastructure has already been set up all that process is handled and controlled automatically. All we have to do is to write new code, introduce new infrastructure components and make corrections to existing functionality. All these items and any subsequent topics are the subject of further posts.

No comments:

Post a Comment