Previous Up Next

Chapter 5  Server Applications

5.1  Introduction

It seems intrinsic to the nature of developing client-server applications that tedious, repetitive code has to be written—especially in server applications. For example, server applications are often required to execute not just “business logic” code, but also “infrastructure logic” code for every incoming request: to perform security checks, validate input parameters, log input and output parameters, and so on.

There are many competing technologies for developing client-server applications. Some of those technologies provide ways to automate commonly required, server-side “infrastructure logic”, while other technologies require programmers to manually write such code.

In this chapter, I explain how Config4* can be used to reduce the burden of writing some types of “infrastructure logic” code.

5.2  Validation Checks for Parameters

Consider a client-server application in which the client presents a form for the user to fill in, and then sends details from the filled-in form to the server for processing. The server should validate the input data before it tries to process it. Doing this can involve a lot of tedious, repetitive code, as you can see in Figure 5.1.

Figure 5.1: Manual validation of parameters can be tedious
void placeOrder(
    String    customerName,
    String[]  shippingAddress,
    Float     cost,
    String    creditCardNumber
    String    discountCode) throws ValidationException
{
    if (customerName == null) {
        throw new ValidationException("You must specify a "
                             + "value for customerName");
    }
    if (customerName.length() > 40) {
        throw new ValidationException("The value of "
                             + "customerName is too long");
    }
    ... // validation checks for the other parameters

    ... // business logic code
}

The pseudocode shown for the placeOrder() operation makes two validation checks on the customerName parameter, in both cases throwing a descriptive exception if the check fails. If all the parameters to placeOrder() require a similar level of validation checking, then the programmer will have to write and maintain several dozen lines of validation code for just a single operation. And if the server’s public interface has many operations, then it is easy to imagine the server containing many hundreds or even several thousands of lines of validation code.

A better approach is to use Config4* to define a simple schema language for describing the validation checks to be performed on parameters. A hypothetical example of this is shown in Figure 5.2.

Figure 5.2: Configuration for validation rules
placeOrder {
  customerName = ["mandatory=true", "maxLength=40"];
  shippingAddress = ["mandatory=true", "minSize=3", "maxSize=5"];
  shippingAddress-item = ["mandatory=true", "maxLength=60"];
  cost = ["mandatory=true", "min=10"];
  discountCode = ["mandatory=false", "maxLength=10"];
  creditCardNumber = ["mandatory=true", "fixedSize=16",
                      "pattern=[0-9]*"];
}

updateOrder {
  #--------
  # Most parameters are similar to those in placeOrder(), so...
  #--------
  @copyFrom "placeOrder";
  ... # now add/modify validation rules as required
}

cancelOrder {
  ...
}

The configuration file contains a scope for each operation in the public interface of the server. Within such a scope, there are variables corresponding to each parameter of the operation. The value of a variable is a list of name=value strings that specify the validation checks to be performed on the parameter when the operation is invoked. Figure 5.3 shows the outline of a Validator class that can perform the validation checks described in the configuration file shown in Figure 5.2.

Figure 5.3: A Validator class
public class Validator {
    private Configuration  cfg;
    private String         opName;

    public Validator(Configuration cfg, String opName)
    {
        this.cfg = cfg;
        this.opName = opName;
    }

    public void validate(String value, String paramName)
                                       throws ValidationException
    {
        String[] constraints = cfg.lookupList(opName, paramName);
        //--------
        // Iterate over "constraints" and throw an exception if
        // "value" violates any of them.
        //--------
        ...
    }

    public void validate(String[] value, String paramName)
                                       throws ValidationException
    { ... }

    public void validate(Float value, String paramName)
                                       throws ValidationException
    { ... }
}

The Validator class provides a validate() operation that is overloaded for parameters of different types. It might require, say, 500–1000 lines of code to implement this class, but that class needs to be implemented just once and then it can be reused to perform parameter validation for many different operations in a single server. Perhaps the class could be reused across several related projects. Thus, the effort required to implement the Validator class can be repaid easily if use of the class significantly reduces the amount of parameter-validation code required in server operations. Figure 5.4 shows the intended use of the Validator class.

Figure 5.4: Example use of the Validator class
//--------
// The following code is executed during server initialisation.
//--------
validationCfg = Configuration.create();
validationCfg.parse(Configuration.INPUT_STRING,
                     embeddedValidationConfig.getString());

//--------
// This is an example of how to perform parameter-validation
// in an operation.
//--------
void placeOrder(
    String   customerName,
    String[] shippingAddress,
    Float    cost,
    String   creditCardNumber
    String   discountCode) throws ValidationException
{
    Validator v = new Validator(validationCfg, "placeOrder");
    v.validate(customerName, "customerName");
    v.validate(shippingAddress, "shippingAddress");
    v.validate(cost, "cost");
    v.validate(creditCardNumber, "creditCardNumber");
    v.validate(discountCode, "discountCode");
    ... // business logic code
}

During initialisation, the server parses a configuration file containing the parameter-validation rules (like those shown in Figure 5.2). As the pseudocode in Figure 5.4 shows, this configuration file could be embedded in the application via use of the config2cpp or config2j utility. Then, the body of each operation in the server can validate all its parameters in a concise way: it creates a Validator object and calls validate() once for each parameter. In this way, the amount of validation code required in an operation with N parameters can be reduced from, say, 8N lines of code (as illustrated by the validation checks for customerName in Figure 5.1) to just N+1 lines of code (as illustrated in Figure 5.4).

If the public API of the server is defined in, say, CORBA IDL, and you have access to a code generation tool, for example, idlgen, then it is possible to reduce the amount of hand-written validation code even further. You can do this by writing a genie that generates a Util class containing utility methods that encapsulate the N+1 lines of validation code for each public operation of the server. By doing this, the code of placeOrder() can be reduced to that shown in Figure 5.5: parameter validation is achieved by delegating to the (generated) utility operation Util.validatePlaceOrder().

Figure 5.5: Encapsulating calls to validate() in utility functions
void placeOrder(
    String   customerName,
    String[] shippingAddress,
    Float    cost,
    String   creditCardNumber
    String   discountCode) throws ValidationException
{
    Util.validatePlaceOrder(customerName, shippingAddress, cost,
                            creditCardNumber, discountCode);
    ... // business logic code
}

To briefly summarise, in this section I have shown a two-step approach to significantly reduce the amount of parameter validation code that needs to be embedded in server-side operations.

The first step is to write a Validator class (Figure 5.3) that can perform parameter validation checks based on information in a configuration file (Figure 5.2). By doing this, the amount of validation code required in an operation with N parameters can be reduced from, say, 8N lines of code (as illustrated by the validation checks for customerName in Figure 5.1) to just N+1 lines of code (as illustrated in Figure 5.4).

The second step is to write a genie that generates a Util class containing utility methods that encapsulate the N+1 lines of validation code for each public operation of the server. By doing this, the parameter validation code embedded in each public operation can be reduced to a single line of code that delegates to a generated utility operation (Figure 5.5).

Ideally, those two steps would not need to be performed by an application developer. Instead, the Validator class and the genie would be provided by a vendor who sells tools for building client-server applications. If this were done, then an application developer might not even need to explicitly invoke Util.validate<OperationName>() from the body of a public operation. Instead, that invocation could be made from the dispatch logic generated by the vendor’s tools for building client-server applications.

5.3  Dispatch Rules

Consider the following scenario. You are in charge of a team that is developing a client-server application. You decide to split your team into two sub-teams: one to develop the client application, and the other to develop the server application. In this way, you hope to get some development work done in parallel. However, it turns out that there is a lot more work required to develop the server application than to develop the client application. The client development team reach their first milestone fairly quickly; then they start to complain that they need a server to test their client against, but the server team have not yet finished their work. Can anything be done to help the client development team make progress?

The obvious solution is for the client development team to implement a test version of the server so they can test their client against it. Even if this test server offers simplistic functionality, it will at least permit the client development team to test basic connectivity. A drawback of this approach is that the test server will have a short lifespan—it will be discarded when the real server application is mature enough to test against—so any work put into writing the test server will appear to be wasted effort. In this section, I describe an alternative approach; one in which Config4* plays a small but important role.

Let’s assume the server application exposes an interface called Foo that defines 10 operations. The server team intend to write a class, called FooImpl, that implements that interfaces. The implementation of the operations in that class will contain the “business logic” code required in the server.

My suggestion is that, along with implementing the FooImpl class, the server should contain two other classes, as I now discuss.

The FooTest class implements the operations of the Foo interface, but with “test logic” rather than with “business logic”. The test logic in an operation might be as simple as printing a message to say the operation was called and then return a dummy result. Or perhaps the test logic might be more complex. The choice is up to the client development team, since they will be writing this class and using it to test their client application.

The other class, FooDispatch, also implements the operations of the Foo interface. Each incoming request is executed by the FooDispatch class, and that class uses a simulation_rules configuration variable like that shown in Figure 5.6 to decide if it should delegate the request to the corresponding operation on the FooImpl or FooTest class.

Figure 5.6: Simulation rules
simulation_rules = [
    # wildcarded operation name   simulate?
    #--------------------------------------
     "Foo.op1",                  "true",
     "Foo.op3",                  "true",
     "*",                        "false",
];

The simulation_rules table maps a wildcarded string of the form interface.operation to a boolean value. Code to implement that mapping is provided by the shouldSimulate() operation of the FooDispatch class, which is shown in Figure 5.7.

Figure 5.7: Pseudo-code of the FooDispatch class
class FooDispatch implements Foo {
    private FooImpl  businessObj;
    private FooTest  testObj;
    private boolean  simulateOp1;
    private boolean  simulateOp2;
    ...
    private boolean  simulateOp10;

    public FooDispatch(String[] simulationRules)
    {
        businessObj = new FooImpl();
        testObj = new FooTest();
        simulateOp1 = shouldSimulate(simulationRules, "Foo.op1");
        simulateOp2 = shouldSimulate(simulationRules, "Foo.op2");
        ...
        simulateOp10 = shouldSimulate(simulationRules, "Foo.op10");
    }

    private boolean shouldSimulate(String[] rules, String opName)
    {
        for (int i = 0; i < rules.length; i += 2) {
            String pattern = rules[i + 0];
            String boolStr = rules[i + 1];
            if (Configuration.patternMatch(opName, pattern)) {
                return boolStr.equals("true");
            }
        }
        return false;
    }

    public long op1(...)
    {
        Foo targetObj = businessObj;
        if (simulateOp1) {
            targetObj = testObj;
        }
        return targetObj.op1(...);
    }
    ... // likewise for the other operations
};

The constructor of the FooDispatch class creates instances of both FooImpl and FooTest, and stores those as instance variables. The constructor then calls shouldSimulate() to decide whether each operation should delegate to the corresponding operation on the FooImpl or FooTest object. For efficiency, these decisions are cached in boolean instance variables, rather than being recalculated for each invocation. The implementation of an operation uses a simple if-then-else statement to delegate the request to the “test” or “business” object.

Some readers may assume the approach outlined above is burdensome because it requires programmers to write three classes—FooImpl, FooTest and FooDispatch—for the server instead of just one (FooImpl). Doesn’t doing this triple the effort required to write the server? Actually, no. The server development team have to write FooImpl, so that does not count as an extra burden. Likewise, if the client development team want a “test server” to test their client against while waiting for the “real” server to be mature enough to test against, then they would have to implement (something similar to) FooTest. As such, the need to write FooTest does not count as an extra burden either. The only extra burden is in writing FooDispatch, and that class is trivial enough to not be much of a burden.1

Using the above approach, project development can proceed as follows.

There are many competing technologies available for building client-server applications. It is common for these technologies to provide a class that delegates an incoming request to the target object. Such classes always provide some “added value” when performing the delegation. For example, the class might unmarshal an incoming request before dispatching (that is, delegating) it. Or the class might perform auditing, security checks, or manage transaction boundaries when dispatching an incoming request. The “should I dispatch this request to the ‘business logic’ or ‘test logic’ implementation of an operation?” functionality could be designed into the dispatch classes of future technologies for building client-server applications. If that ever happens, then it will remove the (albeit small) burden from application developers of writing code such as the FooDispatch class shown in Figure 5.7.

5.4  Summary

It is common for server applications to contain not just “business logic” but also “infrastructure” code. Some technologies for building client-server applications can reduce the burden of implementing some types of infrastructure task. For example, a client-server technology might automate security checks or transaction boundaries.

In this chapter, I have discussed two other types of infrastructure tasks that are unlikely to be automated by current client-server technologies: the validation of input parameters, and deciding whether an incoming request should be dispatched to the real “business logic” implementation of an operation or to a “test” implementation. I have explained how Config4* can simplify the implementation of both those tasks.


1
If you are building your client-server application using the Orbix implementation of CORBA, then you could write an idlgen genie to generate FooDispatch and an initial implementation of FooTest.

Previous Up Next