The org.bzdev.obnaming package
The following topics are described below:- object namers and named objects.
- factories for named objects.
- examples for object namers.
- examples for named-object factories.
Object namers and named objects
It is sometimes useful to be able to look up objects by name, and it is also useful to be able to find the names of those objects, perhaps filtered by their class or one of their superclasses. This package provides such a service. There are default implementations that can simply be subclassed and additionally there are annotations recognized by an annotation processor that will generate helper classes to allow a named-object service to be spiced into an existing class hierarchy. The objects handled are ones in the current process. By contrast, the class java.rmi.Naming provides a facility for referencing remote objects, and the javax.naming package allows various devices to be looked up by name (e.g, printers).The service consists of an object namer and a named object that is the common superclass of all objects recognized by the object namer. There are two cases:
- The object namer and named object base classes do not need to have a superclass (other than Object). In this case "default" classes can be used.
- The object namer and named object base classes both need specific
superclasses. In this case, Java annotations specify how to
build helper classes for an object namer and named object, and
an annotation processor creates these classes at compile time.
When an object namer is a subclass of ScriptingContext, the
object namer will by default be able to use a scripting language
to configure factories, using a convenient syntax. This is
described in the
NamedObjectFactory
documentation.
The figure above indicates that named objects are registered with object namers (via operations that occur when constructors are called), and that the helpers, when used, are specified by annotations.
Factories for named objects
In addition, this package provides a framework for creating factories to configure named objects. This framework provides common methods for setting values, creating objects, and obtaining information about the factory. The following figure shows the relationship between various classes related to factories:
The class NamedObjectFactory
is a generic class. A subclass will use its own class name as the
first type parameter, followed by the class names of the object namer,
the matching named object class, and finally the specific class whose
instance the factory will create. Each value that can be set is
referenced by name, and that name and some other information is
provided by the Parm
class. When the
constructor for a Parm
instance is called,
it requires an instance of ParmParser
as one of its arguments. This instance is typically an anonymous
class: ParmParser
defines methods for parsing various types of objects
and these will throw an UnsupportedOperationException unless
overridden. The ones that are overridden are used to store the values
that were parsed. These values are primitive types (int, long,
double, and boolean), strings, and random variables. If the desired
type is that of a named object, the object is referenced by name and
the object namer is used to find the object itself.
Examples for object namers
As an example, if an object namer does not have a superclass, one can simply extend the classesDefaultObjectNamer
and
DefaultNamedObject
. In the
following code, the object namer will be called Namer and the common
superclass for named objects will be OurNamedObject:
public class Namer extends DefaultObjectNamer<OurNamedObject> {
public Namer() {
super(OurNamedObject.class);
}
}
abstract public class OurNamedObject
extends DefaultNamedObject<OurNamedObject>
{
...
public OurNamedObject(Namer namer, String name, boolean intern) {
super(namer, name, intern);
}
}
A factory class can also be defined:
abstract public class OurNamedObjectFactory<OBJ extends OurNamedObject>
extends DefaultNOFactory<Namer,OurNamedObject,OBJ>
{
protected OurNamedObjectFactory(Namer namer) {
super(namer);
}
}
For the class
DefaultNOFactory
,
there are three type parameters that must be provided: the type of the
object namer, the type of the base class for all named objects, and a
type parameter OBJ that is constrained so that it must extend the
named object class. The example above defining the
class OurNamedObjectFactory
does this. Subclasses
of OurNamedObjectFactory
will then have a single type
parameter, the name of the corresponding class.
Additional classes can be defined for specific subclasses of named objects:
public class Foo extends OurNamedObject {
public Foo(Namer namer, String name) {
super(namer, name, true);
}
...
}
public class Foo1 extends Foo {
...
}
The following code then creates an object namer and implicitly
adds two instances of Foo1 to it (which is handled by the constructor,
specifically the constructor for DefaultNamedObject):
Namer namer = new Namer();
Foo1 x = new Foo1(namer, "x");
Foo2 y = new Foo1(namer, "y");
If an object namer needs to extend a superclass, this can be done
using annotations. The annotations define a helper class that
provides the namer implementation and arranges for this helper
class to extend some other class. For example, suppose we want to
create a class TestNamer that extends a class named NumberStore,
which has two constructors, one of which may throw an
IllegalArgumentException. We can use an ObjectNamer annotation as
follows:
public class NumberStore<T extends Number> {
...
public NumberStore(int initialCapacity) {
...
}
public NumberStore(int initialCapacity, float loadFactor) {
...
}
}
@ObjectNamer(helperClass = "NHelper",
helperSuperclass = "NumberStore",
helperSuperclassTypeParms = "<Integer>"
helperSuperclassConstrTypes =
{
@ObjectNamer.ConstrTypes({"int"}),
@ObjectNamer.ConstrTypes(
values = {"int", "float"},
exceptions = {"IllegalArgumentException"}),
objectClass = "TestObject",
objectHelperClass = "OHelper")
public class TestNamer extends NHelper
implements ObjectNamerOps<TestObject>
{
...
public TestNamer(int initialCapacity) {
super(initialCapacity);
...
}
public TestNamer(int initialCapacity, float loadFactor)
throws IllegalArgumentException
{
super(initialCapacity, loadFactor);
...
}
}
The object namer will be configures so that the common superclass
of all named objects is TestObject. The annotation describes the
constructors of the helper's superclass (NumberStore), which has
similar constructors. TestNamer must extend the helper class (NHelper)
and may implement ObjectNamerOps
@NamedObject(helperClass="OHelper",
helperSuperclass = "OurObject",
helperSuperclassTypeParms = "<Integer, Double>",
helperSuperclassConstrTypes = {
@NamedObject.ConstrTypes({}),
@NamedObject.ConstrTypes(
value = {"int", "float"},
exceptions = {"NegativeArraySizeException"}),
namerHelperClass="NHelper",
namerClass="TestNamer")
public class TestObject extends OHelper implements NamedObjectOps {
...
public TestObject(TestNamer namer, String name, boolean intern)
throws IllegalArgumentException
{
super(namer, name, intern);
}
public TestObject(TestNamer namer, String name, boolean intern,
int ind, float scale)
throws IllegalArgumentException, NegativeArraySizeException
{
super(namer, name, intern, ind, scale);
}
...
}
Other named object classes used by TestNamer must extend TestObject.
The namerClass method is not actually needed, but is required for
documentation reasons. It makes it easier to find the corresponding
object namer.
Examples for named-object factories
Factory classes will typically be arranged in a specific class hierarchy, a convention that is useful but not mandatory. For example, suppose (as described above) one creates an object namer together with a named-object class, UserNamedObject, and two subclasses of UserNamedObject, Foo and Bar. Also assume that Bar is a subclass of Foo. One will typically create factories for each of these classes: a set of abstract factories that are paired with these classes and additional factories to create specific objects. This is illustrated by the following UML diagram:
In this example, the non-abstract factory classes will create the actual objects, while the abstract factories will perform initializations appropriate for the class each abstract factory configures.
When coded directly, the documentation for
NamedObjectFactory
and
Parm
provides the necessary details.
If annotations are used, suppose we have already defined a named object
class TestObject and an object named TestNamer, and that we want to create
a factory for a TestObject2, which is a subclass of TestObject. Assume
TestObject2 is defined as follows:
This class contains three variables. Two are integer-valued and one has a value that is an IntegerRandomVariable.import org.bzdev.math.rv.IntegerRandomVariable; public class TestObject2 extends TestObject { private int value1; public void setValue1(int x) { value1 = x; } public int getValue1() {return value1;} private int value2; public void setValue2(int x) { value2 = x; } public int getValue2() {return value2;} private IntegerRandomVariable value3; public void setValue3(IntegerRandomVariable rv) { value3 = rv; } public int getValue3() { return value3.next(); } TestObject2(TestNamer namer, String name, boolean intern) { super(namer, name, intern); } }
A factory to create this class can be written as follows:
Theimport org.bzdev.obnaming.*; import org.bzdev.bzdev.math.rv.*; @FactoryParmManager("TestObject2ParmManager") public class TestObject2Factory extends NamedObjectFactory <TestObject2Factory, TestNamer, TestObject, TestObject2> { @PrimitiveParm("value1") int value1 = 0; @PrimitiveParm(value="value2", rvmode=true) IntegerRandomVariable value2 = new UniformIntegerRV(0, 10); @PrimitiveParm(value="value3", rvmode = true) IntegerRandomVariableRV
value3 = new FixedIntegerRVRV(new UniformIntegerRV(0, 10)); TestObject2ParmManager pm; public TestObject2Factory(TestNamer namer) { super(namer); pm = new TestObject2ParmManager(this); initParms(pm, TestObject2Factory.class); } public void clear() { pm.setDefaults(this); super.clear(); } protected TestObject2 newObject(String name) { return new TestObject2(getObjectNamer(), name, willIntern()); } protected void initObject(TestObject2 object) { object.setValue1(value1); object.setValue2(value2.next()); object.setValue3(value3.next()); } }
PrimitiveParm
annotations
give the name of the parameter. The type of the parameter is
determined by the type of the field being annotated. When rvmode() is
false (the default), the type of the parameter is the type of the
field; otherwise it is the type of the values a random number generate
produces by calling the random generator's next() method. The
corresponding Parm
is automatically
initialized. The factory class' constructor is expected to create
a ParmManager
object when
annotations are used, and use
the ParmManager
to initialize the
factory's parameters. The method named clear() should use the
ParmManager
ParmManager.setDefaults(org.bzdev.obnaming.NamedObjectFactory)
method to
restore parameters to their default values. Finally the factory must
implement methods such as newObject and initObject to actually create
objects and initialize them. The details are in the documentation for
NamedObjectFactory
Parameters
that are not readily handled by
a ParmManager
can be provided
directly. In either case, a method named initParms is used to add one
or more Parm instances to the factory.
Regardless of whether annotation processing is used, the fully qualified class name of factories that are not abstract classes should appear in a file named META-INF/services/org.bzdev.obnaming.NamedObjectFactory and included in the same jar file as the factory, and in a module-info.java file when modules are used, where the module-info.java file contains the statement that matches the pattern
provides org.bzdev.obnaming.NamedObjectFactory with
FACTORY[, FACTORY]*;
where each instance of FACTORY is replaced with the fully-qualified
path name of a factory class. For example,
Using both forms allows the JAR file to be used with and without modules.module com.foo { exports com.foo.pkg; requires org.bzde.base; requires org.bzdev.obnaming; provides org.bzdev.obnaming.NamedObjectFactory with com.foo.pkg.Factory1, com.foo.pkg.Factory2, com.foo.pkg.Factory3; }
Advice regarding compilation is provided in the BZDev library description.