5
\$\begingroup\$

I am working on an application which takes as input a CSV file containing a list of attributes and a .docx template containing variable names equal to the columns of the CSV, and generates one new .docx for each tuple of the CSV, with the variables replaced by the contents of each 'cell'.

The application uses a point-and-click Scala Swing GUI where the end user can provide the files for input, and the destination for the output.

I tried to make the code as clean as possible, but I still would like to know how it can be improved. I personally think the InteractionMediator class is doing too much, and would like to think of ways to delegate some of its tasks to other entities in a clean way.

I would also like to make it more testable. I know about TDD, but have not really used it while creating this. I'm thinking that maybe I should do a rewrite and do TDD this time.

The InteractionMediator class below is the one which coordinates most of the work, so I am posting it here. But if anyone is interested in the source code for all the other entities it can be found on my GitHub repo.

package lettergenerator
package renderer
import formatter._
import org.docx4j.XmlUtils
import org.docx4j.wml.Document
import org.docx4j.jaxb.Context
import org.docx4j.openpackaging.io.SaveToZipFile
import org.docx4j.openpackaging.packages.WordprocessingMLPackage 
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart
import scala.swing.MainFrame
import java.util.{HashMap => JHashMap}
import scala.annotation.tailrec
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
case class InteractionMediator() {
 private var gui: Wizard = _ 
 def registerInterface(gui: MainFrame): Unit = this.gui = gui.asInstanceOf[Wizard]
 def hasGui: Boolean = Option[MainFrame](gui) match {
 case Some(_) => true
 case None => false
 }
 def runInterface(): Unit = gui.visible = true
 def messageUser(text: String): Unit = gui.message(text)
 def submit(): Unit = {
 messageUser("Processing...")
 Future { validatePaths(PathValidator()) }
 }
 def validatePaths(validator: Validator ): Unit = {
 val paths = List[(String,String)](
 "details file" -> gui.detailsFile,
 "template file" -> gui.templateFile,
 "destination folder" -> gui.destinationFolder
 )
 val message = "Could not reach the %s. Please check if path is correct"+
 ", or report this issue"
 vldt[String](
 paths,
 validator,
 loadDetails(DetailsFormatter(CsvInput(gui.detailsFile))),
 message)
 }
 def loadDetails(form: DetailsFormatter): Unit = {
 val details: List[Map[String,String]] = form.details
 val detailsMessage = "Details file error: the row with values "+
 "%s is incomplete. Please check it and try again" 
 validateDetails(details,DetailsValidator(form.headers), detailsMessage)
 }
 def validateDetails(details: List[Map[String,String]],
 validator: DetailsValidator, message: String): Unit = {
 var flag = false
 try {
 for (mapElement <- details) 
 vldt[Map[String,String]](
 List((mapElement.values.mkString(" "),mapElement)), 
 validator, 
 flag = true,
 message)
 } catch {
 case e: Throwable => {
 gui.message("Error")
 e.printStackTrace()
 gui.alert(e.getStackTrace.mkString("\n"))
 }
 }
 if(flag) loadTemplate(details)
 }
 def loadTemplate(details: List[Map[String,String]]): Unit = {
 val docPack: WordprocessingMLPackage = 
 TemplateFormatter(DocxInput(gui.templateFile))
 .template
 validateTemplate(details, docPack)
 }
 def validateTemplate(details: List[Map[String,String]], 
 docPack: WordprocessingMLPackage): Unit = {
 val docText: String = WordMLToStringFormatter(docPack).text
 val validator = TemplateValidator(docText)
 val message: String = "Error: could not find variable %s on template."
 val headers: List[(String,String)] = gui.fnAlsoInTemplate match {
 case true => details.head.keySet.map(header => (header,header)).toList
 case false => details.head.keySet.filter(_ != gui.fNameColumn)
 .map(header => (header,header)).toList
 }
 vldt[String](headers,validator,generateLetters(details,docPack),message)
 }
 def generateLetters(details: List[Map[String,String]],
 docPack: WordprocessingMLPackage): Unit = {
 import scala.collection.JavaConverters._
 val destination: String = gui.destinationFolder
 val template: MainDocumentPart = docPack.getMainDocumentPart
 val duplFileChecker = PathValidator()
 @tailrec
 def fileName(name: String, counter: Int): String = {
 val increment = counter + 1
 if (duplFileChecker.validate(destination+"/"+name+".docx")) {
 if (duplFileChecker.validate(destination+"/"+name+increment+".docx")) 
 fileName(name,increment)
 else destination+"/"+name+increment+".docx"
 } else destination+"/"+name+".docx"
 }
 for(smap <- details) {
 val fname = smap.collectFirst({
 case (k: String,v: String) if k == gui.fNameColumn => v
 }) match {
 case Some(file) => file
 case None => "Output"
 }
 val map: JHashMap[String,String] = gui.fnAlsoInTemplate match {
 case true => new JHashMap(smap.asJava)
 case false => new JHashMap(smap.filter(_._1 != gui.fNameColumn).asJava)
 }
 val jaxbElement = template.getJaxbElement
 val xml: String = XmlUtils.marshaltoString(jaxbElement, true)
 val replaced: Object = XmlUtils.unmarshallFromTemplate(xml, map)
 template.setJaxbElement(replaced.asInstanceOf[Document])
 new SaveToZipFile(docPack).save(s"${fileName(fname,0)}")
 template.setJaxbElement(jaxbElement)
 }
 messageUser("Done!")
 }
 @tailrec
 private def vldt[A](p: List[(String,A)], validator: Validator,
 op: => Unit, message: String): Unit = p match {
 case Nil => 
 throw new IllegalArgumentException("argument p cannot be an empty list")
 case x :: Nil => validator.validate(x._2) match {
 case true => op
 case false => messageUser(message.format(x._1))
 }
 case x :: xs => validator.validate(x._2) match {
 case true => vldt(xs,validator,op,message)
 case false => messageUser(message.format(x._1))
 }
 }
 def columnsForFileName(): List[String] = {
 val path: List[(String,String)] = List(("details file", gui.detailsFile))
 val validator = PathValidator()
 val message = "Could not reach the %s. Please check if path is correct"+
 ", or report this issue"
 var columns = List[String]()
 vldt[String](path, validator, columns = DetailsFormatter(
 CsvInput(path.head._2)).details.head.keySet.toList, message)
 List("") ++ columns
 } 
}



Below is the code for the GUI. This is the second time I use Swing, and the first time I use it in Scala, so I am still finding my bearings with it. So it would be very helpful if anyone could advise on bad practices or things which I could do to make the code cleaner:

package lettergenerator
package renderer
import scala.swing._
import scala.swing.event._
import java.io.File
import javax.swing.filechooser.FileNameExtensionFilter
/**
 * the main frame. Responsible for laying out the elements
 * @param medium an InteractionMediator object
 */
class Wizard(medium: InteractionMediator) extends MainFrame {
 title = "Letter Maker Wizard" 
 preferredSize = new Dimension(695,360)
 val TextWidth = 56
 // for making the buttons, labels and textfields
 val elementMkr = ElementMaker()
 // for opening files and directories
 private val csvOpener = new FileChooser(new File("."))
 csvOpener.fileFilter = (new FileNameExtensionFilter("CSV (Comma Separated Values)","csv"))
 private val docxOpener = new FileChooser(new File("."))
 docxOpener.fileFilter = (new FileNameExtensionFilter("Word Document","docx"))
 private val dirOpener = new FileChooser(new File("."))
 dirOpener.fileSelectionMode = FileChooser.SelectionMode.DirectoriesOnly
 // source of letter header details
 private val (dtLbl, dtTxt, dtBtn) = 
 elementMkr.mkOpenFileElmts("Please choose the file with the"
 + " details which will go on the letters", csvOpener, TextWidth)
 // drop down box for file name column 
 private var textChangeFlag = dtTxt.text
 private val fileNameColumn = new ComboBox(List[String]())
 private val fnLbl = elementMkr.label(" ") 
 def fNameColumn: String = fileNameColumn.selection.item
 // check box to check if file name is also present in template
 // as a variable to be replaced
 private val fnAlsoInTemplate_ = new CheckBox("File name also part of letter")
 fnAlsoInTemplate_.selected = false
 def fnAlsoInTemplate: Boolean = fnAlsoInTemplate_.selected
 // source of letter template
 private val (tpltLbl, tpltTxt, tpltBtn) = 
 elementMkr.mkOpenFileElmts("Please choose the file with the "
 + " template for the letters", docxOpener, TextWidth)
 // destination folder
 private val (destLbl, destTxt, destBtn) = 
 elementMkr.mkOpenFileElmts("Please choose a destination " 
 + "folder for the letters", dirOpener, TextWidth)
 private val msg: Label = elementMkr.label("Ready")
 def message(text: String): Unit = msg.text = text
 def alert(text: String): Unit = Dialog.showMessage(this,text,"Alert")
 listenTo(dtTxt)
 reactions += { case ValueChanged(dtTxt) => comboBoxRoutine() }
 setMaxHeight(dtTxt)
 setMaxHeight(tpltTxt)
 setMaxHeight(destTxt)
 setMaxHeight(fileNameColumn)
 val VShortGap: Int = 5
 val VLargeGap: Int = 30
 val HShortGap: Int = 3
 contents = new BoxPanel(Orientation.Vertical) {
 contents += new BoxPanel(Orientation.Vertical) {
 contents += dtLbl
 contents += Swing.VStrut(VShortGap)
 contents += new BoxPanel(Orientation.Horizontal) {
 contents += dtTxt
 contents += Swing.HStrut(HShortGap)
 contents += dtBtn
 }
 contents += Swing.VStrut(VShortGap)
 contents += new BoxPanel(Orientation.Vertical) {
 contents += fnLbl
 contents += Swing.VStrut(VShortGap)
 contents += new BoxPanel(Orientation.Horizontal) {
 contents += fileNameColumn
 contents += Swing.HStrut(HShortGap)
 contents += fnAlsoInTemplate_
 }
 }
 }
 contents += Swing.VStrut(VLargeGap)
 contents += new BoxPanel(Orientation.Vertical) {
 contents += tpltLbl
 contents += Swing.VStrut(VShortGap)
 contents += new BoxPanel(Orientation.Horizontal) {
 contents += tpltTxt
 contents += Swing.HStrut(HShortGap)
 contents += tpltBtn
 }
 }
 contents += Swing.VStrut(VLargeGap)
 contents += new BoxPanel(Orientation.Vertical) {
 contents += destLbl
 contents += Swing.VStrut(VShortGap)
 contents += new BoxPanel(Orientation.Horizontal) {
 contents += destTxt
 contents += Swing.HStrut(HShortGap)
 contents += destBtn
 }
 }
 contents += Swing.VStrut(VShortGap)
 contents += new BoxPanel(Orientation.Horizontal) {
 contents += elementMkr.button("Generate Letters", submit())
 contents += Swing.HGlue
 }
 contents += Swing.VStrut(VShortGap)
 contents += new BoxPanel(Orientation.Horizontal) {
 contents += msg
 contents += Swing.HGlue
 }
 for (e <- contents)
 e.xLayoutAlignment = 0.0
 border = Swing.EmptyBorder(10, 10, 10, 10)
 }
 def setMaxHeight(comp: Component) = 
 comp.maximumSize = new Dimension(Short.MaxValue, comp.preferredSize.height)
 def submit(): Unit = medium.submit() 
 def detailsFile: String = dtTxt.text
 def templateFile: String = tpltTxt.text
 def destinationFolder: String = destTxt.text
 def comboBoxRoutine(): Unit = {
 if (dtTxt.text != textChangeFlag) {
 fileNameColumn.peer.setModel(
 ComboBox.newConstantModel(
 medium.columnsForFileName()))
 textChangeFlag = dtTxt.text
 fileNameColumn.selection.item = ""
 if (fileNameColumn.peer.getModel.getSize > 1) 
 fnLbl.text = "Please select the column which contains "+
 "the file names for the new documents"
 }
 }
}
asked Aug 21, 2017 at 9:50
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

The code you posted is rather huge for all-in-one review, so I suggest to make it in more than one iteration.

For this first approach, let's cut more general things from details.

InteractionMediator

Type. It is declared as case class. Is it intended to be used in matchers or to be tested for equality among different instances? If not, that should not be a case class.

Functions. There are really a lot of public defs inside. Are they all called from outside or some of them remain callable from within the class? I think that it should be useful to reduce the visibility of some of them.

Moreover, these functions do many different things and this makes me doubt that InteractionMediator should wrap all this stuff. There are functions to validate details or templates, load them or generate other entities. It looks that a split is necessary: create an entity that will provide validation, another for loading, and yet another for other useful actions. This will be much easier to read, analyze and test.

Wizard

This class looks like a big mess of vars, vals, defs and directly executed calls. Really difficult to see what is happening there. Reordering, restructuring and splitting are the first things to do about.

Some ideas:

  1. Do not hesitate to extract the initialization of FileChoosers, ComboBoxetc in dedicated methods, it will allow to instantiate the respective fields in one line without messy lines after.

  2. Group the instructions executed directly through the constructor into a dedicated function (that might also call other functions of grouped calls) and call this function once, as close as possible to the header of the class.

Once this initial refactoring is done, it would be much easier to understand how the entire thing works and to review the details.

answered Aug 23, 2017 at 9:11
\$\endgroup\$
3
  • \$\begingroup\$ Great, this is a good start already. I may need a couple of days to work on all the changes, as this is a non-work related project so I'm using my free time for it. When I have made them all, I'll edit the question with the new source code. Thanks for taking the time to respond! \$\endgroup\$ Commented Aug 23, 2017 at 20:41
  • 1
    \$\begingroup\$ @claudiusbr: you are welcome. But when the changes in the code are ready, it would be better to post them as another "follow up" question, without editing this one. It keeps questions and answers more consistent. \$\endgroup\$ Commented Aug 23, 2017 at 20:53
  • \$\begingroup\$ Ah! Great. I was actually also going to ask about how to move forward question-wise, but wanted to re-read the guidelines first before I did. I'm still gonna re-read them, in any case, but thanks for the pointer. I'll accept this answer then, as it has already given me a lot to work on, and will post the second iteration as a follow up. Thanks again. \$\endgroup\$ Commented Aug 23, 2017 at 20:57

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.