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:
This example illustrates that it is possible to extend
functionality in an area that uses the original area as a tree
dependency with very little duplication of code. This is
partially achieved by thinking about our system in a different
way: rather than having a public program behave differently in
a sensitive environment, we move the main entry point into a
library. This completely eliminates the whole problem of
conditional linking or making any other decisions
conditionally upon the existence of particular build items or
upon compile-time flags that differ across different
environments. In fact, the top of the
public
tree would happily function as the
root of the forest if the sensitive
tree
and their common parent Abuild.conf
file
were not present on the system.
This example shows an approach to separating interfaces from implementations that makes it possible, without conflict, to completely replace an implementation at runtime. This is achieved by having the implementation be a dependency of the final executable and having the rest of the system depend on only the interfaces.
Although, in this example, the sensitive versions of the
consumers don't actually access any private build items from
the public version of the code, the use of the build item name
consumers.sensitive
and the
visible-to
key would make it possible for
them to do so.
Creating run-time connections between objects without creating any compile-time connections requires some additional infrastructure to be laid. In some languages and compilation environments, this can be done through use of static initializers combined with techniques to ensure that they get run even if there are no explicit references to the classes in question. To keep things both simple and portable, it is still possible to use this pattern by performing some explicit registration step prior to the invocation of the main routine.
[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.