Skip to Content

This blog entry discusses implementation of SpellChecker UI component in WebDynpro using JazzyAPI.

Jazzy overview

Jazzy is an open-source (LGPL) spell checker implementation (core algorithms similar to used by aspell and set of convenient Swing components / utilities). Since its inception Jazzy has gained great popularity among developers and has been incorporated into numerous products such as JEdit and Roller (see Jazzy homepage for more). This example project uses only a small subset of core Jazzy functionality, please download complete binaries and sources at Jazzy project page on SourceForge.

Embedding Jazzy

In example project I strived to have minimal dependency on concrete spell checker implementation. Therefore I was using only small subset of Jazzy functionality.

Core Jazzy functionality may be roughly divided up to two parts: dictionaries implementation (SpellDictionary and related) and spell-checking algorithm itself (SpellChecker and events). I prefer to use the simplest form of either of them to keep code small.

Here is code to load spelling dictionary (Component Controller of SpellCheckerX component):

public void wdDoInit() { //@@begin wdDoInit() try { final BufferedReader dReader = new BufferedReader ( new InputStreamReader ( getClass().getClassLoader().getResourceAsStream ( "com/swabunga/dict/english.txt" ) ) ); _mainDictionary = new SpellDictionaryHashMap( dReader ); } catch (final IOException ex) { wdComponentAPI.getMessageManager().reportException ( new WDNonFatalException(ex), false ); } //@@end }

The functionality above is obvious: we are loading the complete English dictionary in memory. Sure, in real-life it is not a best approach, and Jazzy provides several implementations that balance in-memory / disk access in smart way. Please study original distribution for details.

Other part is spelling check itself. Jazzy used event-based model of stream parsing: you register listener on instance of SpellCheker and receive events as far as checker encounteres misspelled words. Jazzy spell checker provide common set of handful methods to handle spelling errors (ignoring, adding to user dictionary etc.) But in our case we just listen to errors and construct list of valid text fragments / misspelled words as node elements:

public void run( java.lang.String text, com.swabunga.spell.engine.SpellDictionary userDictionary ) { //@@begin run() if ( null == _mainDictionary ) { wdComponentAPI.getMessageManager().reportException ( "Dictionary was not initialized properly", false ); return; } final IPublicSpellCheckerX.IWordsNode nWords = wdContext.nodeWords(); final SpellChecker currentSpelling = new SpellChecker(_mainDictionary); if ( null != userDictionary ) currentSpelling.setUserDictionary( _userDictionary = userDictionary ); final String fText = text; final int[] acurrent = {0}; final SpellCheckListener listener = new SpellCheckListener() { public void spellingError(final SpellCheckEvent event) { final int current = acurrent[0]; final int pos = event.getWordContextPosition(); if ( pos > current ) createValidText( fText, current, pos ); final String errWord = event.getInvalidWord(); final IPublicSpellCheckerX.IWordsElement elWord = nWords.createWordsElement ( new TextEntry( errWord, false ) ); nWords.addElement( elWord ); acurrent[0] = pos + errWord.length(); final List suggestions = event.getSuggestions(); final IPublicSpellCheckerX.ISuggestionsNode nSuggestions = elWord.nodeSuggestions(); for (final Iterator i = suggestions.iterator(); i.hasNext(); ) { final Word suggestion = (Word)i.next(); final IPublicSpellCheckerX.ISuggestionsElement elSuggestion = nSuggestions.createSuggestionsElement(); elSuggestion.setWord( suggestion.getWord() ); nSuggestions.addElement( elSuggestion ); } } }; nWords.invalidate(); currentSpelling.addSpellCheckListener(listener); currentSpelling.checkSpelling( new StringWordTokenizer(fText) ); // Adding valid tail if ( acurrent[0] < fText.length() ) createValidText( fText, acurrent[0], fText.length() ); selectNextMisspelledWord( 0 ); wdThis.wdFireEventContextModified(); //@@end }

Note, that along with populating texts, method adds related suggestion as well. TextEntry class here is just regular JavaBean. Sure, as far as it is used by IWDNodeElement we have corresponding JavaBean’s model imported. See my previous post for details.

The remaining part of functionality is classical replace(all) / ignore(all) / add-to-dictionary trio. You may see in sources that second & third method simply delegates to first one after minimal pre-processing:

public void replace( java.lang.String text, boolean isAll ) { //@@begin replace() final IPublicSpellCheckerX.IWordsElement elWord = wdContext.currentWordsElement(); if ( null == elWord ) return; final ITextEntry theWord = elWord.modelObject(); final String match = theWord.getText(); theWord.validate( text ); if ( isAll ) { final IPublicSpellCheckerX.IWordsNode nWords = wdContext.nodeWords(); final int size = nWords.size(); for (int i = elWord.index(), c = 0; c < size; i = (i + 1) % size, c++ ) { final ITextEntry entry = nWords.getWordsElementAt( i ).modelObject(); if ( entry.getIsValid() ) continue; if ( match.equals( entry.getText() ) ) entry.validate( text ); } } selectNextMisspelledWord( elWord.index() + 1 ); //@@end } public void ignore( boolean isAll ) { //@@begin ignore() final IPublicSpellCheckerX.IWordsElement elWord = wdContext.currentWordsElement(); if ( null == elWord ) return; replace( elWord.getText(), isAll ); //@@end } public void add2dictionary( ) { //@@begin add2dictionary() final IPublicSpellCheckerX.IWordsElement elWord = wdContext.currentWordsElement(); if ( null == elWord ) return; if ( null != _userDictionary) _userDictionary.addWord( elWord.getText() ); ignore( true ); //@@end }

This is all for core spell checking functionality. Forward to most complex part of this article – spell checker UI.

Spell Checker UI

Ok, let us start designing our view as usual: add Component Controller to the list of required controllers and create necessary mappings. However, shortly (even right now!) the question should rise: how to display list of text fragments in user-friendly fashion (like IWDTextView) using structure at hand. It would be trivial to display it as table, but it’s plain ugly.

After some experiments I came to the following solution:

  1. Use IWDUIElementContainer with FlowLayout + IWDTextView for every fragment. Visually it looks like just regular streaming text. In addition IWDTextView provides way to define semantic color, hence it is possible to highlight misspelled words.
  2. As far as IWDTextView may display content of one and only one node element we have to modify our original data structure. Namely, for every text fragment in Words context node it is required to create dynamically one node with cardinality 0..1 / 1..1 to hold related data.
image
Image 1. SpellChecker UI
 

Sounds simple, but take a look to the code that transforms original structure into UI-specific one (in response to event from Component Controller):

public void onEventContextModified(com.sap.tc.webdynpro.progmodel.api.IWDCustomEvent wdEvent ) { //@@begin onEventContextModified(ServerEvent) wdContext.getContext().reset( false ); wdContext.currentContextElement().setIsContextValid( false ); final IWDNodeInfo niProto = wdContext.nodeDynaWordProto().getNodeInfo(); final ICMIModelClassInfo cmiClass = new CMIInfo.ClassInfoByPrototype( IUITextEntry.class.getName(), niProto ); final IWDNodeInfo niRoot = wdContext.nodeDynamicWords().getNodeInfo(); final IPrivateSpellCheckerXCV.IDynamicWordsNode nRoot = wdContext.nodeDynamicWords(); final IPrivateSpellCheckerXCV.IWordsNode nWords = wdContext.nodeWords(); for (int i = 0, size = nWords.size(); i < size; i++) { final IPrivateSpellCheckerXCV.IWordsElement elWord = nWords.getWordsElementAt(i); final IWDNodeInfo niThisDynaWord = niRoot.addChild ( "word_" + i, IUITextEntry.class, niProto.isSingleton(), // singleton niProto.isMandatory(), // non-mandatory niProto.isMultiple(), // non-multiple niProto.isMandatorySelection(), // mandatory selection, niProto.isMultipleSelection(), // non-multiple selection, true, // initialize lead selection niProto.getDataType(), null, null // no supplier, no disposer ); niThisDynaWord.setModelClassInfo( cmiClass ); niThisDynaWord.addAttributesFromModelClassInfo(); final IUITextEntry entry = new UITextEntry ( elWord.modelObject(), cmiClass, _colors ); nRoot.getChildNode( niThisDynaWord.getName(), 0 ).bind ( Collections.singleton( entry ) ); } selectWord(); //@@end }

For this blog entry I will by-skip discussion of com.sap.sdn.samples.spell.components.sc.core.CMIInfo, com.sap.sdn.samples.spell.components.sc.core.CMIBean and related – it deserves more then several articles to describe how the CMI model helps to keep this code (relatively) compact and simple. Yes, it sounds funny when you look at code above – here are almost all of dynamic context manipulation routines used (besides of mapping, perhaps). But believe me, some real time-saver stayed behind (yet) magic CMI code 😉

For now think about IUITextEntry implementation as simple wrapper over original ITextEntry that adds convenient read-only accessors for UI-specific stuff (like color of text depending on type of entry – regular text fragment or misspelled word).

And, finally, the code for creating related UI controls to represent this structure:

public static void wdDoModifyView ( IPrivateSpellCheckerXCV wdThis, IPrivateSpellCheckerXCV.IContextNode wdContext, com.sap.tc.webdynpro.progmodel.api.IWDView view, boolean firstTime ) { //@@begin wdDoModifyView ... final IPrivateSpellCheckerXCV.IContextElement ctx = wdContext.currentContextElement(); if ( !ctx.getIsContextValid() ) { ctx.setIsContextValid( true ); final IWDUIElementContainer text = (IWDUIElementContainer)view.getElement("grText"); text.destroyAllChildren(); final IWDNodeInfo niDynaWords = wdContext.nodeDynamicWords().getNodeInfo(); final IPrivateSpellCheckerXCV.IWordsNode nWords = wdContext.nodeWords(); final String selectIcon = wdContext.currentSettingsElement().getSelectWordIcon(); final boolean hasSelector = null != selectIcon; final IWDAction doSelect = wdThis.wdGetSelectWordAction(); doSelect.setImage( selectIcon ); // Create structure for (int i = 0, size = nWords.size(); i < size; i++) { final IPrivateSpellCheckerXCV.IWordsElement elWord = nWords.getWordsElementAt(i); final IWDNodeInfo niThisDynaWord = niDynaWords.getChild( "word_" + i); final IWDAttributeInfo aiText = niThisDynaWord.getAttribute( IUITextEntry.PROP_TEXT ); final IWDTextView word = (IWDTextView)view.createElement( IWDTextView.class, null ); word.bindText( aiText ); word.bindSemanticColor( niThisDynaWord.getAttribute( IUITextEntry.PROP_TEXT_COLOR ) ); word.setWrapping( true ); word.createLayoutData( IWDFlowData.class ); text.addChild( word ); if ( !elWord.getIsValid() && hasSelector ) { final IWDLinkToAction link = (IWDLinkToAction)view.createElement( IWDLinkToAction.class, null ); link.setType( WDLinkType.RESULT ); link.setOnAction( doSelect ); link.bindTooltip( aiText ); link.bindVisible( niThisDynaWord.getAttribute( IUITextEntry.PROP_VALIDATE_CTRL_VISIBILITY ) ); link.mappingOfOnAction().addParameter("idx", i); link.createLayoutData( IWDFlowData.class ); text.addChild( link ); } } } //@@end }

The IWDLinkToAction element here is my “advent” in usability of “Spelling:” dialogs (do not take this seriously 😉 The regular design allows user to fix / ignore errors one by one and move him/her from beginning to the end of text. Links here allow user to move directly to misspelled word. For this purpose every action control adds index parameter, therefore enabling naviagation to necessary text fragment (this may be only misspelled word, by the way) in handler code. It is an optional feature and by default disabled in component. However, sample application contains “fancy” spell checker that allows the following besides “standard” functionality:

  • Select misspelled word directly
  • Highlight fixed misspellings differently.

See accompanied code for more details.

image
Image 2. Customized SpellChecker UI

Results

Well, we are done. From first look component even beats SDN spell checker (the original functionality target ;). However, there is room for improvements as well:

  • It is possible to enter any text for replace. And it will be marked as valid. Obviously, better option is to re-validate entered fragment and force user to adjust it if text contains newly introduced unrecognizable words.
  • Loading per-component in-memory dictionary is plain wrong approach in real-life. Some better strategy should be taken.
  • In my NW04s environment IWDTextView is able to display spaces and “new line” correctly with wrapping enabled. Not sure it works properly in SP11+. However, this issue also solvable programmatically using techniques enumerated in this post.

Download related project here

To report this post you need to login first.

9 Comments

You must be Logged on to comment or reply to a post.

    1. Valery Silaev Post author
      Yes, I guess it could be really appropriate here while granularity of call is high and well-defined (in->text, out->text-fragments+suggestions). Also it would be convinient to have external multi-lingual spell-checking service. And I there are already such on-line businesses like http://spell.imtranslator.com/.

      VS

      (0) 
  1. Harsh Chawla
    Hi Valery,
        Nice blog! I tried downloading and using your project, but am getting classpath errors, for the following.. am I missing something?

    sldclient.jar
    _webdynpro_portal.jar
    cache_api.jar
    cache_plugins.jar
    cachemgmt.jar

    Regards,
    Harsh

    PS:I’m using Version: 2.0.11 Build id: 200502210240.

    (0) 
      1. Valery Silaev Post author
        I mentioned in this weblog (in results) section that I’m using NW04s (but not NW04 SP11+)
        So you may simply remove missing classpath entries.

        VS

        (0) 
  2. Sharath M G
    There was a requirement for Spell check and had found the jazzy api. But, was not sure if the work around would be able to fulfill the requirement of spell check. Infact, our team had decided to say no to the requirement. Then, I found your reply in forum and finally your blog.

    Now, I can safely say YES to the customer.

    Its with people like you, that there is great potential in these forums to contribute to bigger and better products.

    Thank you again.

    Note: The above text was spell checked using your application. 🙂

    Note: Please perform spell check

    (0) 

Leave a Reply