25.2. Mixed Classification Example

This example shows a sample implementation of how one might solve certain development problems in a mixed classification development environment. To avoid any potential confusion, we'll call our two classification levels “public” and “sensitive.”. These could correspond to different levels of protection of information and could apply to any environment in which people have to be granted special access in order to use parts of a system. The code is divided into two separate build trees: public and sensitive. The public tree's root Abuild.conf file is here:

mixed-classification/public/Abuild.conf

tree-name: public
child-dirs: consumers executable processor

The sensitive tree's root Abuild.conf is here:

mixed-classification/sensitive/Abuild.conf

tree-name: sensitive
tree-deps: public
child-dirs: consumers executable processor

If you were in an environment where the sensitive tree were not present, the root of the public tree could be the root of the forest. In an environment where both trees are available, they can be both be made known to abuild by supplying a common parent Abuild.conf that lists them both as children. Here is the common parent:

mixed-classification/Abuild.conf

child-dirs: public sensitive

Note that connecting these two trees together is achieved without modifying either tree and without having either tree know the location of the other.

In this example, we'll demonstrate a very simple message processing system. When a message is received, it is processed by a message processor and then dispatched to a series of message consumers. Our system allows message consumers to be registered with a special message consumer table. Each message consumer is passed a reference to a message processor. Then, for each message, each consumer processes the message with the message processor and then does whatever it needs to do with the results.

In the public version of the system, we have some message consumers and a message processor. In the sensitive version of the system, we want access to the public consumers, but we also want to register some additional consumers that are only allowed to work in the sensitive environment. In addition, we want to be able to replace the message processor with a different implementation such that even the public consumers can operate on the messages after processing them with the sensitive processor. Furthermore, we wish to be able to achieve these goals with as little code duplication as possible and without losing the ability to run the public version of the system even when operating in the sensitive environment as this may be important for testing the system. We also wish to protect ourselves against ever accidentally creating a dependency from a public implementation to a sensitive implementation of any part of the system.

In our sample implementation, each message is an integer, and the message processor receives the integer as input and returns a string. Rather than having “messages” actually be “received”, we just accept integers on the command line and pass them through the process/consume loop in the system.

This example may be found in doc/example/mixed-classification. The public code is in the public subdirectory, and the sensitive code is in the sensitive subdirectory. The example is implemented in Java, but there is nothing about it that wouldn't work the same way in C or C++. We will study the public area first.

In this example, we have a library of consumers and an executable program that calls each registered consumer the numbers passed in on the command line. The consumers each call the processor function through an interface, an instance of which is passed to the consumer with each message. The public version of consumer library includes two consumers. In order for us to allow the sensitive version to add two more consumers and provide a new processor that completely replaces the one defined in the public version, the processor function's interface and implementation are separated as we will describe below.

There are several things to note about the dependencies and directory layout. First, observe that the Java Processor class defined in the processor build item implements a Java interface (not to be confused with an abuild interface) that is actually defined in the consumers.interface build item in the consumers/interface directory. Here is the interface from the consumers.interface build itme:

mixed-classification/public/consumers/interface/src/java/com/example/consumers/ProcessorInterface.java

package com.example.consumers;

public interface ProcessorInterface
{
    public String process(int n);
}

Here is its implementation from the processor build item:

mixed-classification/public/processor/src/java/com/example/processor/Processor.java

package com.example.processor;

import com.example.consumers.ProcessorInterface;

public class Processor implements ProcessorInterface
{
    public String process(int n)
    {
        return "public processor: n = " + n;
    }
}

This means that the processor build item depends on consumers and the consumers build items do not depend on processor. This helps enforce that the implementation of the processor function can never be a dependency of the consumers (as that would create a circular dependency), thus allowing it to remain completely separate from the consumer implementations.

mixed-classification/public/processor/Abuild.conf

name: processor
platform-types: java
deps: consumers

mixed-classification/public/consumers/Abuild.conf

name: consumers
child-dirs: interface c1 c2
deps: consumers.c1 consumers.c2

The consumers themselves accept a ProcessorInterface instance as a parameter, as you can see from the consumer interface:

mixed-classification/public/consumers/interface/src/java/com/example/consumers/Consumer.java

package com.example.consumers;

public interface Consumer
{
    public void register();
    public void consume(ProcessorInterface processor, int number);
}

Next we will study the executable. If you look at the executable build item, you will observe that it depends on processor and executable.entry:

mixed-classification/public/executable/Abuild.conf

name: executable
platform-types: java
child-dirs: entry
deps: executable.entry processor

Its Main.java is very minimal: it just invokes Entry.runExecutable passing to it an instantiated Processor object and whatever arguments were passed to main:

mixed-classification/public/executable/src/java/com/example/executable/Main.java

package com.example.executable;

import com.example.processor.Processor;
import com.example.executable.entry.Entry;

public class Main
{
    public static void main(String[] args)
    {
        Entry.runExecutable(new Processor(), args);
    }
}

It is important to keep this main routine minimal because we will have to have a separate main in the sensitive area as that is the only way we can have the sensitive version of the code register sensitive consumers prior to calling main. [53] If this were C++, the inclusion of the sensitive consumers would be achieved through linking with additional libraries. In Java, it is achieved by adding additional JAR files to the classpath. In either case, with abuild, it is achieved by simply adding additional dependencies to the build item. We will see this in more depth when we look at the sensitive version of the code.

Turning our attention to the public executable.entry build item, we can see that our Entry.java file has a static initializer that registers our two consumers, C1 and C2: [54]

mixed-classification/public/executable/entry/src/java/com/example/executable/entry/Entry.java

package com.example.executable.entry;

import com.example.consumers.ProcessorInterface;
import com.example.consumers.Consumer;
import com.example.consumers.ConsumerTable;
import com.example.consumers.c1.C1;
import com.example.consumers.c2.C2;

public class Entry
{
    static
    {
        new C1().register();
        new C2().register();
    }

    public static void runExecutable(ProcessorInterface processor,
                                     String args[])
    {
        for (String arg: args)
        {
            int n = 0;
            try
            {
                n = Integer.parseInt(arg);
            }
            catch (NumberFormatException e)
            {
                System.err.println("bad number " + args[0]);
                System.exit(2);
            }

            for (Consumer c: ConsumerTable.getConsumers())
            {
                c.consume(processor, n);
            }
        }
    }
}

Even though no place else in the code has to know about C1 and C2 specifically, we do have to register them explicitly with the table of consumers so that the rest of the application can use them. The main runExecutable function parses the command-line arguments and then passes each one along with the Processor object to each consumer in turn. Adding additional consumers would entail just making sure that they are registered. Observe in the source to one of the consumers how we register the consumer in the consumer table:

mixed-classification/public/consumers/c1/src/java/com/example/consumers/c1/C1.java

package com.example.consumers.c1;

import com.example.consumers.ProcessorInterface;
import com.example.consumers.Consumer;
import com.example.consumers.ConsumerTable;

public class C1 implements Consumer
{
    public void register()
    {
        ConsumerTable.registerConsumer(this);
    }

    public void consume(ProcessorInterface processor, int n)
    {
        System.out.println("public C1: " + processor.process(n));
    }
}

The consumer table is a simple vector of consumers:

mixed-classification/public/consumers/interface/src/java/com/example/consumers/ConsumerTable.java

package com.example.consumers;

import java.util.Vector;

public class ConsumerTable
{
    static private Vector<Consumer> consumers = new Vector<Consumer>();

    static public void registerConsumer(Consumer h)
    {
        consumers.add(h);
    }

    static public Vector<Consumer> getConsumers()
    {
        return consumers;
    }
}

Now we will look at the sensitive version of the code. We have the same three subdirectories in sensitive as in public. In our consumers directory, we define new consumers C3 and C4. They are essentially identical to the public consumers C1 and C2. The processor directory defines the sensitive version of the Processor class:

mixed-classification/sensitive/processor/src/java/com/example/processor/Processor.java

package com.example.processor;

import com.example.consumers.ProcessorInterface;

public class Processor implements ProcessorInterface
{
    public String process(int n)
    {
        return "sensitive processor: n*n = " + n*n;
    }
}

Note that the class name is the same as in the public version, which means that the public and sensitive versions cannot be used simultaneously in the same executable. Also observe that the name of the build item is actually processor.sensitive, to make it different from processor, and that the build item sets its visibility to * so that it can be a dependency of the sensitive version of the executable:

mixed-classification/sensitive/processor/Abuild.conf

name: processor.sensitive
platform-types: java
visible-to: *
deps: consumers

In this particular example, there's no reason that we couldn't have given the build item a public name as there are no subcomponents of the public processor build item that the sensitive one needs. In a real situation, perhaps this would be the real processor build item and the public one would be called something like processor-stub. In any case, all abuild cares about is that the build items have different names.

Looking at the sensitive version of the executable, we can observe that there is no separate sensitive version of the Entry class. This effectively means that we are using the public main routine even though we have sensitive consumers. This provides an example of how to implement the case that people might be inclined to implement by having conditional inclusion of sensitive JAR files or conditional linking of sensitive libraries. Since abuild doesn't support doing anything conditionally upon the existence of a build item or even testing for the existence of a build item, this provides an alternative approach. This approach is actually better because it enables the public version of the system to run intact even in the sensitive environment. After all, if the system automatically used the sensitive handlers whenever they were potentially available, we couldn't run the public version of the test suite in the sensitive environment. This would make it too easy, while working in the sensitive environment, to make modifications to the system that break the system in a way that would only be visible in the public version. By pushing what would have been main into a library, we can avoid duplicating the code. If you look at the actual build item and code in the executable directory, you will see that the build item is called executable.sensitive and that it depends on consumers.sensitive and processor.sensitive, both of which have made themselves visible to * in their respective Abuild.conf files. We saw processor.sensitive's Abuild.conf file above. Here is consumers.sensitive's Abuild.conf:

mixed-classification/sensitive/consumers/Abuild.conf

name: consumers.sensitive
visible-to: *
child-dirs: c3 c4
deps: consumers.c3 consumers.c4

Also observe that executable.sensitive depends on executable.entry just like the public version of the executable did:

mixed-classification/sensitive/executable/Abuild.conf

name: executable.sensitive
platform-types: java
deps: executable.entry consumers.sensitive processor.sensitive

Looking at the sensitive executable's Main.java, we can see that it is essentially identical to the public version except that it registers some additional consumers that were not available in the public version:

mixed-classification/sensitive/executable/src/java/com/example/executable/Main.java

package com.example.executable;

import com.example.processor.Processor;
import com.example.consumers.c3.C3;
import com.example.consumers.c4.C4;
import com.example.executable.entry.Entry;

public class Main
{
    static
    {
        new C3().register();
        new C4().register();
    }

    public static void main(String[] args)
    {
        Entry.runExecutable(new Processor(), args);
    }
}

Here are some key points to take away from this:



[53] Well, it's not really the only way. You could also do something like having a RegisterConsumers object that both versions of the code would implement and provide in separate jar files much as we do with the Processor object. One reason for doing it this way, though, is that it makes the example easier to map to languages with static linkage. In other words, we're trying to avoid doing anything that would only work in Java to make the example as illustrative as possible. This is, after all, not a Java tutorial.

[54] If this were a C++ program and portability to Windows were not required, we could omit this static initializer block entirely and put the static initializers in C1 and C2 themselves as long as we used the whole archive flag (see Section 26.1, “Whole Library Example”) with those libraries. As with C++, however, there is no clean and portable way to force static initializers to run in a class before the class is loaded.