content: /*******************************************************************************
   * Copyright (c) 2000, 2008 IBM Corporation and others.
   * All rights reserved. This program and the accompanying materials
   * are made available under the terms of the Eclipse Public License v1.0
   * which accompanies this distribution, and is available at
   * http://www.eclipse.org/legal/epl-v10.html
   *
   * Contributors:
   *     IBM Corporation - initial API and implementation
   *******************************************************************************/
  
  package org.eclipse.wst.jsdt.internal.ui.text.spelling.engine;
  
  import java.util.Collections;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.Locale;
  import java.util.Set;
  
  import org.eclipse.core.runtime.Assert;
  import org.eclipse.jface.preference.IPreferenceStore;
  import org.eclipse.wst.jsdt.ui.PreferenceConstants;
  
  /**
   * Default spell checker for standard text.
   *
   * 
   */
  public class DefaultSpellChecker implements ISpellChecker {
  
  	/** Array of URL prefixes */
  	public static final String[] URL_PREFIXES= new String[] { "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$
  
  	/**
  	 * Does this word contain digits?
  	 *
  	 * @param word the word to check
  	 * @return <code>true</code> iff this word contains digits, <code>false></code> otherwise
  	 */
  	protected static boolean isDigits(final String word) {
  
  		for (int index= 0; index < word.length(); index++) {
  
  			if (Character.isDigit(word.charAt(index)))
  				return true;
  		}
  		return false;
  	}
  
  	/**
  	 * Does this word contain mixed-case letters?
  	 *
  	 * @param word
  	 *                   The word to check
  	 * @param sentence
  	 *                   <code>true</code> iff the specified word starts a new
  	 *                   sentence, <code>false</code> otherwise
  	 * @return <code>true</code> iff the contains mixed-case letters, <code>false</code>
  	 *               otherwise
  	 */
  	protected static boolean isMixedCase(final String word, final boolean sentence) {
  
  		final int length= word.length();
  		boolean upper= Character.isUpperCase(word.charAt(0));
  
  		if (sentence && upper && (length > 1))
  			upper= Character.isUpperCase(word.charAt(1));
  
  		if (upper) {
  
  			for (int index= length - 1; index > 0; index--) {
  				if (Character.isLowerCase(word.charAt(index)))
  					return true;
  			}
  		} else {
  
  			for (int index= length - 1; index > 0; index--) {
  				if (Character.isUpperCase(word.charAt(index)))
  					return true;
  			}
  		}
  		return false;
  	}
  
  	/**
  	 * Does this word contain upper-case letters only?
  	 *
  	 * @param word
  	 *                   The word to check
  	 * @return <code>true</code> iff this word only contains upper-case
  	 *               letters, <code>false</code> otherwise
  	 */
  	protected static boolean isUpperCase(final String word) {
  
  		for (int index= word.length() - 1; index >= 0; index--) {
  
  			if (Character.isLowerCase(word.charAt(index)))
  				return false;
  		}
  		return true;
  	}
  
  	/**
  	 * Does this word look like an URL?
  	 *
  	 * @param word
  	 *                   The word to check
  	 * @return <code>true</code> iff this word looks like an URL, <code>false</code>
  	 *               otherwise
  	 */
  	protected static boolean isUrl(final String word) {
  
  		for (int index= 0; index < URL_PREFIXES.length; index++) {
  
  			if (word.startsWith(URL_PREFIXES[index]))
  				return true;
  		}
  		return false;
  	}
  
  	/**
  	 * The dictionaries to use for spell checking. Synchronized to avoid
  	 * concurrent modifications.
  	 */
  	private final Set fDictionaries= Collections.synchronizedSet(new HashSet());
  
  	/**
  	 * The words to be ignored. Synchronized to avoid concurrent modifications.
  	 */
  	private final Set fIgnored= Collections.synchronizedSet(new HashSet());
  
  	/**
  	 * The spell event listeners. Synchronized to avoid concurrent
  	 * modifications.
  	 */
  	private final Set fListeners= Collections.synchronizedSet(new HashSet());
  
  	/**
  	 * The preference store. Assumes the <code>IPreferenceStore</code>
  	 * implementation is thread safe.
  	 */
  	private final IPreferenceStore fPreferences;
  
  	/**
  	 * The locale of this checker.
  	 * 
  	 */
  	private Locale fLocale;
  
  	/**
  	 * Creates a new default spell checker.
  	 *
  	 * @param store the preference store for this spell checker
  	 * @param locale the locale
  	 */
  	public DefaultSpellChecker(IPreferenceStore store, Locale locale) {
  		Assert.isLegal(store != null);
  		Assert.isLegal(locale != null);
  		
  		fPreferences= store;
  		fLocale= locale;
  	}
  
  	/*
  	 * @see org.eclipse.spelling.done.ISpellChecker#addDictionary(org.eclipse.spelling.done.ISpellDictionary)
  	 */
  	public final void addDictionary(final ISpellDictionary dictionary) {
  		// synchronizing is necessary as this is a write access
  		fDictionaries.add(dictionary);
  	}
  
  	/*
  	 * @see org.eclipse.spelling.done.ISpellChecker#addListener(org.eclipse.spelling.done.ISpellEventListener)
  	 */
  	public final void addListener(final ISpellEventListener listener) {
  		// synchronizing is necessary as this is a write access
  		fListeners.add(listener);
  	}
  
  	/*
  	 * @see org.eclipse.wst.jsdt.ui.text.spelling.engine.ISpellChecker#acceptsWords()
  	 */
  	public boolean acceptsWords() {
  		// synchronizing might not be needed here since acceptWords is
  		// a read-only access and only called in the same thread as
  		// the modifying methods add/checkWord (?)
  		Set copy;
  		synchronized (fDictionaries) {
  			copy= new HashSet(fDictionaries);
  		}
  
  		ISpellDictionary dictionary= null;
  		for (final Iterator iterator= copy.iterator(); iterator.hasNext();) {
  
  			dictionary= (ISpellDictionary)iterator.next();
  			if (dictionary.acceptsWords())
  				return true;
  		}
  		return false;
  	}
  
  	/*
  	 * @see org.eclipse.wst.jsdt.internal.ui.text.spelling.engine.ISpellChecker#addWord(java.lang.String)
  	 */
  	public void addWord(final String word) {
  		// synchronizing is necessary as this is a write access
  		Set copy;
  		synchronized (fDictionaries) {
  			copy= new HashSet(fDictionaries);
  		}
  
  		final String addable= word.toLowerCase();
  		for (final Iterator iterator= copy.iterator(); iterator.hasNext();) {
  			ISpellDictionary dictionary= (ISpellDictionary)iterator.next();
  			if (dictionary.acceptsWords())
  				dictionary.addWord(addable);
  		}
  	
  	}
  
  	/*
  	 * @see org.eclipse.wst.jsdt.ui.text.spelling.engine.ISpellChecker#checkWord(java.lang.String)
  	 */
  	public final void checkWord(final String word) {
  		// synchronizing is necessary as this is a write access
  		fIgnored.remove(word.toLowerCase());
  	}
  
  	/*
  	 * @see org.eclipse.spelling.done.ISpellChecker#execute(org.eclipse.spelling.ISpellCheckTokenizer)
  	 */
  	public void execute(final ISpellCheckIterator iterator) {
  
  		final boolean ignoreDigits= fPreferences.getBoolean(PreferenceConstants.SPELLING_IGNORE_DIGITS);
  		final boolean ignoreMixed= fPreferences.getBoolean(PreferenceConstants.SPELLING_IGNORE_MIXED);
  		final boolean ignoreSentence= fPreferences.getBoolean(PreferenceConstants.SPELLING_IGNORE_SENTENCE);
  		final boolean ignoreUpper= fPreferences.getBoolean(PreferenceConstants.SPELLING_IGNORE_UPPER);
  		final boolean ignoreURLS= fPreferences.getBoolean(PreferenceConstants.SPELLING_IGNORE_URLS);
  		final boolean ignoreNonLetters= fPreferences.getBoolean(PreferenceConstants.SPELLING_IGNORE_NON_LETTERS);
  		final boolean ignoreSingleLetters= fPreferences.getBoolean(PreferenceConstants.SPELLING_IGNORE_SINGLE_LETTERS);
  		
  		iterator.setIgnoreSingleLetters(ignoreSingleLetters);
  		
  		Iterator iter= fDictionaries.iterator();
  		while (iter.hasNext())
  			((ISpellDictionary)iter.next()).setStripNonLetters(ignoreNonLetters);
  
  		String word= null;
  		boolean starts= false;
  
  		while (iterator.hasNext()) {
  
  			word= (String)iterator.next();
  			if (word != null) {
  
  				// synchronizing is necessary as this is called inside the reconciler
  				if (!fIgnored.contains(word)) {
  
  					starts= iterator.startsSentence();
  					if (!isCorrect(word)) {
  
  					    boolean isMixed=  isMixedCase(word, true);
  					    boolean isUpper= isUpperCase(word);
  					    boolean isDigits= isDigits(word);
  					    boolean isURL= isUrl(word);
  
  					    if ( !ignoreMixed && isMixed || !ignoreUpper && isUpper || !ignoreDigits && isDigits || !ignoreURLS && isURL || !(isMixed || isUpper || isDigits || isURL))
  					        fireEvent(new SpellEvent(this, word, iterator.getBegin(), iterator.getEnd(), starts, false));
  
  					} else {
  
  						if (!ignoreSentence && starts && Character.isLowerCase(word.charAt(0)))
  							fireEvent(new SpellEvent(this, word, iterator.getBegin(), iterator.getEnd(), true, true));
  					}
  				}
  			}
  		}
  	}
  
  	/**
  	 * Fires the specified event.
  	 *
  	 * @param event
  	 *                   Event to fire
  	 */
  	protected final void fireEvent(final ISpellEvent event) {
  		// synchronizing is necessary as this is called from execute
  		Set copy;
  		synchronized (fListeners) {
  			copy= new HashSet(fListeners);
  		}
  		for (final Iterator iterator= copy.iterator(); iterator.hasNext();) {
  			((ISpellEventListener)iterator.next()).handle(event);
  		}
  	}
  
  	/*
  	 * @see org.eclipse.spelling.done.ISpellChecker#getProposals(java.lang.String,boolean)
  	 */
  	public Set getProposals(final String word, final boolean sentence) {
  
  		// synchronizing might not be needed here since getProposals is
  		// a read-only access and only called in the same thread as
  		// the modifing methods add/removeDictionary (?)
  		Set copy;
  		synchronized (fDictionaries) {
  			copy= new HashSet(fDictionaries);
  		}
  
  		ISpellDictionary dictionary= null;
  		final HashSet proposals= new HashSet();
  
  		for (final Iterator iterator= copy.iterator(); iterator.hasNext();) {
  
  			dictionary= (ISpellDictionary)iterator.next();
  			proposals.addAll(dictionary.getProposals(word, sentence));
  		}
  		return proposals;
  	}
  
  	/*
  	 * @see org.eclipse.wst.jsdt.internal.ui.text.spelling.engine.ISpellChecker#ignoreWord(java.lang.String)
  	 */
  	public final void ignoreWord(final String word) {
  		// synchronizing is necessary as this is a write access
  		fIgnored.add(word.toLowerCase());
  	}
  
  	/*
  	 * @see org.eclipse.wst.jsdt.internal.ui.text.spelling.engine.ISpellChecker#isCorrect(java.lang.String)
  	 */
  	public final boolean isCorrect(final String word) {
  		// synchronizing is necessary as this is called from execute
  		Set copy;
  		synchronized (fDictionaries) {
  			copy= new HashSet(fDictionaries);
  		}
  
  		if (fIgnored.contains(word.toLowerCase()))
  			return true;
  
  		ISpellDictionary dictionary= null;
  		for (final Iterator iterator= copy.iterator(); iterator.hasNext();) {
  
  			dictionary= (ISpellDictionary)iterator.next();
  			if (dictionary.isCorrect(word))
  				return true;
  		}
  		return false;
  	}
  
  	/*
  	 * @see org.eclipse.spelling.done.ISpellChecker#removeDictionary(org.eclipse.spelling.done.ISpellDictionary)
  	 */
  	public final void removeDictionary(final ISpellDictionary dictionary) {
  		// synchronizing is necessary as this is a write access
  		fDictionaries.remove(dictionary);
  	}
  
  	/*
  	 * @see org.eclipse.spelling.done.ISpellChecker#removeListener(org.eclipse.spelling.done.ISpellEventListener)
  	 */
  	public final void removeListener(final ISpellEventListener listener) {
  		// synchronizing is necessary as this is a write access
  		fListeners.remove(listener);
  	}
  
  	/*
  	 * @see org.eclipse.wst.jsdt.internal.ui.text.spelling.engine.ISpellChecker#getLocale()
  	 * 
  	 */
  	public Locale getLocale() {
  		return fLocale;
  	}
  }
  