This post is part of a series on an ongoing project. Today I cover the last step to get the basic function of the app complete! And that topic is generating the output to display back to the user. Let’s start by looking at how the legacy code generates the output
Legacy Output
In the existing code, the output is generated as plain text. It would be nice to use HTML to generate this output in the web app.
And the output is all generated from the Warnings class. You’ve seen this class before. The existing code uses it as an ongoing collector for status as the code analyzes the data in the spreadsheets. The analysis code logs errors and other notable conditions through the addWarning() and addWorkerWarning() functions. The Warnings class organizes the individual warnings into a collection that isn’t necessary to explain here.
The relevant part is that the Warnings class provides a function to get the output. The function is called getOutput(). And this is where we’re going to start implementing the changes. So let’s take a look at how the code currently generates the output. Here is the start of the outputWorkerWarnings() function.
private static final void outputWorkerWarnings( StringBuilder buffer, Map<String,Map<Name,WarningWorker>> map )
{
boolean first = true;
for( Map.Entry<String,Map<Name,WarningWorker>> entry : map.entrySet() )
{
String type = entry.getKey();
Map<Name,WarningWorker> type_map = entry.getValue();
if( ( type == null ) || ( type.length() < 1 ) || ( type_map == null ) || ( type_map.isEmpty() == true ) )
continue;
if( first == true )
first = false;
else
buffer.append( "\n--------------------------------------------------------\n" );
buffer.append( type );
buffer.append( "\n" );
The code loops through all the warnings by type (the String variable). Each subsequent time through the loop, it outputs a line of dash characters, but not on the first time. Then it outputs a line consisting of the ‘type’ of warnings.
What follows next is a lot of Java collection manipulation. I’m not changing any of that stuff, so I’ll skip it. All you need to know is that following about 50-60 lines of Java code is that we now have three parallel lists (one for the worker name, one for a printable version of the worker’s name, and one for a text string describing the warning).
The last step the code takes is to output this information, along with the dates on which the errors occured. These are the last steps before the loop (and function) finish. So here are the final lines of the outputWorkerWarnings() function. I’ve added the comments identifying the end of the for loop and function just for clarity.
Iterator<Name> name_iter = name_list.iterator();
Iterator<String> name_str_iter = name_string_list.iterator();
Iterator<String> warning_iter = warning_list.iterator();
while( name_iter.hasNext() )
{
Name name = name_iter.next();
String name_str = name_str_iter.next();
String warning = warning_iter.next();
buffer.append( " " );
buffer.append( name_str );
buffer.append( warning );
WarningWorker warning_worker = type_map.get( name );
outputDates( buffer, warning_worker.getDates() );
buffer.append( "\n" );
}
} // End of for loop
} // End of outputWorkerWarnings()
Protecting with Test Cases
One of the challenges of this project is that there are no tests protecting the current code. In his excellent book, Working Effectively With Legacy Code, Michael Feathers talks about searching for areas in legacy code that can be protected by tests. He recommends finding these areas, and creating test cases around them before making any changes to the legacy code. That’s the technique I’m going to use here.
This feature of generating output to the user is a great place to implement some test cases. The output is simple (plain text) and easy to test with known conditions. And this is code that’s going to have to change to product HTML output. I keep these tests manual, as changing the code to use automated test cases would require some significant architecture changes, and I don’t want to introduce that level of risk in this project.
But I can run through the sample input files provided by the customer and capture the text output into files. Then I will simply run a test to validate that the content of the file equals the content of the text output. Before I make any changes to the code, I run my test cases and see that everything looks good.
OutputFormatter
To change the format of the output, I’m first going to modify the code to allow the format of the output to be extensible. The code in the Warnings class isn’t going to know what kind of format it is producing.
To do this, I’ll create an interface named ‘OutputFormatter’. And one of the super cool features of Kotlin is that it’s interoperable with Java code. So not only can I call Java code from Kotlin (as I’ve done with the Spring REST controller), I can also call Kotlin code from Java! I’m going to write the OutputFormatter interface and its implementing classes all in Kotlin. It is listed below.
interface OutputFormatter {
fun getText(): String
fun addErrorHeader()
fun addErrorAndWorkerErrorSeparator()
fun addErrorTrailer()
fun addWarningHeader()
fun addWarningAndWorkerWarningSeparator()
fun addWarningTrailer()
fun addFooterForReportWithErrors()
fun addSeparatorBetweenEachWorkerWarningType()
fun addWorkerWarningType(type: String)
fun addWorkerWarningsPrefix()
fun addWorkerWarningsPostfix()
fun addWorkerWarning(name: String, warning: String, dates: Set<Calendar>)
fun addSeparatorBetweenEachWarningType()
fun addWarningType(type: String)
fun addWarningsPrefix()
fun addWarningsPostfix()
fun addWarning(message: String)
}
Then, I’ll change all the code in Warnings that generted the text output to, instead, call the appropriate function. Additionally, whatever code that calls the Warnings.outputWorkerWarnings() function, and others like it, need to pass in an OutputFormatter as a parameter.
Obviously, I’ll also have to create a class that implements the OutputFormatter interface. So I create the OutputFormatterText class, which should create plain text output in the exact same format as the code did before I implemented any changes. It’s code that turns pretty long, and I won’t bore you with all the details, so let’s just take a look at how the code we’ve already examined changes.
Here is the new start of Warnings.outputWorkerWarnings() function. You’ll see a new OutputFormatter parameter, and you’ll see the changes that call functions of that parameter.
private static void outputWorkerWarnings(OutputFormatter formatter, Map<String,Map<Name,WarningWorker>> map )
{
boolean first = true;
for( Map.Entry<String,Map<Name,WarningWorker>> entry : map.entrySet() )
{
String type = entry.getKey();
Map<Name,WarningWorker> type_map = entry.getValue();
if( ( type == null ) || ( type.length() < 1 ) || ( type_map == null ) || (type_map.isEmpty()) )
continue;
if(first)
first = false;
else
formatter.addSeparatorBetweenEachWorkerWarningType();
formatter.addWorkerWarningType(type);
Now lets’ turn to the end of the function.
Iterator<Name> name_iter = name_list.iterator();
Iterator<String> name_str_iter = name_string_list.iterator();
Iterator<String> warning_iter = warning_list.iterator();
while( name_iter.hasNext() )
{
Name name = name_iter.next();
String name_str = name_str_iter.next();
String warning = warning_iter.next();
WarningWorker warning_worker = type_map.get( name );
formatter.addWorkerWarning(name_str, warning, warning_worker.getDates());
}
formatter.addWorkerWarningsPostfix();
} // End of for loop
} // End of outputWorkerWarnings()
As a side bonus, replacing this code with the OutputFormatter interface allows me to name those functions. This let’s me put a descriptive name into the code so the next time someone looks at it, it’s a little more clear what the code is doing.
OutputFormatterText
Now let’s dive into the OutputFormatterText class. Again, this class is written in Kotlin and implements the OutputFormatter interface. Here are the functions called from the code I listed above.
override fun addSeparatorBetweenEachWorkerWarningType() {
buffer.append("\n--------------------------------------------------------\n")
}
override fun addWorkerWarningType(type: String) {
buffer.append(type)
buffer.append("\n")
}
override fun addWorkerWarningsPrefix() {}
override fun addWorkerWarningsPostfix() {}
override fun addWorkerWarning(name: String, warning: String, dates: Set<Calendar>) {
buffer.append(" ")
buffer.append(name)
buffer.append(warning)
buffer.append(outputDates(dates))
buffer.append("\n")
}
The last part is to create an OutputFormatterText and pass it into the Warnings functions. That’s a simple change in the Warnings.getOutput() function. The new version looks like this.
public String getOutput() {
return getOutput(new OutputFormatterText());
}
outputDates()
If you’ve noticed, there is a function call to the outputDates() function embedded in both the legacy code and my new Kotlin code. As part of this project, I move and rewrite this function. It’s small in scope, so there’s not much to rewrite. And I’m protecting the output generation with test cases, so I can be sure I’m not breaking anything. Let’s take a quick detour to examine the before and after versions of this function.
Here it is in the legacy code, as a private static function in the Warnings class.
private static void outputDates( StringBuilder buffer, Set<Calendar> dates )
{
if( ( dates != null ) && ( dates.isEmpty() == false ) )
{
buffer.append( " - " );
List<Calendar> list = new ArrayList<Calendar>( dates.size() );
list.addAll( dates );
Collections.sort( list );
boolean first = true;
for( Calendar date : list )
{
if( first == true )
first = false;
else
buffer.append( ", " );
buffer.append( Util.calendarToString( date ) );
}
}
}
Uhhh, that’s a whole lot of code to format a list of dates. Let’s see how Kotlin can shape this up. In the Kotlin code, I’ll define this function in the outputCollector.kt file, but it will simply be a function defined outside of any class. I’ll also mark it as ‘internal’ so it isn’t visible outside the project. And here it is.
internal fun outputDates(dates: Set<Calendar>?): String {
if((dates == null) || dates.isEmpty())
return ""
return dates.toSortedList().joinToString(
prefix = " - ",
separator = ", ",
transform = {
Util.calendarToString(it)
}
)
}
That’s 22 lines of Java code replaced by 12 lines in Kotlin. Is it more clear what’s going on here? I don’t think there’s any question about that. This is just one reason that, to any Java developer, Kotlin is a breath of fresh air!
OutputFormatterHtml
Okay, I’ve replaced the code in Warnings with an interface and a class that implements it. After doing that, I’ve used my test cases to validate that the code is still producing the exact same output. Now it’s time to upgrade the output to HTML.
And that’s easy! I just need to create a new new class that implements OutputFormatter and generates HTML in the functions. So let’s take a loot at the OutputFormatterHtml class. Here are the same functions in the HTML flavor.
override fun addSeparatorBetweenEachWorkerWarningType() {
buffer.append("<hr/>")
}
override fun addWorkerWarningType(type: String) {
buffer.append(type)
buffer.append("<br/>")
}
override fun addWorkerWarningsPrefix() {
buffer.append("<ul>")
}
override fun addWorkerWarningsPostfix() {
buffer.append("</ul>")
}
override fun addWorkerWarning(name: String, warning: String, dates: Set<Calendar>) {
buffer.append("<li>")
buffer.append(escape(name))
buffer.append(escape(warning))
buffer.append(escape(outputDates(dates)))
buffer.append("</li>")
}
And to call this new class, I add a new function to Warnings and call it getOutputAsHtml(). It’s a super simple function and is shown below.
public String getOutputAsHtml() {
return getOutput(new OutputFormatterHtml());
}
Gluing It Together
Now all that remains is to return the output to the user in response to the analyze request. But I’ve built all the ground work, so this should be pretty simple to plug in. Let’s return to the tryToAnalyzeTime() function that I covered in the previous post. A few changes to the function is all that’s needed. Let’s take a look at the changes.
private fun tryToAnalyzeTime(files: AnalyzeFiles, params: AnalyzeParams?): AnalyzeResponse {
// Code from previous example
val costMap = CostMap.create(projectSheet, analyzedData, context, warnings)
val exportFile = createExportFile(warnings, costMap, getExportDate(params, context))
return createResponse(warnings, timeReport, params, context, exportFile)
}
Ignore the CostMap.create() function because that’s pretty specific to the existing business rules, and I covered the createResponse() function in the previous post. But let’s dive into the createExportFile() function before wrapping up. The function is listed below.
private fun createExportFile(warnings: Warnings, costMap: CostMap, exportDate: Calendar): String? {
val output = ByteArrayOutputStream()
return try {
TimeTracking.generateIifFile(PrintWriter(output), costMap, exportDate)
saveFile(output.toByteArray())
} catch(t: Throwable) {
warnings.addWarning(Warnings.WarningThrowable(t))
null
}
}
So, the code calls one of the legacy functions to create the export file (an .iif file – which is an import file for QuickBooks), and it stores the content of the file into a temporary memory buffer, the byte array output stream. Then it returns the result of calling the safeFile() function to save it to the local storage on the server.
If you recall, from the post on uploading files, this is the same function used when the user uploads an Excel file. It generates a name for the file, based on a MD5 hash of the file content, and then saves it under the /var/tmp/.jobcost directory, and returns the name of the file.
And that’s all there is for today! I now have a basic function of the app working correctly and tested. What’s left? There’s a few clean up items like cleaning up the temporary files and securing the password transmission, and then this one will be all done.