OSDN Git Service

6b24e2e1a9fdf63f01e568c1611ab253d35dd6ab
[neighbornote/NeighborNote.git] / src / com / swabunga / spell / event / SpellChecker.java
1 /*\r
2 Jazzy - a Java library for Spell Checking\r
3 Copyright (C) 2001 Mindaugas Idzelis\r
4 Full text of license can be found in LICENSE.txt\r
5 \r
6 This library is free software; you can redistribute it and/or\r
7 modify it under the terms of the GNU Lesser General Public\r
8 License as published by the Free Software Foundation; either\r
9 version 2.1 of the License, or (at your option) any later version.\r
10 \r
11 This library is distributed in the hope that it will be useful,\r
12 but WITHOUT ANY WARRANTY; without even the implied warranty of\r
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
14 Lesser General Public License for more details.\r
15 \r
16 You should have received a copy of the GNU Lesser General Public\r
17 License along with this library; if not, write to the Free Software\r
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\r
19 */\r
20 package com.swabunga.spell.event;\r
21 \r
22 import java.io.IOException;\r
23 import java.util.ArrayList;\r
24 import java.util.Enumeration;\r
25 import java.util.HashMap;\r
26 import java.util.Hashtable;\r
27 import java.util.Iterator;\r
28 import java.util.List;\r
29 import java.util.Map;\r
30 import java.util.Vector;\r
31 \r
32 import com.swabunga.spell.engine.Configuration;\r
33 import com.swabunga.spell.engine.SpellDictionary;\r
34 import com.swabunga.spell.engine.SpellDictionaryHashMap;\r
35 import com.swabunga.spell.engine.Word;\r
36 import com.swabunga.util.VectorUtility;\r
37 \r
38 \r
39 /**\r
40  * This is the main class for spell checking (using the new event based spell\r
41  * checking). \r
42  * <p/>\r
43  * By default, the class makes a user dictionary to accumulate added words.\r
44  * Since this user directory has no file assign to persist added words, they\r
45  * will be retained for the duration of the spell checker instance.\r
46  * If you set a user dictionary like \r
47  * {@link com.swabunga.spell.engine.SpellDictionaryHashMap SpellDictionaryHashMap}\r
48  * to persist the added word, the user dictionary will have the possibility to\r
49  * grow and be available across differents invocations of the spell checker.\r
50  *\r
51  * @author     Jason Height (jheight@chariot.net.au)\r
52  * 19 June 2002\r
53  */\r
54 public class SpellChecker {\r
55   /** Flag indicating that the Spell Check completed without any errors present*/\r
56   public static final int SPELLCHECK_OK = -1;\r
57   /** Flag indicating that the Spell Check completed due to user cancellation*/\r
58   public static final int SPELLCHECK_CANCEL = -2;\r
59 \r
60   @SuppressWarnings("unchecked")\r
61 private final Vector eventListeners = new Vector();\r
62   @SuppressWarnings("unchecked")\r
63 private final Vector dictionaries = new Vector();\r
64   private SpellDictionary userdictionary;\r
65 \r
66   private final Configuration config = Configuration.getConfiguration();\r
67 \r
68   /**This variable holds all of the words that are to be always ignored */\r
69   @SuppressWarnings("unchecked")\r
70 private Vector ignoredWords = new Vector();\r
71   @SuppressWarnings("unchecked")\r
72 private Hashtable autoReplaceWords = new Hashtable();\r
73   \r
74   // added caching - bd\r
75   // For cached operation a separate user dictionary is required\r
76   @SuppressWarnings("unchecked")\r
77 private Map cache;\r
78   private int threshold = 0;\r
79   private int cacheSize = 0;\r
80   \r
81 \r
82   /**\r
83    * Constructs the SpellChecker.\r
84    */\r
85   public SpellChecker() {\r
86     try {\r
87       userdictionary = new SpellDictionaryHashMap();\r
88     } catch (IOException e) {\r
89       throw new RuntimeException("this exception should never happen because we are using null phonetic file");\r
90     }\r
91   }\r
92 \r
93   /**\r
94    * Constructs the SpellChecker. The default threshold is used\r
95    *\r
96    * @param  dictionary  The dictionary used for looking up words.\r
97    */\r
98   public SpellChecker(SpellDictionary dictionary) {\r
99     this();\r
100     addDictionary(dictionary);\r
101   }\r
102 \r
103 \r
104   /**\r
105    * Constructs the SpellChecker with a threshold\r
106    *\r
107    * @param  dictionary  the dictionary used for looking up words.\r
108    * @param  threshold   the cost value above which any suggestions are \r
109    *                     thrown away\r
110    */\r
111   public SpellChecker(SpellDictionary dictionary, int threshold) {\r
112     this(dictionary);\r
113     config.setInteger(Configuration.SPELL_THRESHOLD, threshold);\r
114   }\r
115 \r
116   /**\r
117    * Accumulates a dictionary at the end of the dictionaries list used\r
118    * for looking up words. Adding a dictionary give the flexibility to\r
119    * assign the base language dictionary, then a more technical, then...\r
120    *\r
121    * @param dictionary the dictionary to add at the end of the dictionary list.\r
122    */\r
123   @SuppressWarnings("unchecked")\r
124 public void addDictionary(SpellDictionary dictionary) {\r
125     if (dictionary == null) {\r
126       throw new IllegalArgumentException("dictionary must be non-null");\r
127     }\r
128     this.dictionaries.addElement(dictionary);\r
129   }\r
130 \r
131   /**\r
132    * Registers the user dictionary to which words are added.\r
133    *\r
134    * @param dictionary the dictionary to use when the user specify a new word\r
135    * to add.\r
136    */\r
137   public void setUserDictionary(SpellDictionary dictionary) {\r
138     userdictionary = dictionary;\r
139   }\r
140 \r
141   /**\r
142    * Supply the instance of the configuration holding the spell checking engine\r
143    * parameters.\r
144    *\r
145    * @return Current Configuration\r
146    */\r
147   public Configuration getConfiguration() {\r
148     return config;\r
149   }\r
150 \r
151   /**\r
152    * Adds a SpellCheckListener to the listeners list.\r
153    *\r
154    * @param  listener  The feature to be added to the SpellCheckListener attribute\r
155    */\r
156   @SuppressWarnings("unchecked")\r
157 public void addSpellCheckListener(SpellCheckListener listener) {\r
158     eventListeners.addElement(listener);\r
159   }\r
160 \r
161 \r
162   /**\r
163    * Removes a SpellCheckListener from the listeners list.\r
164    *\r
165    * @param  listener  The listener to be removed from the listeners list.\r
166    */\r
167   public void removeSpellCheckListener(SpellCheckListener listener) {\r
168     eventListeners.removeElement(listener);\r
169   }\r
170 \r
171 \r
172   /**\r
173    * Fires off a spell check event to the listeners.\r
174    *\r
175    * @param  event  The event that need to be processed by the spell checking\r
176    * system.\r
177    */\r
178   protected void fireSpellCheckEvent(SpellCheckEvent event) {\r
179     for (int i = eventListeners.size() - 1; i >= 0; i--) {\r
180       ((SpellCheckListener) eventListeners.elementAt(i)).spellingError(event);\r
181     }\r
182   }\r
183 \r
184 \r
185   /**\r
186    * This method clears the words that are currently being remembered as\r
187    *  <code>Ignore All</code> words and <code>Replace All</code> words.\r
188    */\r
189   @SuppressWarnings("unchecked")\r
190 public void reset() {\r
191     ignoredWords = new Vector();\r
192     autoReplaceWords = new Hashtable();\r
193   }\r
194 \r
195 \r
196   /**\r
197    * Checks the text string.\r
198    *  <p>\r
199    *  Returns the corrected string.\r
200    *\r
201    * @param  text   The text that need to be spelled checked\r
202    * @return        The text after spell checking\r
203    * @deprecated    use checkSpelling(WordTokenizer)\r
204    */\r
205   @Deprecated\r
206 public String checkString(String text) {\r
207     StringWordTokenizer tokens = new StringWordTokenizer(text);\r
208     checkSpelling(tokens);\r
209     return tokens.getContext();\r
210   }\r
211 \r
212 \r
213   /**\r
214    * Verifies if the word that is being spell checked contains at least a\r
215    * digit.\r
216    * Returns true if this word contains a digit.\r
217    *\r
218    * @param  word  The word to analyze for digit.\r
219    * @return       true if the word contains at least a digit.\r
220    */\r
221   private final static boolean isDigitWord(String word) {\r
222     for (int i = word.length() - 1; i >= 0; i--) {\r
223       if (Character.isDigit(word.charAt(i))) {\r
224         return true;\r
225       }\r
226     }\r
227     return false;\r
228   }\r
229 \r
230 \r
231   /**\r
232    * Verifies if the word that is being spell checked contains an Internet \r
233    * address. The method look for typical protocol or the habitual string \r
234    * in the word:\r
235    * <ul>\r
236    * <li>http://</li>\r
237    * <li>ftp://</li>\r
238    * <li>https://</li>\r
239    * <li>ftps://</li>\r
240    * <li>www.</li>\r
241    * </ul>\r
242    *\r
243    * One limitation is that this method cannot currently recognize email\r
244    * addresses. Since the 'word' that is passed in, may in fact contain\r
245    * the rest of the document to be checked, it is not (yet!) a good\r
246    * idea to scan for the @ character.\r
247    *\r
248    * @param  word  The word to analyze for an Internet address.\r
249    * @return       true if this word looks like an Internet address.\r
250    */\r
251     public final static boolean isINETWord(String word) {\r
252         String lowerCaseWord = word.toLowerCase();\r
253         return lowerCaseWord.startsWith("http://") ||\r
254               lowerCaseWord.startsWith("www.") ||\r
255               lowerCaseWord.startsWith("ftp://") ||\r
256               lowerCaseWord.startsWith("https://") ||\r
257               lowerCaseWord.startsWith("ftps://");\r
258   }\r
259 \r
260 \r
261   /**\r
262    * Verifies if the word that is being spell checked contains all\r
263    * uppercases characters.\r
264    *\r
265    * @param  word  The word to analyze for uppercases characters\r
266    * @return       true if this word contains all upper case characters\r
267    */\r
268   private final static boolean isUpperCaseWord(String word) {\r
269     for (int i = word.length() - 1; i >= 0; i--) {\r
270       if (Character.isLowerCase(word.charAt(i))) {\r
271         return false;\r
272       }\r
273     }\r
274     return true;\r
275   }\r
276 \r
277 \r
278   /**\r
279    * Verifies if the word that is being spell checked contains lower and\r
280    * upper cased characters. Note that a phrase beginning with an upper cased\r
281    * character is not considered a mixed case word.\r
282    *\r
283    * @param  word  The word to analyze for mixed cases characters\r
284    * @param startsSentence True if this word is at the start of a sentence\r
285    * @return       true if this word contains mixed case characters\r
286    */\r
287   private final static boolean isMixedCaseWord(String word, boolean startsSentence) {\r
288     int strLen = word.length();\r
289     boolean isUpper = Character.isUpperCase(word.charAt(0));\r
290     //Ignore the first character if this word starts the sentence and the first\r
291     //character was upper cased, since this is normal behaviour\r
292     if ((startsSentence) && isUpper && (strLen > 1))\r
293       isUpper = Character.isUpperCase(word.charAt(1));\r
294     if (isUpper) {\r
295       for (int i = word.length() - 1; i > 0; i--) {\r
296         if (Character.isLowerCase(word.charAt(i))) {\r
297           return true;\r
298         }\r
299       }\r
300     } else {\r
301       for (int i = word.length() - 1; i > 0; i--) {\r
302         if (Character.isUpperCase(word.charAt(i))) {\r
303           return true;\r
304         }\r
305       }\r
306     }\r
307     return false;\r
308   }\r
309 \r
310 \r
311   /**\r
312    * This method will fire the spell check event and then handle the event\r
313    *  action that has been selected by the user.\r
314    *\r
315    * @param  tokenizer        Description of the Parameter\r
316    * @param  event            The event to handle\r
317    * @return                  Returns true if the event action is to cancel the current spell checking, false if the spell checking should continue\r
318    */\r
319   @SuppressWarnings("unchecked")\r
320 protected boolean fireAndHandleEvent(WordTokenizer tokenizer, SpellCheckEvent event) {\r
321     fireSpellCheckEvent(event);\r
322     String word = event.getInvalidWord();\r
323     //Work out what to do in response to the event.\r
324     switch (event.getAction()) {\r
325       case SpellCheckEvent.INITIAL:\r
326         break;\r
327       case SpellCheckEvent.IGNORE:\r
328         break;\r
329       case SpellCheckEvent.IGNOREALL:\r
330         ignoreAll(word);\r
331         break;\r
332       case SpellCheckEvent.REPLACE:\r
333         tokenizer.replaceWord(event.getReplaceWord());\r
334         break;\r
335       case SpellCheckEvent.REPLACEALL:\r
336         String replaceAllWord = event.getReplaceWord();\r
337         if (!autoReplaceWords.containsKey(word)) {\r
338           autoReplaceWords.put(word, replaceAllWord);\r
339         }\r
340         tokenizer.replaceWord(replaceAllWord);\r
341         break;\r
342       case SpellCheckEvent.ADDTODICT:\r
343         String addWord = event.getReplaceWord();\r
344         if (!addWord.equals(word))\r
345           tokenizer.replaceWord(addWord);\r
346         userdictionary.addWord(addWord);\r
347         break;\r
348       case SpellCheckEvent.CANCEL:\r
349         return true;\r
350       default:\r
351         throw new IllegalArgumentException("Unhandled case.");\r
352     }\r
353     return false;\r
354   }\r
355 \r
356   /**\r
357    * Adds a word to the list of ignored words\r
358    * @param word The text of the word to ignore\r
359    */\r
360   @SuppressWarnings("unchecked")\r
361 public void ignoreAll(String word) {\r
362     if (!ignoredWords.contains(word)) {\r
363       ignoredWords.addElement(word);\r
364     }\r
365   }\r
366   \r
367   /**\r
368    * Adds a word to the user dictionary\r
369    * @param word The text of the word to add\r
370    */\r
371   public void addToDictionary(String word) {\r
372     if (!userdictionary.isCorrect(word))\r
373       userdictionary.addWord(word);\r
374   }\r
375   \r
376   /**\r
377    * Indicates if a word is in the list of ignored words\r
378    * @param word The text of the word check\r
379    */\r
380   public boolean isIgnored(String word){\r
381         return ignoredWords.contains(word);\r
382   }\r
383   \r
384   /**\r
385    * Verifies if the word to analyze is contained in dictionaries. The order \r
386    * of dictionary lookup is:\r
387    * <ul>\r
388    * <li>The default user dictionary or the one set through \r
389    * {@link SpellChecker#setUserDictionary}</li>\r
390    * <li>The dictionary specified at construction time, if any.</li>\r
391    * <li>Any dictionary in the order they were added through \r
392    * {@link SpellChecker#addDictionary}</li>\r
393    * </ul>\r
394    *\r
395    * @param word The word to verify that it's spelling is known.\r
396    * @return true if the word is in a dictionary.\r
397    */\r
398   @SuppressWarnings("unchecked")\r
399 public boolean isCorrect(String word) {\r
400     if (userdictionary.isCorrect(word)) return true;\r
401     for (Enumeration e = dictionaries.elements(); e.hasMoreElements();) {\r
402       SpellDictionary dictionary = (SpellDictionary) e.nextElement();\r
403       if (dictionary.isCorrect(word)) return true;\r
404     }\r
405     return false;\r
406   }\r
407 \r
408   /**\r
409    * Produces a list of suggested word after looking for suggestions in various\r
410    * dictionaries. The order of dictionary lookup is:\r
411    * <ul>\r
412    * <li>The default user dictionary or the one set through \r
413    * {@link SpellChecker#setUserDictionary}</li>\r
414    * <li>The dictionary specified at construction time, if any.</li>\r
415    * <li>Any dictionary in the order they were added through \r
416    * {@link SpellChecker#addDictionary}</li>\r
417    * </ul>\r
418    *\r
419    * @param word The word for which we want to gather suggestions\r
420    * @param threshold the cost value above which any suggestions are \r
421    *                  thrown away\r
422    * @return the list of words suggested\r
423    */\r
424   @SuppressWarnings("unchecked")\r
425 public List getSuggestions(String word, int threshold) {\r
426     if (this.threshold != threshold && cache != null) {\r
427        this.threshold = threshold;\r
428        cache.clear();\r
429     }\r
430     \r
431     ArrayList suggestions = null;\r
432     \r
433     if (cache != null)\r
434        suggestions = (ArrayList) cache.get(word);\r
435 \r
436     if (suggestions == null) {\r
437        suggestions = new ArrayList(50);\r
438     \r
439        for (Enumeration e = dictionaries.elements(); e.hasMoreElements();) {\r
440            SpellDictionary dictionary = (SpellDictionary) e.nextElement();\r
441            \r
442            if (dictionary != userdictionary)\r
443               VectorUtility.addAll(suggestions, dictionary.getSuggestions(word, threshold), false);\r
444        }\r
445 \r
446        if (cache != null && cache.size() < cacheSize)\r
447          cache.put(word, suggestions);\r
448     }\r
449     \r
450     VectorUtility.addAll(suggestions, userdictionary.getSuggestions(word, threshold), false);\r
451     suggestions.trimToSize();\r
452     \r
453     return suggestions;\r
454   }\r
455 \r
456   /**\r
457   * Activates a cache with the maximum number of entries set to 300\r
458   */\r
459   public void setCache() {\r
460     setCache(300);\r
461   }\r
462 \r
463   /**\r
464   * Activates a cache with specified size\r
465   * @param size - max. number of cache entries (0 to disable chache)\r
466   */\r
467   @SuppressWarnings("unchecked")\r
468 public void setCache(int size) {\r
469     cacheSize = size;\r
470     if (size == 0)\r
471       cache = null;\r
472    else\r
473      cache = new HashMap((size + 2) / 3 * 4);\r
474   }\r
475 \r
476   /**\r
477    * This method is called to check the spelling of the words that are returned\r
478    * by the WordTokenizer.\r
479    * <p/>\r
480    * For each invalid word the action listeners will be informed with a new \r
481    * SpellCheckEvent.<p>\r
482    *\r
483    * @param  tokenizer  The media containing the text to analyze.\r
484    * @return Either SPELLCHECK_OK, SPELLCHECK_CANCEL or the number of errors found. The number of errors are those that\r
485    * are found BEFORE any corrections are made.\r
486    */\r
487   @SuppressWarnings("unchecked")\r
488 public final int checkSpelling(WordTokenizer tokenizer) {\r
489     int errors = 0;\r
490     boolean terminated = false;\r
491     //Keep track of the previous word\r
492 //    String previousWord = null;\r
493     while (tokenizer.hasMoreWords() && !terminated) {\r
494       String word = tokenizer.nextWord();\r
495       //Check the spelling of the word\r
496       if (!isCorrect(word)) {\r
497           if ((config.getBoolean(Configuration.SPELL_IGNOREMIXEDCASE) && isMixedCaseWord(word, tokenizer.isNewSentence())) ||\r
498             (config.getBoolean(Configuration.SPELL_IGNOREUPPERCASE) && isUpperCaseWord(word)) ||\r
499             (config.getBoolean(Configuration.SPELL_IGNOREDIGITWORDS) && isDigitWord(word)) ||\r
500             (config.getBoolean(Configuration.SPELL_IGNOREINTERNETADDRESSES) && isINETWord(word))) {\r
501           //Null event. Since we are ignoring this word due\r
502           //to one of the above cases.\r
503         } else {\r
504           //We cant ignore this misspelt word\r
505           //For this invalid word are we ignoring the misspelling?\r
506           if (!isIgnored(word)) {\r
507             errors++;\r
508             //Is this word being automagically replaced\r
509             if (autoReplaceWords.containsKey(word)) {\r
510               tokenizer.replaceWord((String) autoReplaceWords.get(word));\r
511             } else {\r
512               //JMH Need to somehow capitalise the suggestions if\r
513               //ignoreSentenceCapitalisation is not set to true\r
514               //Fire the event.\r
515               List suggestions = getSuggestions(word, config.getInteger(Configuration.SPELL_THRESHOLD));\r
516               if (capitalizeSuggestions(word, tokenizer))\r
517                 suggestions = makeSuggestionsCapitalized(suggestions);\r
518               SpellCheckEvent event = new BasicSpellCheckEvent(word, suggestions, tokenizer);\r
519               terminated = fireAndHandleEvent(tokenizer, event);\r
520             }\r
521           }\r
522         }\r
523       } else {\r
524         //This is a correctly spelt word. However perform some extra checks\r
525         /*\r
526          *  JMH TBD          //Check for multiple words\r
527          *  if (!ignoreMultipleWords &&) {\r
528          *  }\r
529          */\r
530         //Check for capitalisation\r
531         if (isSupposedToBeCapitalized(word, tokenizer)) {\r
532           errors++;\r
533           StringBuffer buf = new StringBuffer(word);\r
534           buf.setCharAt(0, Character.toUpperCase(word.charAt(0)));\r
535           Vector suggestion = new Vector();\r
536           suggestion.addElement(new Word(buf.toString(), 0));\r
537           SpellCheckEvent event = new BasicSpellCheckEvent(word, suggestion, tokenizer);\r
538           terminated = fireAndHandleEvent(tokenizer, event);\r
539         }\r
540       }\r
541     }\r
542     if (terminated)\r
543       return SPELLCHECK_CANCEL;\r
544     else if (errors == 0)\r
545       return SPELLCHECK_OK;\r
546     else\r
547       return errors;\r
548   }\r
549   \r
550   \r
551   @SuppressWarnings("unchecked")\r
552 private List makeSuggestionsCapitalized(List suggestions) {\r
553     Iterator iterator = suggestions.iterator();\r
554     while(iterator.hasNext()) {\r
555       Word word = (Word)iterator.next();\r
556       String suggestion = word.getWord();\r
557       StringBuffer stringBuffer = new StringBuffer(suggestion);\r
558       stringBuffer.setCharAt(0, Character.toUpperCase(suggestion.charAt(0)));\r
559       word.setWord(stringBuffer.toString());\r
560     }\r
561     return suggestions;\r
562   }\r
563 \r
564     \r
565    private boolean isSupposedToBeCapitalized(String word, WordTokenizer wordTokenizer) {\r
566      boolean configCapitalize = !config.getBoolean(Configuration.SPELL_IGNORESENTENCECAPITALIZATION);\r
567      return configCapitalize && wordTokenizer.isNewSentence() && Character.isLowerCase(word.charAt(0));\r
568   } \r
569 \r
570    private boolean capitalizeSuggestions(String word, WordTokenizer wordTokenizer) {\r
571    // if SPELL_IGNORESENTENCECAPITALIZATION and the initial word is capitalized, suggestions should also be capitalized\r
572    // if !SPELL_IGNORESENTENCECAPITALIZATION, capitalize suggestions only for the first word in a sentence\r
573      boolean configCapitalize = !config.getBoolean(Configuration.SPELL_IGNORESENTENCECAPITALIZATION);\r
574      boolean uppercase = Character.isUpperCase(word.charAt(0));\r
575      return (configCapitalize && wordTokenizer.isNewSentence()) || (!configCapitalize && uppercase);\r
576    }\r
577 }\r