Adding a spell checker to a JavaFX application has been made easier thanks to the good folks over at languagetool.org.

In this article, I'm going to show you where to get the languagetool library, how to add it to a JavaFX project, and how to implement a simple spell checking functionality in the app using languagetool. I'm not going to walk through starting a new JavaFX project because I'm assuming that if you're reading this article that you probably already have a project that needs a spell checking capability.

Where to get the library

You can download the library using the link languagetool 3.0 snapshot and downloading the languagetool-standalone3.X.zip file. Make sure you download the zip file and not one of the jar files.

download page

What do to with the library

Now that you have the zip file, go ahead and extract its contents to any folder except your project folder. We are going to have to create a jar file from some of the contents in this zip file.

Once you've unzipped the snapshot archive navigate to the new folder with finder, explorer, bash, or whatever you're comfortable with.

unzipped folder structure

Languagetool needs the META-INF and org folders in a jar file, so our next step is to create that.

Since I'm on a Mac, I'm going to do this using bash, but creating a jar is as simple as creating a .zip file and renaming it to .jar. On my Mac I use the zip command in the terminal:

zip -r languages.jar org/ META-INF/

This will create the languages.jar file that contains the contents of the org folder and META-INF folder.

jar contents

Now that we have the jar file created we need to add it, and some other dependencies to our projects build path.

I'm using Eclipse so if you're using another IDE you'll need to refer to that IDE's knowledgebase for details on how to add libraries to a project.

Add the languages.jar to your build path, then add the following jars as well:

  • languagetool.core.jar
  • segment.jar
  • commons-logging.jar
  • hccp.jar
  • all the morfologik jars
  • all the opennlp jars

library selection

If you were following this tutorial in a clean JavaFX project your build path configuration would look something like:build path configuration

Now you should have everything configured correctly to start using languagetool for spell checking.

Using the library

Finding the correct library and figuring out which libraries to add to the build path was the hardest part of this project. Actually using the library is very simple and the API is pretty well documented.

GUI

The first step is to design a simple GUI to test out the languagetool library. The three images below show my final bland fxml views.

  • The main window with misspellings:main window

  • The spell checker popup with the misspelled word and suggestions:spell checker

  • The main window again with the correct spellings:done

Code

Below is all the code I needed to get the spell checker to show the misspelled words and suggestions to replace them with.


package application;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.stage.Stage;

import org.languagetool.JLanguageTool;
import org.languagetool.language.AmericanEnglish;
import org.languagetool.rules.RuleMatch;

/******************************************************************************
 * SpellCheckController.java
 * @author Jeramy Singleton
 * Description: SpellCheckController handles the spell checking functionality
 * of the SpellChecker view.
 *******************************************************************************/
public class SpellCheckerController {

    // FXML Fields
    @FXML private Button ignore; 
    @FXML private Button ignoreAll; 
    @FXML private Button change; 
    @FXML private Button changeAll; 
    @FXML private Button accept; 
    @FXML private Button cancel;
    @FXML private TextField misspelled;
    @FXML private TextField changeTo;
    @FXML private ListView<String> suggestions;


    private TextArea input;         // input text for the spell checker
    private String replacementText; // the replacement text that will be 
                                    // returned to the input text area
    private List<String> misspells; // list of misspelled words
    private ObservableList<String> suggestionsList; // list for suggestions 
                                                    // listview
    List<RuleMatch> matches;      // list of misspellings
    private int currentIndex;       // index for current position in misspells list

    /******************************************************************************
     * SpellCheckerController
     * Default constructor.
     *******************************************************************************/
    public SpellCheckerController(TextArea input) 
    {
        currentIndex = -1;
        this.input = input;
        replacementText = input.getText();
        misspells = new ArrayList<>();
        matches = null;
        suggestionsList = FXCollections.observableArrayList();
    }

    /******************************************************************************
     * initialize
     * initialize sets up the suggestion list with an event listener that changes
     * the changeTo text to the user selected listcell contents.  initialize also
     * runs the spell checking tool on the input and populates the corresponding
     * lists needed for the rest of the spell checking functionality
     *******************************************************************************/
    public void initialize()
    {
        suggestions.setItems(suggestionsList);
        // set a listener on the list views selection model to get the text of any
        // selected row
        suggestions.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> {
            changeTo.setText(newValue);
        });

        JLanguageTool langTool = new JLanguageTool(new AmericanEnglish());

        try
        {
            matches = langTool.check(input.getText());
            matches.forEach(match -> {
                // to get the actual word that is misspelled we must get the starting
                // position of the word, in the input text, and the end position
                int start = match.getFromPos();
                int end = match.getToPos();
                // use the start and end positions to get the substring of the input
                // text
                misspells.add(input.getText().substring(start, end)); 
            });

            // set the misspelled text area to the first misspelled word
            nextMisspelling(0);
        }
        catch(IOException e) 
        {
            e.printStackTrace();
        }

    }

    /******************************************************************************
     * nextMisspelling
     * nextMisspelling sets the view up for the next misspelled word and suggestions
     * @param index - the index of the misspelled word in the misspelled list
     *******************************************************************************/
    private void nextMisspelling(int index)
    {
        if(index >= misspells.size())
        {
            disableAndClearControls();
            return;
        }

        currentIndex = index;

        misspelled.setText(misspells.get(index));
        suggestionsList.clear();
        suggestionsList.addAll(matches.get(index).getSuggestedReplacements());
        changeTo.setText(suggestionsList.get(0));
    }

    /******************************************************************************
     * disableAndClearControls
     * disableAndClearControls sets the text fields, buttons, and listview into an
     * unusable state while also resetting the values.
     *******************************************************************************/
    private void disableAndClearControls()
    {
        misspelled.setText("");
        changeTo.setText("");
        suggestionsList.clear();
        ignore.setDisable(true);
        ignoreAll.setDisable(true);
        change.setDisable(true);
        changeAll.setDisable(true);
    }


    /******************************************************************************
     * changeAction
     * changeAction initiates the spelling change and sets up the gui for the next
     * misspelled word
     *******************************************************************************/
    @FXML
    public void changeAction()
    {
        changeMisspelledWord(misspells.get(currentIndex), changeTo.getText());
        nextMisspelling(currentIndex);
    }

    /******************************************************************************
     * changeMisspelledWord
     * changeMisspelledWord updates the replacement text to the value of the string 
     * parameter.
     * @param misspell - the word(s) to replace
     * @param text - the string to change the misspelled word to
     *******************************************************************************/
    private void changeMisspelledWord(String misspell, String replacement) {
        replacementText = replacementText.replace(misspell, replacement);
        currentIndex += 1;
    }

    /******************************************************************************
     * changeAllAction
     * changeAllAction replaces all misspelled words with the default suggestion
     *******************************************************************************/
    @FXML
    public void changeAllAction()
    {
        for(int i = 0; i < misspells.size(); i++)
        {
            String suggestion = matches.get(i).getSuggestedReplacements().get(0);
            if(suggestion != null && !suggestion.isEmpty())
            {
                changeMisspelledWord(misspells.get(i), suggestion);
            }
        }

        disableAndClearControls();
    }


    /******************************************************************************
     * ignoreAction
     * ignoreAction skips the current misspelling and moves on to the next 
     * misspelled word
     *******************************************************************************/
    @FXML
    public void ignoreAction()
    {
        currentIndex += 1;
        nextMisspelling(currentIndex);
    }

    /******************************************************************************
     * ignoreAllAction
     * ignoreAllAction does nothing except call a method to clear and disable the
     * GUI controls on the view
     *******************************************************************************/
    @FXML
    public void ignoreAllAction()
    {
        disableAndClearControls();
    }

    /******************************************************************************
     * acceptAction
     * acceptAction changes the original input text to replacement text that has all
     * the spell checker's changes and closes the spell checker window
     * @param event - the source of the ActionEvent (i.e. a button press)
     *******************************************************************************/
    @FXML
    public void acceptAction(ActionEvent event)
    {
        input.setText(replacementText);
        closeWindow((Button)event.getSource());
    }

    /******************************************************************************
     * closeWindow
     * closeWindow gets the current stage from a control on the view and closes it
     * @param button - the button that is responsible for calling the closeWindow
     * method
     *******************************************************************************/
    private void closeWindow(Button button)
    {
        Stage stage = (Stage) button.getParent().getScene().getWindow();
        stage.close();
    }

    /******************************************************************************
     * cancelAction
     * cancelAction closes the spell checker window without making any changes
     * @param event - the source of the ActionEvent (i.e. a button press)
     *******************************************************************************/
    @FXML
    public void cancelAction(ActionEvent event)
    {
        closeWindow((Button)event.getSource());
    }
}

I know my code isn't the best. It's probably down right horrible, but it works so I'm not too ashamed of it.

Now back to languagetool.

Languagetool

The majority, well actually all of the spell checking work is performed in the initialize method. You can put this code anywhere, it just thought it was convenient to do it here.

I initiate the languagetool at line 73

JLanguageTool langTool = new JLanguageTool(new AmericanEnglish());

The JLanguageTool constructor is where the language, or dialect of a language, is passed to the languagetool so that it knows which dictionary to query.

Once the languagetool has been instantiated we have to pass our input to it and get back any misspelled words, which languagetool calls a RuleMatch.

The JLanguageTool object passes back a list of RuleMatch objects when you pass the input to the check method:

matches = langTool.check(input.getText());

One thing I found odd about the languagetool is that it doesn't actually pass back the misspelled words. It passes back their positions in the input string. That isn't a big deal though, because we can just as easily get the word out of the string using the start position and end position of the RuleMatch. In the code below, I get each misspelled word from the list of RuleMatches and place the word into a list of strings:


matches.forEach(match -> {
                // to get the actual word that is misspelled we must get the starting
                // position of the word, in the input text, and the end position
                int start = match.getFromPos();
                int end = match.getToPos();
                // use the start and end positions to get the substring of the input
                // text
                misspells.add(input.getText().substring(start, end)); 
            });

Once I have the list of actual misspelled words I get a list of word suggestions for each misspelled word from the languagetool.

suggestionsList.addAll(matches.get(index).getSuggestedReplacements());

Wrap up

That is basically all that is needed to implement a simple spell checker using the languagetool library. My implementation wasn't anything ground breaking nor will it win any design awards anytime soon, but it gets the job done. The next logical step would be to implement a smarter spell check into the actual textarea itself, but that is beyond the scope of this article.