We are now going to begin development of a GUI for managing the properties of lines: length, stroke (weight), color and draw (a boolean property that says whether a line is to be drawn at all). Our first task will be to establish a facility we can use to encapsulate and manage all the different properties of lines (in the context of our Cartesian plane application). This facility will take the form of a set of classes, the abstract class LinePropertySet and four concrete subclasses. This page is devoted to creating that facility.
GitHub repository: Cartesian Plane Part 16
Previous lesson: Cartesian Plane Lesson 16 Page 6: Custom Controls, Testing
Class LinePropertySet
We’ll start by defining four different categories of lines used in our Cartesian plane application. If you look in CPConstants you’ll see we have already started the process. The categories are:
- Axes (see CPConstants.AXIS_…);
- Major Tics (see CPConstants.TIC_MAJOR_…);
- Minor Tics (see CPConstants.TIC_MINOR_…); and
- Grid Lines (see CPConstants.GRID_LINE_…)
We have five properties associated with each category; they are:
- Stroke;
- Length;
- Spacing;
- Color; and
- The boolean property Draw, which indicates whether a particular category of lines should appear in our depiction of the Cartesian plane.
Note that not every category of line supports all properties; axes, for example, do not have a spacing property; grid lines do not have a length property. Nevertheless each category will have a setting for each property, plus an additional flag indicating whether a particular property can be set for that category. So for each property our abstract superclass LinePropertySet will have to know:
- The value of the property;
- Whether or not a given category supports the property; and
- For each supported property, the name of the property as recognized by the PropertyManager.
For example, for the major tics category, we must know:
| Property | Supported | Property Manager Connection |
|---|---|---|
| Length | Yes | CPConstants.TIC_MAJOR_LEN_PN |
| Spacing | Yes | CPConstants.TIC_MAJOR_MPU_PN |
| Stroke | Yes | CPConstants.TIC_MAJOR_WEIGHT_PN |
| Color | Yes | CPConstants.TIC_MAJOR_COLOR_PN |
| Draw | Yes | CPConstants.TIC_MAJOR_DRAW_PN |
And for the axes category:
| Property | Supported | Property Manager Connection |
|---|---|---|
| Length | No | — |
| Spacing | No | — |
| Stroke | Yes | CPConstants.AXIS_WEIGHT_PN |
| Color | Yes | CPConstants.AXIS_COLOR_PN |
| Draw | No | — |
The configuration data for each category will be supplied by a concrete subclass of LinePropertySet: LinePropertySetAxes, LinePropertySetTicMajor, LinePropertySetTicMinor and LinePropertySetGridLines. The subclasses are responsible only for configuration data; the superclass will do most of the work.
LinePropertySet has the following field declarations (annotations follow):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public abstract class LinePropertySet { private final PropertyManager pMgr = PropertyManager.INSTANCE; private final String drawProperty; private final String strokeProperty; private final String lengthProperty; private final String spacingProperty; private final String colorProperty; private Optional<Boolean> draw; private Optional<Float> stroke; private Optional<Float> length; private Optional<Float> spacing; private Optional<Color> color; // ... } |
- Line 3: Convenient declaration for PropertyManager instance; allows us to write, for example, pmgr.setProperty( … ) instead of PropertyManager.Instance.setProperty( … ).
- Line 4: For a category of lines, the name corresponding to the draw property, for example CPConstants.GRID_LINE_DRAW_PN.
- Line 5: For a category of lines, the name corresponding to the stroke property, for example CPConstants.GRID_LINE_WEIGHT_PN.
- Line 6: For a category of lines, the name corresponding to the length property, for example CPConstants.TIC_MAJOR_LEN_PN.
- Line 7: For a category of lines, the name corresponding to the spacing property, for example CPConstants.TIC_MAJOR_MPU_PN.
- Line 8: For a category of lines, the name corresponding to the color property, for example CPConstants.TIC_MAJOR_COLOR_PN.
- Line 10: Optional encapsulating the Boolean draw property; empty if not supported by a given category.
- Line 11: Optional encapsulating the stroke property; empty if not supported by a given category.
- Line 12: Optional encapsulating the length property; empty if not supported by a given category.
- Line 13: Optional encapsulating the spacing property; empty if not supported by a given category.
- Line 14: Optional encapsulating the color property; empty if not supported by a given category.
A subclass supplies the superclass with the property names via the constructor; if a category does not support a particular property, it passes an empty string for that property name. The LinePropertySet constructor looks like this:
public LinePropertySet(
String drawProperty,
String strokeProperty,
String lengthProperty,
String spacingProperty,
String colorProperty
)
{
super();
this.drawProperty = drawProperty;
this.strokeProperty = strokeProperty;
this.lengthProperty = lengthProperty;
this.spacingProperty = spacingProperty;
this.colorProperty = colorProperty;
reset();
}
Note: the reset method gets a value for each property name from the Property Manager; see below.
And the code for the LinePropertySetGridLines class, in its entirety, looks like this:
public class LinePropertySetGridLines extends LinePropertySet
{
public LinePropertySetGridLines()
{
super(
CPConstants.GRID_LINE_DRAW_PN,
CPConstants.GRID_LINE_WEIGHT_PN,
"", // grid lines don't have a length
CPConstants.GRID_LINE_LPU_PN,
CPConstants.GRID_LINE_COLOR_PN
);
}
}
For each encapsulated property the LinePropertySet class has a getter that begins with has, and returns a Boolean indicating whether or not the property is supported by the encapsulated category of lines. Here, for example, is the has method for the spacing property:
public boolean hasSpacing()
{
return spacing.isPresent();
}
For each property, the LinePropertySet class has a getter and a setter. If a property is not supported by the encapsulated class of lines, the getter ignores the input, and the setter returns a default value. Here are the getter and setter for the stroke property:
public void setStroke( float stroke )
{
if ( hasStroke() )
this.stroke = Optional.of( stroke );
}
public double getStroke()
{
return stroke.orElse( -1f );
}
public boolean hasStroke()
{
return stroke.isPresent();
}
For convenience, the class has the following three helper methods for obtaining a Boolean, color or float value from the PropertyManager:
private Optional<Float> asFloat( String propertyName )
{
Float val = pMgr.asFloat( propertyName );
Optional<Float> optional = Optional.ofNullable( val );
return optional;
}
private Optional<Color> asColor( String propertyName )
{
Color val = pMgr.asColor( propertyName );
Optional<Color> optional = Optional.ofNullable( val );
return optional;
}
private Optional<Boolean> asBoolean( String propertyName )
{
Boolean val = pMgr.asBoolean( propertyName );
Optional<Boolean> optional = Optional.ofNullable( val );
return optional;
}
Note: the above code uses a method we haven’t see before. Optional.ofNullable( T val ) returns an Optional encapsulating val only if val is non-null; if val is null an empty Optional is returned.
The class has an apply method that can be called when the user wishes to save the currently stored values to the PropertyManager:
public void apply()
{
if ( hasDraw() )
pMgr.setProperty( drawProperty, draw.get() );
if ( hasStroke() )
pMgr.setProperty( strokeProperty, stroke.get() );
if ( hasLength() )
pMgr.setProperty( lengthProperty, length.get() );
if ( hasSpacing() )
pMgr.setProperty( spacingProperty, spacing.get() );
if ( hasColor() )
pMgr.setProperty( colorProperty, color.get() );
}
And it has a reset method which will discard any changes to the current values, and reset them from values maintained by the PropertyManager; recall that the as… methods return empty Optionals if the named property can’t be found:
public void reset()
{
draw = asBoolean( drawProperty );
stroke = asFloat( strokeProperty );
length = asFloat( lengthProperty );
spacing = asFloat( spacingProperty );
color = asColor( colorProperty );
}
LinePropertySet Subclasses
As previously noted, there are four concrete subclasses of LinePropertySet, each of which configures an instance of the superclass for a particular category of lines. We’ve already seen LinePropertySetGridLines, above. The code for LinePropertySetAxes, LinePropertySetTicMajor and LinePropertySetTicMinor are directly analogous, and can be found in the GitHub repository.
Testing LinePropertySet
So we are about to build a GUI, a moderately sophisticated one, and five classes based on LinePropertySet, also not entirely straightforward. We’re about to marry the two together, and integrate them with the Cartesian plane PropertyManager; once again not a simple facility. As we begin to code the GUI interface, all sorts of things can go wrong. I propose that we would be foolish to continue with our production code until we’re confident that the LinePropertySet classes are fully tested.
Let’s take a look at one scenario for testing LinePropertySetGridLines:
- Instantiate a LinePropertySetGridLines object.
- Verify that the draw, length, stroke, spacing and color properties correctly reflect the values stored by the PropertyManager.
- Change all the properties in the LinePropertySetGridLines object to new values.
- Verify that the object’s getters return the new values.
- Call the object’s reset method.
- Verify that all properties revert to their original values, the ones maintained by the PropertyManager.
- Change all the properties in the LinePropertySetGridLines object to new values.
- Call the object’s apply method.
- Verify that the new property values are properly stored by the PropertyManager.
One of the challenges in this test is to make sure that the values initially stored by the PropertyManager are well known and unique; we don’t want initial values for length and spacing to be the same, otherwise we might accidentally connect the LinePropertySet length property to the PropertyManager weight property, and our test wouldn’t catch it.
Configuring property values in the PropertyManager is a problem we’ve tackled before, and we came up with a couple of different solutions for it. This time let’s try yet another approach. Recall that property values can be set via command line arguments, and that such values wind up as strings in the System properties facility. We can simulate that by setting property values directly in the System properties, for example: System.setProperty(
CPConstants.TIC_MAJOR_COLOR_PN, "0x00FF00FF"
);
Fortunately we don’t have to perform this initialization for every property known to the PropertyManager, just those with property names that begin with TIC_MINOR_, TIC_MAJOR_, GRID_LINE_ and AXIS_. We’ll have a utility class in the …test_utils package under the test source tree that does this for us; the code follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | import static com.acmemail.judah.cartesian_plane.CPConstants.*; public class LinePropertySetInitializer { // TIC_MAJOR public static final String TIC_MAJOR_COLOR = "100"; public static final String TIC_MAJOR_WEIGHT = "1.1"; public static final String TIC_MAJOR_LEN = "1.2"; public static final String TIC_MAJOR_MPU = "1.3"; public static final String TIC_MAJOR_DRAW = "true"; // TIC_MINOR public static final String TIC_MINOR_COLOR = "110"; public static final String TIC_MINOR_WEIGHT = "2.1"; public static final String TIC_MINOR_LEN = "2.2"; public static final String TIC_MINOR_MPU = "2.3"; public static final String TIC_MINOR_DRAW = "true"; // GRID_LINE public static final String GRID_LINE_WEIGHT = "3.0"; public static final String GRID_LINE_LPU = "3.1"; public static final String GRID_LINE_COLOR = "120"; public static final String GRID_LINE_DRAW = "true"; // AXIS public static final String AXIS_COLOR = "130"; public static final String AXIS_WEIGHT = "4.1"; public static void initProperties() { System.setProperty( TIC_MAJOR_COLOR_PN, TIC_MAJOR_COLOR ); System.setProperty( TIC_MAJOR_WEIGHT_PN, TIC_MAJOR_WEIGHT ); System.setProperty( TIC_MAJOR_LEN_PN, TIC_MAJOR_LEN ); System.setProperty( TIC_MAJOR_MPU_PN, TIC_MAJOR_MPU ); System.setProperty( TIC_MAJOR_DRAW_PN, TIC_MAJOR_DRAW ); System.setProperty( TIC_MINOR_COLOR_PN, TIC_MINOR_COLOR ); System.setProperty( TIC_MINOR_WEIGHT_PN, TIC_MINOR_WEIGHT ); System.setProperty( TIC_MINOR_LEN_PN, TIC_MINOR_LEN ); System.setProperty( TIC_MINOR_MPU_PN, TIC_MINOR_MPU ); System.setProperty( TIC_MINOR_DRAW_PN, TIC_MINOR_DRAW ); System.setProperty( GRID_LINE_WEIGHT_PN, GRID_LINE_WEIGHT ); System.setProperty( GRID_LINE_LPU_PN, GRID_LINE_LPU ); System.setProperty( GRID_LINE_COLOR_PN, GRID_LINE_COLOR ); System.setProperty( GRID_LINE_DRAW_PN, GRID_LINE_DRAW ); System.setProperty( AXIS_COLOR_PN, AXIS_COLOR ); System.setProperty( AXIS_WEIGHT_PN, AXIS_WEIGHT ); } } |
Note: I wrote a program to generate the above source code. While this had the advantage of making sure we didn’t miss any of the target property names, I can’t really say that it was worth the trouble (it was fun, though!). For a task like this you should be comfortable typing all the code out by hand. If you’re curious about the generator program, it’s LinePropertySetInitializerGenerator in the …test_utils package and can be found in the GitHub repository. There’s also a JUnit test for it.
For our test we will have one abstract JUnit class, LinePropertySetTest, that does the lion’s share of the work, and four concrete subclasses that provide configuration information:
- LinePropertySetAxesTest
- LinePropertySetTicMajorTest
- LinePropertySetTicMinorTest
- LinePropertySetGridLinesTest
The configuration information includes the property names for the draw, stroke, length, spacing and color properties, and a Supplier for instantiating the appropriate subclass of LinePropertySet. Here is the code for LinePropertySetTicMajorTest in its entirety.
class LinePropertySetTicMajorTest extends LinePropertySetTest
{
public LinePropertySetTicMajorTest()
{
super(
CPConstants.TIC_MAJOR_DRAW_PN,
CPConstants.TIC_MAJOR_WEIGHT_PN,
CPConstants.TIC_MAJOR_LEN_PN,
CPConstants.TIC_MAJOR_MPU_PN,
CPConstants.TIC_MAJOR_COLOR_PN,
() -> new LinePropertySetTicMajor()
);
}
}
When a test encapsulates a LinePropertySet object that doesn’t support a particular property, it passes an empty string for that property name. Here, for example, is the constructor for LinePropertySetAxesTest:
public LinePropertySetAxesTest()
{
super(
"", // axes don't have a draw property
CPConstants.AXIS_WEIGHT_PN,
"", // axes don't have a length property
"", // axes don't have a spacing property
CPConstants.AXIS_COLOR_PN,
() -> new LinePropertySetAxes()
);
}
The fields for LinePropertySetTest include the names of properties used by the PropertyManager, the original values of properties as initially read from the PropertyManager and the expected values of the properties stored in the LinePropertySetTest object. Their annotated declarations are below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public abstract class LinePropertySetTest { private static final PropertyManager pMgr = PropertyManager.INSTANCE; private final Supplier<LinePropertySet> setSupplier; private final String drawProperty; private final String strokeProperty; private final String lengthProperty; private final String spacingProperty; private final String colorProperty; private final Optional<Boolean> drawOrig; private final Optional<Float> strokeOrig; private final Optional<Float> lengthOrig; private final Optional<Float> spacingOrig; private final Optional<Color> colorOrig; private Optional<Boolean> drawCurr; private Optional<Float> strokeCurr; private Optional<Float> lengthCurr; private Optional<Float> spacingCurr; private Optional<Color> colorCurr; // ... } |
- Line 3,4: Convenient declaration for the PropertyManager singelton. It eliminates the need to write code like: PropertyManager.instance.asBoolean(…).
- Line 6: The LinePropertySet supplier provided by the subclass.
- Line 8: The name of the draw property for a given subclass of LinePropertySet, for example, CPConstants.GRID_LINE_DRAW_PN.
- Line 9: The name of the stroke property for a given subclass of LinePropertySet, for example, CPConstants.GRID_LINE_WEIGHT_PN.
- Line 10: The name of the length property for a given subclass of LinePropertySet, for example, CPConstants.TIC_MAJOR_LEN_PN.
- Line 11: The name of the spacing property for a given subclass of LinePropertySet, for example, CPConstants.TIC_MAJOR_MPU_PN.
- Line 12: The name of the color property for a given subclass of LinePropertySet, for example, CPConstants.TIC_MAJOR_COLOR_PN.
- Lines 14-18: The original values of the draw, stroke, length, spacing, and color properties for a given subclass of LinePropertySet, as initially stored by the PropertyManager. If a particular property is not supported by the given subclass, its Optional will be empty.
- Lines 20-24: The current values of the draw, stroke, length, spacing, and color properties for a given subclass of LinePropertySet. If a particular property is not supported by the given subclass, its Optional will be empty.
We have a bunch of helper methods, including three that we’ve seen before: asBoolean, asFloat, and asColor. These methods take a property name as input and return an Optional, which will be empty if the PropertyManager can’t find the given property. Here’s the code for asColor:
private static Optional<Color> asColor( String propertyName )
{
Color val = pMgr.asColor( propertyName );
Optional<Color> optional = Optional.ofNullable( val );
return optional;
}
The LinePropertySetTest constructor does all the initialization. Note, in particular, that the first thing it does is invoke initProperties() in the LinePropertySetInitializer class. Here’s the code.
public LinePropertySetTest(
String drawProperty,
String strokeProperty,
String lengthProperty,
String spacingProperty,
String colorProperty,
Supplier<LinePropertySet> setSupplier
)
{
super();
LinePropertySetInitializer.initProperties();
this.setSupplier = setSupplier;
this.drawProperty = drawProperty;
this.strokeProperty = strokeProperty;
this.lengthProperty = lengthProperty;
this.spacingProperty = spacingProperty;
this.colorProperty = colorProperty;
drawCurr = drawOrig = asBoolean( drawProperty );
strokeCurr = strokeOrig = asFloat( strokeProperty );
lengthCurr = lengthOrig = asFloat( lengthProperty );
colorCurr = colorOrig = asColor( colorProperty );
spacingCurr = spacingOrig = asFloat( spacingProperty );
}
There are three helper methods for getting unique values. Methods newBoolean, newFloat and newColor take Optionals as input, and return Optionals. If the input Optional is empty, they just return an empty Optional. Otherwise they obtain a new, guaranteed distinct value from the input, and return an Optional containing it. Here, for example, is the newColor method:
private static Optional<Boolean>
newBoolean( Optional<Boolean> currBoolean )
{
Optional<Boolean> optional = Optional.empty();
if ( !currBoolean.isEmpty() )
{
boolean newVal = !currBoolean.get();
optional = Optional.of( newVal );
}
return optional;
}
The getUniqueValues(LinePropertySet set) method calculates new values for all the properties in set, skipping those that are not supported by the set. It looks like this:
private void getUniqueValues( LinePropertySet set )
{
if ( (drawCurr = newBoolean( drawCurr )).isPresent() )
set.setDraw( drawCurr.get() );
if ( (colorCurr = newColor( colorCurr )).isPresent() )
set.setColor( colorCurr.get() );
if ( (strokeCurr = newFloat( strokeCurr )).isPresent() )
set.setStroke( strokeCurr.get() );
if ( (lengthCurr = newFloat( lengthCurr )).isPresent() )
set.setLength( lengthCurr.get() );
if ( (spacingCurr = newFloat( spacingCurr )).isPresent() )
set.setSpacing( spacingCurr.get() );
}
Method assertEqualsIfPresent( Object actual, Optional<?> expected ) first tests expected to see if it contains a value. If the Optional is empty the operation is ignored, otherwise the contained value is tested for equality against the actual value:
private static void
assertEqualsIfPresent( Object actual, Optional<?> expected )
{
if ( expected.isPresent() )
assertEquals( actual, expected.get() );
}
There are three more helper methods that assist with assertions; they are listed below in brief. If you’d like to see the complete code you can find it in the GitHub repository.
- assertPresentIf( LinePropertySet set )
Validates the has… methods, for example:assertEquals( set.hasDraw(), drawOrig.isPresent() ) - assertHasOrigValues( LinePropertySet set )
Verifies that the properties encapsulated in set match the values initially obtained from the PropertyManager, for example:assertEqualsIfPresent( set.getDraw(), drawOrig ); - assertHasCurrValues( LinePropertySet set )
Verifies that the properties encapsulated in set match the expected current values, for example:assertEqualsIfPresent( set.getDraw(), drawCurr ); - assertHasAppliedValues( LinePropertySet set )
Verifies that the properties encapsulated in set match the expected current values, for example:assertEquals( lengthCurr, asFloat( lengthProperty ) );
That gets us to our sole @Test method, which is annotated below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Test public void test() { LinePropertySet set = setSupplier.get(); assertPresentIf( set ); assertHasOrigValues( set ); getUniqueValues( set ); assertHasCurrValues( set ); set.reset(); assertHasOrigValues( set ); getUniqueValues( set ); assertHasCurrValues( set ); set.apply(); assertHasAppliedValues(); } |
- Line 4: Gets a LinePropertySet to test against.
- Line 6: Validates all the has… methods. For example, if hasDraw() is false, verify that the PropertyManager doesn’t store a draw property for the given type of line.
- Line 7: Verifies that set is initialized with the correct values from the PropertyManager.
- Line 8: Gives all the properties in set a new value.
- Line 9: Verifies that the getters return the expected values.
- Lines 10,11: Invokes the set’s reset() method; verifies that all property values are restored to their original state.
- Line 12: Gives all properties new values.
- Line 13: Verifies that all properties have the expected values.
- Lines 14,15: Calls the set’s apply method; verifies that all values have been updated in the PropertyManager.
Concrete Subclasses of LinePropertySetTest
Of the four concrete subclasses of LinePropertySetTest, we have already seen two (see LinePropertySetTicMajorTest and LinePropertySetAxesTest, above). The other two are directly analogous; you can find them in the GitHub repository.
Summary
On this page we wrote and tested a set of classes to encapsulate the properties of four categories of lines: the axes, grid lines, major tic marks and minor tic marks. One of the convenient features of this class is that it is able to interact directly with the PropertyManager, reading and writing all the properties for a category of lines in two methods, reset and apply. On the next page we will begin developing the LinePropertiesPanel itself.