This is the conclusion of the lesson about property management. In this lesson, we will use reflection to finalize the PropertyManager class. This will also require modifying CartesianPlane to work with PropertyManager. We’ll finish with a unit test suite for PropertyManager that will require additional work with reflection and some work with process management: creating and communicating with processes. Here are some links to some associated educational material.
- The Reflection API in the Oracle Java tutorial
- Java Reflection, by W3schools
- Javadoc Pages in the Java API:
GitHub repository: Cartesian Plane Part 7b
Previous lesson: Cartesian Plane Lesson 7: Property Management (Page 1)
Lesson 7: Property Management (Page 2)
So, what remains to do for the development of the PropertManager class? Not necessarily in the given order, we have to:
- Change CartesianPlane to use the property manager instead of just grabbing the default value of a property out of CPConstants.
- Make CartesianPlane into a property change listener.
- Add a whole bunch of something_PN = “something” declarations to CPConstants.
- Change PropertyManager to initialize all properties, not just the three we used on Page 1.
- Write a lengthy unit test for PropertyManager.
Tasks 1 and 2 above will be tedious enough; what I don’t look forward to is implementing the changes to PropertyManager and ensuring I remember to include everything presently in CPConstants and everything we add to CPConstants in the future. So, let’s not do that. Instead, let’s programmatically pull all the property names and default values out of CPConstants and write a loop to do the work. The way we do this in Java is with reflection.
Introduction to Reflection
Reflection is cool stuff. Using it, we can discover just about anything we could want to know about a class, for example:
- We can find all the constructors in the class and a description of their parameters. We can even choose a constructor and use it to instantiate an object.
- We can find all the fields in a class. We can determine their type, whether they are public or private, whether or not they are class or instance fields, and whether or not they are final. And we can get their values.
- We can find all the methods in a class, get their names and the types of their parameters, etc. We can also invoke a method and get its return value.
Of the above, we are most interested in number two: finding a field by name and discovering its value. Using reflection, we can find all the _PN fields and their corresponding _DV fields, get their values, and use them to make name/value pairs. In case you’re worried, the needs of our project can be satisfied by a very small portion of the Java reflection facility.
Four applications in the project sandbox package may be helpful:
- ReflectionPublicFieldsDemo demonstrates how to find all the public fields in a class, determine their characteristics, and get their values.
- ReflectionCPConstantsDemo takes what we learned from ReflectionPublicFieldsDemo and demonstrates the technique to get all the property names and default values out of CPConstants.
- ReflectionAllFieldsDemo is basically the same as ReflectionPublicFieldsDemo but shows how to interrogate all fields, whether public or private. It’s not entirely relevant to our project because we only care about the class fields in CPConstants, which are all public.
- ReflectionMethodsDemo doesn’t introduce anything we need for this project but is included because it may interest the student. It takes a field, figures out the name of its getter (assuming all naming conventions are followed), and invokes the getter to obtain the field’s value.
In terms of the Java reflection facility overall, we’re only interested in two classes:
- The Class class, which has the method getFields() that returns an array of descriptors for all of its public fields, and getField(String name), which returns a descriptor for the field with the given name; and
- The Field class, which allows us to get the name and value of a field:
- Field.getName() retrieves the name of a field, for example, MW_WIDTH_PN.
- Field.get( Object obj ) gets the value of the field. Examples are: value of MW_WIDTH_PN = “mwWidth” and value of MW_WIDTH_DV = “500”. The object passed as an argument is only required for instance fields; we will always pass null because every field in CPConstants is a class field. The return value is type Object; the values of all the fields in CPConstants are String, so we will always cast the value returned to String:
String val = (String)field.get( null );
Implementing the Logic
Changes to CPConstants
For every constant in CPConstants that ends in _DV, we need to declare a corresponding constant that ends in _PN; the value of the _PN constant will be a string formed by taking the name of the constant and converting it to CamelCase (without the suffix _PN), for example:
- LABEL_FONT_SIZE_PN ➟ “labelFontSize”
- USER_PROPERTIES_PN ➟ “userProperties”
Now, all the constants come in pairs that look like this:
Don’t forget: these are public fields and require documentation in Javadoc.
/** Axis color property name */
public static final String AXIS_COLOR_PN = "axisColor";
/** Axis color default value: int. */
public static final String AXIS_COLOR_DV = "0X000000";
/** Axis weight property name */
public static final String AXIS_WEIGHT_PN = "axisWeight";
/** Axis weight default value: float. */
public static final String AXIS_WEIGHT_DV = "2";
Changes to PropertyManager
All the changes we make to PropertyManager will be in the constructor. Currently, we have four sets of lines that look like this:
val = getProperty(
CPConstants.TIC_MAJOR_COLOR_PN,
CPConstants.TIC_MAJOR_COLOR_DV
);
propertyMap.put( CPConstants.TIC_MAJOR_COLOR_PN, val );
This code will be removed and replaced with our new reflection-based logic. We’ll start by getting all the public fields from the CPConstants class and finding those representing property names.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | private PropertyManager() { changeSupport = new PropertyChangeSupport( this ); // Compile the properties stored in the application ini file. getAppProperties(); // Merge the properties from the user's ini file, if any; // note that anything in the user's ini file will overwrite // with a matching property name. getUserProperties(); // Get all property names and their default values. for ( Field pnField : CPConstants.class.getFields() ) { String fieldName = pnField.getName(); if ( fieldName.endsWith( "_PN" ) ) { // ... } } } |
By the way, getFields() returns only public fields. If you also want the private fields, use getDeclaredFields(); you have to be careful because this might precipitate a security violation. Similarly, getField( name ) will only succeed if name is a public field; to include private fields, use getDeclaredField( name ).
Once we have a _PN field, we will need to find its corresponding _DV field. We derive the name of the _DV field by substituting “_DV” for “_PN” in the property name field: // Get the name of the PN field
int pNameLen = fieldName.length();
// ... get the start of the PN field name (without the _PN)
String pNamePrefix = fieldName.substring( 0, pNameLen - 3 );
// ... construct the name of the DV field
String dvName = pNamePrefix + "_DV";
Now we have to:
- Get the field descriptor for dvName. We do this with the Class.getField(String) method. This method can throw a NoSuchFieldException, so it must go in a try block. If dvName is not found, it is a serious programming error, so we’ll print a description of the error and terminate the application.
- We have to get the values of the property name and default value fields. We do this with the Field.get(Object) method. Since we are interrogating a class field, the Object argument will be null. This method can throw an IllegalAccessException, so this, too, has to go in a try block. Since all the fields we’re interrogating are public, it would be very strange to get this error, but it’s a checked exception, so we need a catch block for it. Inside the catch block, we’ll print a stack trace and exit the application.
Here is the logic for getting the name/default value pair:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | String propName = ""; String propDefault = ""; try { Field dvField = CPConstants.class.getField( dvName ); propName = (String)pnField.get( null ); propDefault = (String)dvField.get( null ); } catch ( NoSuchFieldException exc ) { String msg = dvName + ": field not found"; System.err.println( msg ); exc.printStackTrace(); System.exit( 1 ); } catch ( IllegalAccessException exc ) { exc.printStackTrace(); System.exit( 1 ); } |
Now that we know the property name and its default value, we can negotiate for a final value with the getProperty(name, defaultValue) method, which will determine whether the value for the property has been set from the command line, the environment, the user’s property file or the application property file: String finalVal = getProperty( propName, propDefault );
propertyMap.put( propName, finalVal );
Here’s the constructor in its entirety.
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | private PropertyManager() { changeSupport = new PropertyChangeSupport( this ); // Compile the properties stored in the application ini file. getAppProperties(); // Merge the properties from the user's ini file, if any; // note that anything in the user's ini file will overwrite // with a matching property name. getUserProperties(); // Get all property names and their default values. for ( Field pnField : CPConstants.class.getFields() ) { String fieldName = pnField.getName(); if ( fieldName.endsWith( "_PN" ) ) { // derive the name of the associated DV field... // ... get the name of the PN field int pNameLen = fieldName.length(); // ... get the start of the PN field name (without the _PN) String pNamePrefix = fieldName.substring( 0, pNameLen - 3 ); // ... construct the name of the DV field String dvName = pNamePrefix + "_DV"; String propName = ""; String propDefault = ""; try { // Get the field that describes the default value // must be inside a try block because getField // throws a checked exception. Field dvField = CPConstants.class.getField( dvName ); // Because we are interrogating a class field // (declared static) we don't need to pass an // object to the Field.get method. // Must be inside a try block because get // throws a checked exception. propName = (String)pnField.get( null ); propDefault = (String)dvField.get( null ); } catch ( NoSuchFieldException exc ) { // This exception indicates a programming error. String msg = dvName + ": field not found"; System.err.println( msg ); exc.printStackTrace(); System.exit( 1 ); } catch ( IllegalAccessException exc ) { // this is a wholly unexpected error; // print an error and exit. exc.printStackTrace(); System.exit( 1 ); } // Interrogate command line, environment, and // ini files for a value overriding the default. String finalVal = getProperty( propName, propDefault ); propertyMap.put( propName, finalVal ); } } } |
Changes to the CartesianPlane Class
To complete the CartesianPlane class for this lesson, we need to:
- Remove all the property setters and getters. Property maintenance now runs through the PropertyManager class.
- Change the instance variable initialization logic.
- Make CartesianPlane a PropertyChangeListener.
Removing the setters and getters:
This is easy: delete them.
Changing the instance variable initialization logic:
At the top of CartesianPlane.java we have a long list of instance variable declarations that look like this: /** Grid units (pixels-per-unit) default value: float. */
private float gridUnit =
CPConstants.asFloat( CPConstants.GRID_UNIT_DV );
These all have to be changed to obtain initial values from the PropertyManager: private float gridUnit =
pmgr.asFloat( CPConstants.GRID_UNIT_PN );
By the way, I get tired of writing lines of long lines of code like gridUnit = To improve this, at least by a little, I have declared the class variable
PropertyManager.INSTANCE.asFloat( CPConstants.GRID_UNIT_PN ); PropertyManager pmgr = PropertyManager.INSTANCE; Now, assignments like the above can be simplified as like this: gridUnit = pmgr.asFloat( CPConstants.GRID_UNIT_PN );
Here are a few of the modified declarations; the complete code can be found in the GitHub repository.
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 45 46 47 48 | /** * This declaration is just for convenience; it saves * having to write "PropertyManager.INSTANCE" over and over. */ private static final PropertyManager pmgr = PropertyManager.INSTANCE; private static final int mainWindowWidthDV = pmgr.asInt( CPConstants.MW_WIDTH_PN ); private static final int mainWindowHeightDV = pmgr.asInt( CPConstants.MW_HEIGHT_PN ); ///////////////////////////////////////////////// // General grid properties ///////////////////////////////////////////////// /** Grid units (pixels-per-unit) default value: float. */ private float gridUnit = pmgr.asFloat( CPConstants.GRID_UNIT_PN ); ///////////////////////////////////////////////// // Main window properties // Note: "width" and "height" are included as // main window properties in CPConstants, // but it is not necessary to encapsulate // their values in instance variables. // See the default constructor. ///////////////////////////////////////////////// private Color mwBGColor = pmgr.asColor( CPConstants.MW_BG_COLOR_PN ); ///////////////////////////////////////////////// // Margin properties ///////////////////////////////////////////////// private float marginTopWidth = pmgr.asFloat( CPConstants.MARGIN_TOP_WIDTH_PN ); private Color marginTopBGColor = pmgr.asColor( CPConstants.MARGIN_TOP_BG_COLOR_PN ); private float marginRightWidth = pmgr.asFloat( CPConstants.MARGIN_RIGHT_WIDTH_PN ); private Color marginRightBGColor = pmgr.asColor( CPConstants.MARGIN_RIGHT_BG_COLOR_PN ); private float marginBottomWidth = pmgr.asFloat( CPConstants.MARGIN_BOTTOM_WIDTH_PN ); private Color marginBottomBGColor = pmgr.asColor( CPConstants.MARGIN_BOTTOM_BG_COLOR_PN ); private float marginLeftWidth = pmgr.asFloat( CPConstants.MARGIN_LEFT_WIDTH_PN ); private Color marginLeftBGColor = pmgr.asColor( CPConstants.MARGIN_LEFT_BG_COLOR_PN ); |
Making CartesianPlane a PropertyChangeListener:
We make CartesianPlane a property-change listener by changing the class declaration: public class CartesianPlane
extends JPanel implements PropertyChangeListener
and adding the propertyChange(PropertyChangeEvent) method required by interface PropertyChangeListener: public void propertyChange( PropertyChangeEvent evt )More about this method in a moment.
{
// ...
}
We also have to go to the constructor and register our CartesianPlane instance as a property-change listener in PropertyManager:
public CartesianPlane( int width, int height )
{
Dimension dim = new Dimension( width, height );
setPreferredSize( dim );
pmgr.addPropertyChangeListener( this );
}
(Recall that pmgr is a shortcut for PropertyManager.INSTANCE.)
To complete the propertyChange method, we’ll have a lengthy switch statement that determines whether the property changed is of interest to CartesianPlane and, if it is, will change the appropriate instance variable.
Note 1: The programmers who use our PropertyManager must be aware that changes to a property will not instantly be reflected on the display. If they want to see the effect of the property change, they have to cause the paintComponent to be executed; to do this, invoke the repaint method in the CartesianPlane object: PropertyManager.INSTANCE.setProperty( ... );
canvas.repaint();
Note 2: You can’t call paintComponent directly. Recall from the “plumber’s helper” analogy that another thread controls the CartesianPlane frame. Calling repaint will precipitate a series of events that will eventually end up with the paintComponent method being correctly invoked.
Here’s an annotated listing of a portion of our switch statement. The complete code can be found in the GitHub repository. You might notice that there are no cases for MW_WIDTH_PN or MW_HEIGHT_PN; that’s because those properties are only used during construction. Changes to these properties after instantiation have no effect on the class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public void propertyChange( PropertyChangeEvent evt ) { String pName = evt.getPropertyName(); String newVal = (String)evt.getNewValue(); switch ( pName ) { case CPConstants.GRID_UNIT_PN: gridUnit = CPConstants.asFloat( newVal ); break; case CPConstants.MW_BG_COLOR_PN: mwBGColor = CPConstants.asColor( newVal ); break; case CPConstants.MARGIN_TOP_WIDTH_PN: marginTopWidth = CPConstants.asFloat( newVal ); break; case CPConstants.MARGIN_TOP_BG_COLOR_PN: marginTopBGColor = CPConstants.asColor( newVal ); break; // ... } |
- Lines 3,4: The PropertyChangeEvent object passed to the propertyChange method includes the name of the changed property and its new value. For a complete description of this object, see the Java API documentation for the PropertyChangeEvent class.
Testing
We will save testing of the PropertyManager until after we have discussed Interprocess Communication. But we can quickly dispatch testing of the CPConstants class.
Testing CPConstants
We haven’t added any executable code to CPConstants, so it’s tempting to say that no additional testing is necessary. We did make changes to the code, however, and we added a new requirement: every _PN field must have a corresponding _DV field, and we need to ensure we have satisfied that requirement. Also, when we set out to modify CPConstants, we started with the _DV fields and created new _PN fields, so we should verify that: every _DV field has a corresponding _PN field. I propose we add two test cases to CPConstantsTest: one to make sure that every _PN field has a corresponding _DV field, and another to make sure we didn’t miss anything when we created the _PN fields.
The first test case, verifying the _PN → _DV mapping, is closely related to the code we wrote for the PropertyManager constructor. The second case, verifying the _DV → _PN mapping uses a minor variation on the first case. Here are the two test methods.
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | @Test public void testPNMapToDV() { // Verify that all variables with names that end in _PN // match a corresponding variable with a name that ends in _DV. for ( Field pnField : CPConstants.class.getFields() ) { String fieldName = pnField.getName(); if ( fieldName.endsWith( "_PN" ) ) { // derive the name of the associated DV field... // ... get the name of the PN field int pNameLen = fieldName.length(); assertTrue( pNameLen > 3 ); // ... get the start of the PN field name (without the _PN) String pNamePrefix = fieldName.substring( 0, pNameLen - 3 ); // ... construct the name of the DV field String dvName = pNamePrefix + "_DV"; try { // If the field is there the test succeeds; if it // throws NoSuchFieldException it fails. CPConstants.class.getField( dvName ); } catch ( NoSuchFieldException exc ) { StringBuilder bldr = new StringBuilder(); bldr.append( "For property name field " ) .append( fieldName ) .append( " no default value field (" ) .append( dvName ) .append( ") found" ); fail( bldr.toString() ); } } } } @Test public void testDVMapToPN() { // Verify that have generated _PN fields for all the // original _DV fields. for ( Field pnField : CPConstants.class.getFields() ) { String fieldName = pnField.getName(); if ( fieldName.endsWith( "_DV" ) ) { // derive the name of the associated PN field... // ... get the name of the DV field int dvNameLen = fieldName.length(); assertTrue( dvNameLen > 3 ); // ... get the start of the PN field name (without the _PN) String dvNamePrefix = fieldName.substring( 0, dvNameLen - 3 ); // ... construct the name of the DV field String pName = dvNamePrefix + "_PN"; try { // If the field is there the test succeeds; if it // throws NoSuchFieldException it fails. CPConstants.class.getField( pName ); } catch ( NoSuchFieldException exc ) { StringBuilder bldr = new StringBuilder(); bldr.append( "For default value name field " ) .append( fieldName ) .append( " no default property name field (" ) .append( pName ) .append( ") found" ); fail( bldr.toString() ); } } } } |
As long as we’re on the subject of constants… I’ve decided I don’t like the constant appPropertiesName living in the PropertyManager class. I’ve moved it to CPConstants, made it public, and called it APP_PROPERTIES_NAME. Note that this is not a property in the sense of being part of a name/value pair, so its name doesn’t have a _PN suffix, and there is no corresponding _DV constant.
1 2 3 4 5 6 7 8 9 10 11 | @ParameterizedTest @ValueSource(strings={ "0xFF0000", "0x00FF00", "0x0000FF", "#FF00FF" } ) void testColorAccessors( String strVal ) { String propName = "phonyColorProp"; int intColor = Integer.decode( strVal ); Color expColor = new Color( intColor ); pmgr.setProperty( propName, expColor ); Color actColor = pmgr.asColor( propName ); assertEquals( expColor, actColor ); } |
Something to Think About…
Using reflection, we managed to automatically encapsulate setters and getters for all properties defined in CPConstants, without knowing what those properties are ahead of time. But the technique we used leaves us wishing for more. Instead of pmgr.asColor(CPConstants.GRID_LINE_WEIGHT_PN) wouldn’t it be nice if we just had a traditional getter like this one? public float getGridLineWeight()
We could have that if we wanted to work for it. One way to do it would be to go through CPConstants by hand and, for each property, write a setter and a getter. But that’s not only a lot of work; it introduces dependencies that must be managed throughout the project. For example, if you change the type of a property from float to double, you must remember to add a comment in CPConstants and make two changes to PropertyManager (for the setter and getter). If you add a property, delete a property, or make a minor change to a property name, you have the same three changes to make.
Another way to do this would be to make a text file (or a database) describing the name, default value, and type of each property. Then, you can write a program that parses the file and, for each property, generates …_PN and …_DV declarations for CPConstants, as well as a setter and getter for PropertyManager. Adding or removing a property, name changes, and type changes can then be managed in a single place: the text file. To make a change, you modify the text file and then run the program to regenerate all the code. It’s not that hard. The one wrinkle I can think of is how to convince your build platform (Eclipse, Maven, etc.) to recognize changes to the text file and regenerate the necessary code automatically (this is part of the dependency management task).
Maybe we’ll take a shot at that in a future lesson.
Summary
That completes our development of the PropertyManager and leaves us with the task of testing it. To test PropertyManager initialization, we must be able to manipulate our environment; one test, for example, will put a name/value pair in the environment and another pair with the same name on the command line, then verify that the PropertyManager gets the initial value from the command line. But that’s a problem because, in Java, once a program starts running, you are not permitted to manipulate the environment. To get around that, we will start a new program for every JUnit test, then ask the new program to determine what the initial value the PropertyManager selected.
That gets us into the topics of process management and interprocess communication. On the next page, we will introduce interprocess communication and discuss the PropertyManager JUnit test on the following page.