2b4da353a1470ea4268c494f3a4759a5cf772f04
[phpeclipse.git] / net.sourceforge.phpeclipse.ui / src / net / sourceforge / phpdt / internal / ui / text / spelling / engine / DefaultSpellChecker.java
1 /*******************************************************************************
2  * Copyright (c) 2000, 2003 IBM Corporation and others.
3  * All rights reserved. This program and the accompanying materials 
4  * are made available under the terms of the Common Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/cpl-v10.html
7  * 
8  * Contributors:
9  *     IBM Corporation - initial API and implementation
10  *******************************************************************************/
11
12 package net.sourceforge.phpdt.internal.ui.text.spelling.engine;
13
14 import java.util.Collections;
15 import java.util.HashSet;
16 import java.util.Iterator;
17 import java.util.Set;
18
19 import org.eclipse.jface.preference.IPreferenceStore;
20
21 /**
22  * Default spell checker for standard text.
23  * 
24  * @since 3.0
25  */
26 public class DefaultSpellChecker implements ISpellChecker {
27
28         /** Array of url prefixes */
29         public static final String[] URL_PREFIXES = new String[] {
30                         "http://", "https://", "www.", "ftp://", "ftps://", "news://", "mailto://" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$
31
32         /**
33          * Does this word contain digits?
34          * 
35          * @param word
36          *            The word to check
37          * @return <code>true</code> iff this word contains digits,
38          *         <code>false></code> otherwise
39          */
40         protected static boolean isDigits(final String word) {
41
42                 for (int index = 0; index < word.length(); index++) {
43
44                         if (Character.isDigit(word.charAt(index)))
45                                 return true;
46                 }
47                 return false;
48         }
49
50         /**
51          * Does this word contain mixed-case letters?
52          * 
53          * @param word
54          *            The word to check
55          * @param sentence
56          *            <code>true</code> iff the specified word starts a new
57          *            sentence, <code>false</code> otherwise
58          * @return <code>true</code> iff the contains mixed-case letters,
59          *         <code>false</code> otherwise
60          */
61         protected static boolean isMixedCase(final String word,
62                         final boolean sentence) {
63
64                 final int length = word.length();
65                 boolean upper = Character.isUpperCase(word.charAt(0));
66
67                 if (sentence && upper && (length > 1))
68                         upper = Character.isUpperCase(word.charAt(1));
69
70                 if (upper) {
71
72                         for (int index = length - 1; index > 0; index--) {
73                                 if (Character.isLowerCase(word.charAt(index)))
74                                         return true;
75                         }
76                 } else {
77
78                         for (int index = length - 1; index > 0; index--) {
79                                 if (Character.isUpperCase(word.charAt(index)))
80                                         return true;
81                         }
82                 }
83                 return false;
84         }
85
86         /**
87          * Does this word contain upper-case letters only?
88          * 
89          * @param word
90          *            The word to check
91          * @return <code>true</code> iff this word only contains upper-case
92          *         letters, <code>false</code> otherwise
93          */
94         protected static boolean isUpperCase(final String word) {
95
96                 for (int index = word.length() - 1; index >= 0; index--) {
97
98                         if (Character.isLowerCase(word.charAt(index)))
99                                 return false;
100                 }
101                 return true;
102         }
103
104         /**
105          * Does this word look like an URL?
106          * 
107          * @param word
108          *            The word to check
109          * @return <code>true</code> iff this word looks like an URL,
110          *         <code>false</code> otherwise
111          */
112         protected static boolean isUrl(final String word) {
113
114                 for (int index = 0; index < URL_PREFIXES.length; index++) {
115
116                         if (word.startsWith(URL_PREFIXES[index]))
117                                 return true;
118                 }
119                 return false;
120         }
121
122         /**
123          * The dictionaries to use for spell-checking. Synchronized to avoid
124          * concurrent modifications.
125          */
126         private final Set fDictionaries = Collections
127                         .synchronizedSet(new HashSet());
128
129         /**
130          * The words to be ignored. Synchronized to avoid concurrent modifications.
131          */
132         private final Set fIgnored = Collections.synchronizedSet(new HashSet());
133
134         /**
135          * The spell event listeners. Synchronized to avoid concurrent
136          * modifications.
137          */
138         private final Set fListeners = Collections.synchronizedSet(new HashSet());
139
140         /**
141          * The preference store. Assumes the <code>IPreferenceStore</code>
142          * implementation is thread safe.
143          */
144         private final IPreferenceStore fPreferences;
145
146         /**
147          * Creates a new default spell-checker.
148          * 
149          * @param store
150          *            The preference store for this spell-checker
151          */
152         public DefaultSpellChecker(final IPreferenceStore store) {
153                 fPreferences = store;
154         }
155
156         /*
157          * @see org.eclipse.spelling.done.ISpellChecker#addDictionary(org.eclipse.spelling.done.ISpellDictionary)
158          */
159         public final void addDictionary(final ISpellDictionary dictionary) {
160                 // synchronizing is necessary as this is a write access
161                 fDictionaries.add(dictionary);
162         }
163
164         /*
165          * @see org.eclipse.spelling.done.ISpellChecker#addListener(org.eclipse.spelling.done.ISpellEventListener)
166          */
167         public final void addListener(final ISpellEventListener listener) {
168                 // synchronizing is necessary as this is a write access
169                 fListeners.add(listener);
170         }
171
172         /*
173          * @see net.sourceforge.phpdt.ui.text.spelling.engine.ISpellChecker#acceptsWords()
174          */
175         public boolean acceptsWords() {
176                 // synchronizing might not be needed here since acceptWords is
177                 // a read-only access and only called in the same thread as
178                 // the modifing methods add/checkWord (?)
179                 Set copy;
180                 synchronized (fDictionaries) {
181                         copy = new HashSet(fDictionaries);
182                 }
183
184                 ISpellDictionary dictionary = null;
185                 for (final Iterator iterator = copy.iterator(); iterator.hasNext();) {
186
187                         dictionary = (ISpellDictionary) iterator.next();
188                         if (dictionary.acceptsWords())
189                                 return true;
190                 }
191                 return false;
192         }
193
194         /*
195          * @see net.sourceforge.phpdt.internal.ui.text.spelling.engine.ISpellChecker#addWord(java.lang.String)
196          */
197         public void addWord(final String word) {
198                 // synchronizing is necessary as this is a write access
199                 Set copy;
200                 synchronized (fDictionaries) {
201                         copy = new HashSet(fDictionaries);
202                 }
203
204                 final String addable = word.toLowerCase();
205                 fIgnored.add(addable);
206
207                 ISpellDictionary dictionary = null;
208                 for (final Iterator iterator = copy.iterator(); iterator.hasNext();) {
209
210                         dictionary = (ISpellDictionary) iterator.next();
211                         dictionary.addWord(addable);
212                 }
213         }
214
215         /*
216          * @see net.sourceforge.phpdt.ui.text.spelling.engine.ISpellChecker#checkWord(java.lang.String)
217          */
218         public final void checkWord(final String word) {
219                 // synchronizing is necessary as this is a write access
220                 fIgnored.remove(word.toLowerCase());
221         }
222
223         /*
224          * @see org.eclipse.spelling.done.ISpellChecker#execute(org.eclipse.spelling.ISpellCheckTokenizer)
225          */
226         public void execute(final ISpellCheckIterator iterator) {
227
228                 final boolean ignoreDigits = fPreferences
229                                 .getBoolean(ISpellCheckPreferenceKeys.SPELLING_IGNORE_DIGITS);
230                 final boolean ignoreMixed = fPreferences
231                                 .getBoolean(ISpellCheckPreferenceKeys.SPELLING_IGNORE_MIXED);
232                 final boolean ignoreSentence = fPreferences
233                                 .getBoolean(ISpellCheckPreferenceKeys.SPELLING_IGNORE_SENTENCE);
234                 final boolean ignoreUpper = fPreferences
235                                 .getBoolean(ISpellCheckPreferenceKeys.SPELLING_IGNORE_UPPER);
236                 final boolean ignoreURLS = fPreferences
237                                 .getBoolean(ISpellCheckPreferenceKeys.SPELLING_IGNORE_URLS);
238
239                 String word = null;
240                 boolean starts = false;
241
242                 while (iterator.hasNext()) {
243
244                         word = (String) iterator.next();
245                         if (word != null) {
246
247                                 // synchronizing is necessary as this is called inside the
248                                 // reconciler
249                                 if (!fIgnored.contains(word)) {
250
251                                         starts = iterator.startsSentence();
252                                         if (!isCorrect(word)) {
253
254                                                 boolean isMixed = isMixedCase(word, true);
255                                                 boolean isUpper = isUpperCase(word);
256                                                 boolean isDigits = isDigits(word);
257                                                 boolean isURL = isUrl(word);
258
259                                                 if (!ignoreMixed && isMixed || !ignoreUpper && isUpper
260                                                                 || !ignoreDigits && isDigits || !ignoreURLS
261                                                                 && isURL
262                                                                 || !(isMixed || isUpper || isDigits || isURL))
263                                                         fireEvent(new SpellEvent(this, word, iterator
264                                                                         .getBegin(), iterator.getEnd(), starts,
265                                                                         false));
266
267                                         } else {
268
269                                                 if (!ignoreSentence && starts
270                                                                 && Character.isLowerCase(word.charAt(0)))
271                                                         fireEvent(new SpellEvent(this, word, iterator
272                                                                         .getBegin(), iterator.getEnd(), true, true));
273                                         }
274                                 }
275                         }
276                 }
277         }
278
279         /**
280          * Fires the specified event.
281          * 
282          * @param event
283          *            Event to fire
284          */
285         protected final void fireEvent(final ISpellEvent event) {
286                 // synchronizing is necessary as this is called from execute
287                 Set copy;
288                 synchronized (fListeners) {
289                         copy = new HashSet(fListeners);
290                 }
291                 for (final Iterator iterator = copy.iterator(); iterator.hasNext();) {
292                         ((ISpellEventListener) iterator.next()).handle(event);
293                 }
294         }
295
296         /*
297          * @see org.eclipse.spelling.done.ISpellChecker#getProposals(java.lang.String,boolean)
298          */
299         public Set getProposals(final String word, final boolean sentence) {
300
301                 // synchronizing might not be needed here since getProposals is
302                 // a read-only access and only called in the same thread as
303                 // the modifing methods add/removeDictionary (?)
304                 Set copy;
305                 synchronized (fDictionaries) {
306                         copy = new HashSet(fDictionaries);
307                 }
308
309                 ISpellDictionary dictionary = null;
310                 final HashSet proposals = new HashSet();
311
312                 for (final Iterator iterator = copy.iterator(); iterator.hasNext();) {
313
314                         dictionary = (ISpellDictionary) iterator.next();
315                         proposals.addAll(dictionary.getProposals(word, sentence));
316                 }
317                 return proposals;
318         }
319
320         /*
321          * @see net.sourceforge.phpdt.internal.ui.text.spelling.engine.ISpellChecker#ignoreWord(java.lang.String)
322          */
323         public final void ignoreWord(final String word) {
324                 // synchronizing is necessary as this is a write access
325                 fIgnored.add(word.toLowerCase());
326         }
327
328         /*
329          * @see net.sourceforge.phpdt.internal.ui.text.spelling.engine.ISpellChecker#isCorrect(java.lang.String)
330          */
331         public final boolean isCorrect(final String word) {
332                 // synchronizing is necessary as this is called from execute
333                 Set copy;
334                 synchronized (fDictionaries) {
335                         copy = new HashSet(fDictionaries);
336                 }
337
338                 if (fIgnored.contains(word.toLowerCase()))
339                         return true;
340
341                 ISpellDictionary dictionary = null;
342                 for (final Iterator iterator = copy.iterator(); iterator.hasNext();) {
343
344                         dictionary = (ISpellDictionary) iterator.next();
345                         if (dictionary.isCorrect(word))
346                                 return true;
347                 }
348                 return false;
349         }
350
351         /*
352          * @see org.eclipse.spelling.done.ISpellChecker#removeDictionary(org.eclipse.spelling.done.ISpellDictionary)
353          */
354         public final void removeDictionary(final ISpellDictionary dictionary) {
355                 // synchronizing is necessary as this is a write access
356                 fDictionaries.remove(dictionary);
357         }
358
359         /*
360          * @see org.eclipse.spelling.done.ISpellChecker#removeListener(org.eclipse.spelling.done.ISpellEventListener)
361          */
362         public final void removeListener(final ISpellEventListener listener) {
363                 // synchronizing is necessary as this is a write access
364                 fListeners.remove(listener);
365         }
366 }