This page will discuss the ProfileParser class, which we will add to the com.acmemail.judah.cartesian_plane.input package. It’s a companion to the Profile class that we developed on the previous page. It can be used to save a Profile’s properties to a file and read them from a file. Part of this task will be to use the Stream.Builder class from the java.util.stream package. Our discussion will begin with the Stream.Builder class.
GitHub repository: Cartesian Plane Lesson 18
Previous lesson: Cartesian Plane Lesson 18 Page 3: Profiles
About Stream.Builder<T>
A Stream Builder provides an efficient way to assemble a Stream dynamically. The interface Stream.Builder<T> is based on the Builder Pattern and is a subinterface of Consumer<T>. Elements of type T are added to the builder as needed. When the elements of the stream are fully assembled, the build method is called to produce the stream, after which no new elements may be added to the builder. The build method may only be called once.
See also:
- What is the purpose of a Stream.Builder in Java? in StackOverflow
- The Java 8 Stream API Tutorial on the Baeldung website
- Builder Design Pattern on the GeeksforGeeks website
- Builder Pattern in Wikipedia
A Builder object is created via the static build() method in the Stream interface, for example: Stream.Builder<String> bldr = Stream.<String>builder();*
Elements can be added to the builder via the accept(T t) method provided by the superinterface. You can also use the default add(T t) method provided by the Builder interface. The add method is equivalent to the accept method but also returns the Builder instance so that method chaining can be used (e.g., bldr.add(1).add(2).add(3)). The StreamBuilderAcceptDemo and StreamBuilderAddDemo applications from the project sandbox demonstrate both strategies and are shown below. The StreamBuilderAddDemo code is abbreviated slightly; the complete code is in the Git repository.
*About the syntax:
Stream.Builder<String> bldr = Stream.<String>builder();
Builder<T> is a generic interface in the generic class Stream<T>. The method builder() is a generic method in Stream<T> that returns a Stream.Builder<T>. It’s declared like this:
static <T> Stream.Builder<T> builder()
The full syntax when calling a generic method is to declare the type in angle brackets before the generic method. If getList in class GenericMethodDemo1 is declared this way:
public static <T> List<T> getList()
We could invoke it like this:
List<String> strList = GenericMethodDemo1.<String>getList();
List<Integer> intList = GenericMethodDemo1.<Integer>getList();
The extra <type>, however, can usually be omitted because the compiler is able to infer it, giving us:
List<String> strList = GenericMethodDemo1.getList();
List<Integer> intList = GenericMethodDemo1.getList();
Or, in the case of our Stream example:
Stream.Builder<String> bldr = Stream.builder();
Compare this the way that the compiler can infer the type of ArrayList<> in the following declaration:
List<String> strList = new ArrayList<>();
I chose to include the extra <type> at least once, because that’s how I saw it in a lot of the examples on the web, and I wanted to be able to call attention to it.
See also: Generic Methods in the Oracle Java tutorial.
public class StreamBuilderAcceptDemo
{
public static void main( String[] args )
{
Stream.Builder<String> bldr = Stream.<String>builder();
String name = null;
while ( (name = getNextName()) != null )
bldr.accept( name );
Stream<String> stream = bldr.build();
stream.forEach( System.out::println );
}
private static String getNextName()
{
final String prompt = "Enter the next name; cancel when done";
String name = JOptionPane.showInputDialog( prompt );
return name;
}
}
public class StreamBuilderAddDemo
{
public static void main( String[] args )
{
Stream.Builder<String> bldr = Stream.<String>builder()
.add( "Alabama" )
.add( "Alaska" )
.add( "Arizona" )
.add( "Arkansas" )
.add( "California" )
.add( "Colorado" )
// ...
.add( "Virginia" )
.add( "Washington" )
.add( "WestVirginia" )
.add( "Wisconsin" )
.add( "Wyoming" );
Stream<String> stream = bldr.build();
stream.forEach( System.out::println );
}
}
The ProfileParser Class
An object of this class will encapsulate a Profile object. It can initialize a Profile from a formatted text stream and write its encapsulated Profile to a formatted text stream.
Data Organization
A data stream containing Profile data consists of individual lines of text containing a directive or name/value pair, one per line. Here is an example of a text stream that sets a Profile’s name and grid unit; the font size property in its GraphPropertySet; and the stroke property in its LinePropertySetGridLines object.
PROFILE Cardioid
gridUnit 65.0
CLASS GraphPropertySetMW
font_size 10
CLASS LinePropertySetGridLines
stroke 2
⬛ Directives
Following is a list of valid directives:
- PROFILE name
This directive marks the beginning of a profile data stream, where name is the profile’s name. See also Profile.PROFILE. - CLASS name
This directive indicates that the following lines of data, up to the next class directive, are to be applied to the named class. Valid class names are GraphPropertySetMW or the simple name of one of the LinePropertySet concrete subclasses, LinePropertySetAxes, LinePropertySetGridLines, LinePropertySetTicMajor, or LinePropertySetTicMinor. All profile properties except gridUnit must be preceded by a CLASS directive. See also Profile.CLASS.
⬛ Property Names
Following is a list of valid property names.
- gridUnit val
The grid unit for a Profile object. It must have a text value that is convertible to type float. See also Profile.GRID_UNIT. - draw val
Specifies the draw property value for either the GraphPropertySetMW or one of the LinePropertySet subclasses. The preceding CLASS directive determines the target property set. The syntax of the text value is specified by Boolean.parseBoolean(String). See also Profile.DRAW. - stroke val
Specifies the stroke property value for one of the LinePropertySet subclasses. Its text value must be convertible to type float. See also Profile.STROKE. - spacing val
Specifies the spacing property value for one of the LinePropertySet subclasses. Its text value must be convertible to type float. See also Profile.SPACING. - length val
Specifies the length property value for one of the LinePropertySet subclasses. Its text value must be convertible to type float. See also Profile.LENGTH. - color val
Specifies the color property value for one of the LinePropertySet subclasses. The text value must be convertible to type int and may be specified in decimal (255), hexadecimal (0xFF), or HTML notation (#FF). See also Profile.COLOR. - width val
Specifies the value for the width GraphPropertySetMW class. See also Profile.WIDTH. - fgColor val
Specifies the text color property value for the GraphPropertySetMW class. The text value must be convertible to type int and may be specified in decimal (255), hexadecimal (0xFF), or HTML notation (#FF). See also Profile.FG_COLOR. - bgColor val
Specifies the background color property value for the GraphPropertySetMW class. The text value must be convertible to type int and may be specified in decimal (255), hexadecimal (0xFF), or HTML notation (#FF). See also Profile.BG_COLOR. - font_name val
Specifies the font name property value for the GraphPropertySetMW class. The value must be one of the logical font names: Serif, SansSerif, Monospaced, Dialog, or DialogInput. See also Profile.FONT_NAME. - font_size val
Specifies the font size property value for the GraphPropertySetMW class. Its text value must be convertible to type float. See also Profile.FONT_SIZE. - font_bold val
Specifies whether or not text is to be displayed in a bold font. The format of the text value is specified by Boolean.parseBoolean. See also Profile.FONT_BOLD. - font_italic val
Specifies whether or not text is to be displayed in an italic font. The format of the text value is specified by Boolean.parseBoolean. See also Profile.FONT_ITALIC.
ProfileParser Implementation
Following is a discussion of the ProfileParser class implementation, beginning with a list of the class’s constant variables.
⬛ Constant Variables
The ProfileParser declares the following constant variables to use when compiling or parsing a text stream that describes a profile; see also Directives and Property Names.
public static final String PROFILE = "PROFILE";
public static final String CLASS = "class";
public static final String GRID_UNIT = CPConstants.GRID_UNIT_PN;
public static final String DRAW = "draw";
public static final String STROKE = "stroke";
public static final String SPACING = "spacing";
public static final String LENGTH = "length";
public static final String COLOR = "color";
public static final String WIDTH = "width";
public static final String FG_COLOR = "fgColor";
public static final String BG_COLOR = "bgColor";
public static final String FONT_NAME = "font_name";
public static final String FONT_SIZE = "font_size";
public static final String FONT_BOLD = "font_bold";
public static final String FONT_ITALIC = "font_italic";
⬛ Instance Variables
Following is an annotated list of instance variables used by the ProfileParser.
1 2 3 4 | private final Profile workingProfile; private final GraphPropertySet mainWindow; private LinePropertySet currLineSet = null; private GraphPropertySet currGraphSet = null; |
- Line 1: This is the profile that is currently encapsulated by the ProfileParser instance. See also Constructors.
- Line 2: This is the GraphPropertySet from the encapsulated profile.
- Line 3: This instance variable is used as the destination for parsing input data representing GraphPropertySet properties. When input is directed to a LinePropertySet, the value of this variable is null. See also parseClass and parseNameValue.
- Line 4: This instance variable is used as the destination for parsing input data representing LinePropertySet properties. When input is directed to the GraphPropertySet, the value of this variable is null. See also parseClass and parseNameValue.
⬛ Private Methods for Input Processing
Following are the helper methods used to support processing input streams.
🟦 private void postError( String message )
This method posts an error message in a JOptionPane dialog, as shown below.
private void postError( String message )
{
JOptionPane.showMessageDialog(
null,
message,
"Profile Parse Error",
JOptionPane.ERROR_MESSAGE
);
}
🟦 private void postParseError( String[] pair )
This method interprets a given string array as an invalid name/value pair and posts an error message formatted as Failed to parse “value“ for field “name,” for example: Failed to parse "3..0" for field "stroke"
The input must be a two-element array. Here’s the code.
private void postParseError( String[] pair )
{
String fmt = "Failed to parse \"%s\" for field \"%s\"";
String message = String.format( fmt, pair[1], pair[0] );
postError( message );
}
🟦 private void postParseError( String value, String descrip )
For a given value and description, this method posts an error message formatted as “value “ + description. The code follows.
private void postParseError( String value, String descrip )
{
String message = "\"" + value + "\" " + descrip;
postError( message );
}
🟦 private boolean isNameValue( String[] args )
This method verifies that a line has been parsed into two arguments that may be treated as a name/value pair. An error message will be posted if an invalid name/value pair is detected. It makes no effort to validate the name or the value. It looks like this.
private boolean isNameValue( String[] args )
{
boolean result =
args.length == 2 && !args[1].isEmpty();
if ( !result )
{
StringBuilder bldr = new StringBuilder();
bldr.append( "Invalid name/value pair: " );
for ( String arg : args )
bldr.append( '"').append( arg ).append( "\" " );
postError( bldr.toString() );
}
return result;
}
🟦 private void parseClass( String value )
Given a CLASS directive, interpret the argument as referring to the current Profile’s GraphPropertySet or one of its LinePropertySets. If the argument begins with “Graph,” the currGraphSet instance variable is set to the Profile’s GraphPropertySet, and the currLineSet instance variable is set to null. Otherwise, treat the argument as the name of a LinePropertySet subclass, attempt to obtain the indicated object from the current Profile, and set currGraphSet to null. An error message is posted if the argument cannot be resolved to a valid value. Here’s a listing of the method.
private void parseClass( String value )
{
if ( value.startsWith( "Graph" ) )
{
currGraphSet = workingProfile.getMainWindow();
currLineSet = null;
}
else
{
currGraphSet = null;
currLineSet = workingProfile.getLinePropertySet( value );
if ( currLineSet == null )
postParseError( value, "not a valid property set name" );
}
}
🟦 private void parseString( Consumer<String> consumer, String[] pair )
🟦 private void parseFloat( Consumer<Float> consumer, String[] pair )
🟦 private void parseColor( Consumer<Color> consumer, String[] pair )
🟦 private void parseBoolean( Consumer<Boolean> consumer, String[] pair )
The above methods have two parameters: a Consumer<T> and a string array containing a name/value pair; they assume that pair is a valid 2-dimensional array. The method attempts to convert the string value from the array to a value of type T and then passes the converted value to the Consumer. Note that color values are expressed as integers. The code for these methods follows.
private void parseString( Consumer<String> consumer, String[] pair )
{
try
{
String val = pair[1];
consumer.accept( val );
}
catch ( NullPointerException exc )
{
postError( "No current property set" );
}
}
private void parseFloat( Consumer<Float> consumer, String[] pair )
{
try
{
float val = Float.parseFloat( pair[1] );
consumer.accept( val );
}
catch ( NumberFormatException exc )
{
postParseError( pair );
}
}
private void parseColor( Consumer<Color> consumer, String[] pair )
{
try
{
int val = Integer.decode( pair[1] );
Color color = new Color( val );
consumer.accept( color );
}
catch ( NumberFormatException exc )
{
postParseError( pair );
}
}
private void parseBoolean( Consumer<Boolean> consumer, String[] pair )
{
boolean val = Boolean.parseBoolean( pair[1] );
consumer.accept( val );
}
🟦 private boolean validateCurrPropertySet( Object obj )
Verifies that the given object is non-null. The given object is expected to be either the currGraphSet instance variable or the currLineSet instance variable. If the object is non-null, true is returned. Otherwise, an error message is posted, and false is returned. The purpose of this method is to simplify parsing logic such as the following:
switch ( property name )
{
...
case STROKE:
if ( currLinePropertySet != null )
formulate error message
post error message
else
set stroke in currLinePropertySet to value
break
...
}
The parseNameValue(String[] args) method has a switch statement with eleven similar cases, which, with the help of validateCurrPropertySet, can be reduced to:
switch ( property name )
{
...
case STROKE:
if ( validateCurrPropertySet( currLineSet ) )
set stroke in currLinePropertySet to value
break
...
}
The code for the validateCurrPropertySet method follows.
private boolean validateCurrPropertySet( Object obj )
{
boolean result = obj != null;
if ( !result )
postError( "Target property set not configured" );
return result;
}
🟦 private void parseNameValue( String[] args )
Given a name/value pair of non-null, non-empty strings, this method attempts to interpret args[0] as a directive or property name. Given a valid directive or property name, it attempts to interpret args[1] in the context of the name. Except for the gridUnit property, the correct interpretation of a property value is contingent upon the correct configuration of the currLineSet and currGraphSet instance variables via a prior CLASS directive; see Instance Variables and the parseClass method above. An abbreviated listing for this method follows; the complete code can be found in the GitHub repository.
private void parseNameValue( String[] args )
{
switch ( args[0] )
{
case CLASS:
parseClass( args[1] );
break;
case GRID_UNIT:
parseFloat( workingProfile::setGridUnit, args );
break;
case DRAW:
if ( validateCurrPropertySet( currLineSet ) )
parseBoolean( currLineSet::setDraw, args );
break;
...
case FONT_STYLE:
if ( validateCurrPropertySet( currGraphSet ) )
parseString( currGraphSet::setFontStyle, args );
break;
default:
postParseError( args[0], "is not a valid property" );
}
⬛ Private Methods for Output Processing
Following are the helper methods used to support the creation of output streams.
🟦 private String format( String label, String value )
Given a label and a value, this method formats a string in the form label value, for example: stroke 3
Here’s the method listing.
private String format( String label, String value )
{
String result = label + " " + value;
return result;
}
🟦 private String fromClass( Class<?> clazz )
Given a GraphPropertySet or LinePropertySet class object, this method formulates a class directive for inclusion in the profile output stream, for example: CLASS GraphPropertySetMW
Here’s the code.
private String fromClass( Class<?> clazz )
{
String result = format( CLASS, clazz.getSimpleName() );
return result;
}
🟦 private String fromFloat( String label, float fVal )
🟦 private String fromBoolean( String label, boolean bVal )
🟦 private String fromColor( String label, Color color )
Given a label and a value, this method formats a string of the form label value, where label is presumably a directive or property name; see Constant Variables above. For example: font_name monospaced
Here’s the code.
private String fromFloat( String label, float fVal )
{
String sVal = String.valueOf( fVal );
String result = format( label, sVal );
return result;
}
private String fromColor( String label, Color color )
{
int iVal = color.getRGB() & 0xFFFFFF;
String sVal = String.format( "0x%06x", iVal );
String result = format( label, sVal );
return result;
}
private String fromBoolean( String label, boolean bVal )
{
String sVal = String.valueOf( bVal );
String result = format( label, sVal );
return result;
}
🟦 private void compile( GraphPropertySet set, Stream.Builder<String> bldr )
Given a GraphPropertySet and a Stream Builder, this method adds to the builder a CLASS directive for GraphPropertySetMW. Then, for each property in the set, a string in the format property-name value is formulated and added to the builder. For example:
CLASS GraphPropertySetMW
bgColor 0xe6e6e6
fgColor 0x000000
font_draw true
font_name Dialog
font_size 10.0
font_style 0
A listing for this method follows.
private void
compile( GraphPropertySet set, Stream.Builder<String> bldr )
{
bldr.add( fromClass( set.getClass() ) );
bldr.add( fromFloat( WIDTH, set.getWidth() ) );
bldr.add( fromColor( BG_COLOR, set.getBGColor() ) );
bldr.add( fromColor( FG_COLOR, set.getFGColor() ) );
bldr.add( fromBoolean( FONT_DRAW, set.isFontDraw() ) );
bldr.add( format( FONT_NAME, set.getFontName() ) );
bldr.add( fromFloat( FONT_SIZE, set.getFontSize() ) );
bldr.add( fromBoolean( FONT_BOLD, set.isBold() ) );
bldr.add( fromBoolean( FONT_ITALIC, set.isItalic() ) );
}
🟦 private void compile( LinePropertySet set, Stream.Builder<String> bldr )
Given a LinePropertySet subclass and a Stream Builder, this method adds a CLASS directive for the given LinePropertySet subclass to the builder. Then, for each supported property in the set, a string in the format property-name value is formulated and added to the builder. For example:
CLASS LinePropertySetAxes
color 0x000000
stroke 2.0
The listing for this method follows.
private void
compile( LinePropertySet set, Stream.Builder<String> bldr )
{
bldr.add( fromClass( set.getClass() ) );
if ( set.hasColor() )
bldr.add( fromColor( COLOR, set.getColor() ) );
if ( set.hasDraw() )
bldr.add( fromBoolean( DRAW, set.getDraw() ) );
if ( set.hasLength() )
bldr.add( fromFloat( LENGTH, set.getLength() ) );
if ( set.hasSpacing() )
bldr.add( fromFloat( SPACING, set.getSpacing() ) );
if ( set.hasStroke() )
bldr.add( fromFloat( STROKE, set.getStroke() ) );
}
⬛ Constructors
The ProfileParser class has two constructors which initialize the class with a working Profile. The first instantiates the Profile object to be encapsulated, and the second takes the object as input. The code for these constructors follows.
public ProfileParser()
{
this( new Profile() );
}
public ProfileParser( Profile workingProfile )
{
this.workingProfile = workingProfile;
this.mainWindow = workingProfile.getMainWindow();
}
⬛ Public Methods
Following is a discussion of this class’s public methods.
🟦 public Profile getProfile()
This method simply returns the working Profile as shown here:
public Profile getProfile()
{
return workingProfile;
}
🟦 public Stream<String> getProperties()
This method compiles a stream of strings that encapsulates the properties of the working Profile. Each string is a properly formatted directive or property name/value pair (see Directives and Property Names above). Here is an annotated listing of this method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public Stream<String> getProperties() { Stream.Builder<String> bldr = Stream.<String>builder(); bldr.add( format ( PROFILE, " " + workingProfile.getName() ) ); bldr.add( fromFloat( GRID_UNIT, workingProfile.getGridUnit() ) ); compile( mainWindow, bldr ); Stream.of( LinePropertySetAxes.class, LinePropertySetGridLines.class, LinePropertySetTicMajor.class, LinePropertySetTicMinor.class ) .map( Class::getSimpleName ) .map( workingProfile::getLinePropertySet ) .forEach( v -> compile( v, bldr ) ); return bldr.build(); } |
- Line 3: Instantiates a Stream.Builder object.
- Line 4. Adds a PROFILE directive to the stream builder.
- Line 5: Adds the gridUnit property to the stream builder.
- Line 6: Adds to the stream builder all the properties from the GraphPropertySetMW object; see compile(GraphPropertySet) above.
- Lines 7-12: For each of the Class classes of the concrete LinePropertySet subclasses:
- Line 13: Gets the simple name of the class.
- Line 14: Gets the target class from the working profile.
- Line 15: Adds to the stream builder all the properties from the named LinePropertySet object; see compile(LinePropertySet) above.
🟦 public void loadProperties( Stream<String> inStream )
For a given Stream<String>, where each stream element describes a well-formed profile directive or property, decode the stream and set the target properties in the current working profile. An annotated listing of this method follows.
1 2 3 4 5 6 7 8 9 | public void loadProperties( Stream<String> inStream ) { inStream .map( String::trim ) .filter( s -> !s.isEmpty() ) .map( s -> s.split( "\\s" ) ) .filter( a -> isNameValue( a ) ) .forEach( this::parseNameValue ); } |
- Line 3: For each string in inStream:
- Line 4: Trim the string.
- Line 5: Filter out empty strings.
- Line 6: Map the string to an array of tokens.
- Line 7: Filter out any array that does not describe a directive or name/value pair.
- Line 8: Parse the directive or name/value pair and set the indicated property in the working profile.
Summary
On this page, we learned about the Stream.Builder class. Then, we implemented the ProfileParser facility, which can read or write streams of Profile properties formatted as directives (PROFILE or CLASS) or name/value pairs such as stroke 3. On the next page, we’ll write a JUnitTest for the ProfileParser class.