/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.plugin.spellcheck; import java.beans.*; import java.io.*; import java.lang.ref.*; import java.net.*; import java.util.*; import net.java.sip.communicator.service.gui.*; import net.java.sip.communicator.service.gui.event.*; import net.java.sip.communicator.util.*; import org.dts.spell.dictionary.*; import org.jitsi.service.fileaccess.*; import org.jitsi.util.OSUtils; import org.osgi.framework.*; import javax.swing.*; /** * Model for spell checking capabilities. This allows for the on-demand * retrieval of dictionaries in other languages which are cached with the user's * configurations. * * @author Damian Johnson * @author Lyubomir Marinov */ class SpellChecker implements ChatListener { /** * The Logger used by the SpellChecker class and its * instances for logging output. */ private static final Logger logger = Logger.getLogger(SpellChecker.class); private static final String LOCALE_CONFIG_PARAM = "net.java.sip.communicator.plugin.spellchecker.LOCALE"; // default bundled dictionary private static final String DEFAULT_DICT_PATH = "/resources/config/spellcheck/"; // location where dictionaries are stored private static final String DICT_DIR = "spellingDictionaries/"; // filename of custom dictionary (added words) private static final String PERSONAL_DICT_NAME = "custom.per"; /** * System directory for hunspell-dictionaries. */ private static final String SYSTEM_HUNSPELL_DIR = "net.java.sip.communicator.plugin.spellcheck.SYSTEM_HUNSPELL_DIR"; /*- * Dictionary resources. * Note: Dictionary needs to be created with input streams that AREN'T CLOSED * (dictionaries will handle it). Closing the stream or creating with a file * will cause an internal NullPointerException in the spell checker. */ private File personalDictLocation; private File dictLocation; private SpellDictionary dict; private Parameters.Locale locale; // dictionary locale // chat instances the spell checker is currently attached to private ArrayList attachedChats = new ArrayList(); private boolean enabled = true; static final String LOCALE_CHANGED_PROP = "LocaleChanged"; /** * Listeners waiting for spell checker locale update. */ private final List> propertyListeners = new ArrayList>(); /** * Associates spell checking capabilities with all chats. This doesn't do * anything if this is already running. * * @param bc execution context of the bundle * @throws Exception */ synchronized void start(BundleContext bc) throws Exception { enabled = SpellCheckActivator.getConfigService().getBoolean( "plugin.spellcheck.ENABLE", true); FileAccessService faService = SpellCheckActivator.getFileAccessService(); // checks if DICT_DIR exists to see if this is the first run File dictionaryDir = faService.getPrivatePersistentFile(DICT_DIR, FileCategory.CACHE); if (!dictionaryDir.exists()) { dictionaryDir.mkdir(); } // gets resource for personal dictionary this.personalDictLocation = faService.getPrivatePersistentFile(DICT_DIR + PERSONAL_DICT_NAME, FileCategory.PROFILE); if (!personalDictLocation.exists()) personalDictLocation.createNewFile(); // gets dictionary locale String localeIso = SpellCheckActivator.getConfigService().getString( LOCALE_CONFIG_PARAM); if (localeIso == null) { // sets locale to be the default localeIso = Parameters.getDefault(Parameters.Default.LOCALE); if (localeIso == null) throw new Exception( "No default locale provided for spell checker"); } Parameters.Locale tmp = Parameters.getLocale(localeIso); if (tmp == null) throw new Exception("No dictionary resources defined for locale: " + localeIso); this.locale = tmp; // needed for synchronization lock SwingUtilities.invokeLater(new Runnable() { public void run() { // attaches to uiService so we'll be attached to future chats SpellCheckActivator.getUIService() .addChatListener(SpellChecker.this); for (Chat chat : SpellCheckActivator.getUIService() .getAllChats()) chatCreated(chat); } }); if (logger.isInfoEnabled()) logger.info("Spell Checker loaded."); } /** * Removes the spell checking capabilities from all chats. This doesn't do * anything if this isn't running. */ synchronized void stop() { // removes spell checker listeners from chats synchronized (this.attachedChats) { for (ChatAttachments chat : this.attachedChats) chat.detachListeners(); this.attachedChats.clear(); SpellCheckActivator.getUIService().removeChatListener(this); } } /** * Notifies this instance that a Chat has been closed. * * @param chat the Chat which has been closed */ public void chatClosed(Chat chat) { synchronized (attachedChats) { ChatAttachments wrapper = getChatAttachments(chat); if (wrapper != null) { attachedChats.remove(wrapper); wrapper.detachListeners(); } // this is the last chat, window is closed if(attachedChats.size() == 0) stop(); } } /** * Notifies this instance that a new Chat has been created. * Attaches listeners to the new chat. * * @param chat the new Chat which has been created */ public void chatCreated(Chat chat) { synchronized (attachedChats) { if (getChatAttachments(chat) == null && this.dict != null) { ChatAttachments wrapper = new ChatAttachments(chat, this.dict); // if it contains wrapper for the same chat, don't add it if(attachedChats.contains(wrapper)) return; wrapper.setEnabled(enabled); wrapper.attachListeners(); attachedChats.add(wrapper); } } } /** * Gets the ChatAttachments instance associated with a specific * Chat. * * @param chat the Chat whose associated ChatAttachments * instance is to be returned * @return the ChatAttachments instances associated with the * specified chat; otherwise, null */ private ChatAttachments getChatAttachments(Chat chat) { synchronized (attachedChats) { for (ChatAttachments chatAttachments : attachedChats) if (chatAttachments.chat.equals(chat)) return chatAttachments; } return null; } /** * Provides the user's list of words to be ignored by the spell checker. * * @return user's word list */ List getPersonalWords() { List personalWords = new ArrayList(); synchronized (personalDictLocation) { try { // Retrieves contents of the custom dictionary Scanner personalDictScanner = new Scanner(personalDictLocation); try { while (personalDictScanner.hasNextLine()) personalWords.add(personalDictScanner.nextLine()); } finally { personalDictScanner.close(); } } catch (FileNotFoundException exc) { logger.error("Unable to read custom dictionary", exc); } } return personalWords; } /** * Writes custom dictionary and updates spell checker to utilize new * listing. * * @param words words to be ignored by the spell checker */ void setPersonalWords(List words) { synchronized (this.personalDictLocation) { try { // writes new word list BufferedWriter writer = new BufferedWriter( new FileWriter(this.personalDictLocation)); for (String customWord : words) { writer.append(customWord); writer.newLine(); } writer.flush(); writer.close(); // resets dictionary being used to include changes synchronized (this.attachedChats) { InputStream dictInput = new FileInputStream(this.dictLocation); this.dict = new OpenOfficeSpellDictionary(dictInput, this.personalDictLocation); // updates chats for (ChatAttachments chat : this.attachedChats) chat.setDictionary(this.dict); } } catch (IOException exc) { logger.error("Unable to access personal spelling dictionary", exc); } } } /** * Provides the locale of the dictionary currently being used by the spell * checker. * * @return locale of current dictionary */ Parameters.Locale getLocale() { synchronized (this.locale) { return this.locale; } } /** * Resets spell checker to use a different locale's dictionary. This uses * the local copy of the dictionary if available, otherwise it's downloaded * and saved for future use. * * @param locale locale of dictionary to be used * @throws Exception problem occurring in utilizing locale's dictionary */ void setLocale(Parameters.Locale locale) throws Exception { synchronized (this.locale) { if(this.locale.equals(locale) && this.dict != null) return; File dictLocation = getLocalDictForLocale(locale); InputStream dictInput = null; InputStream affInput = null; if (OSUtils.IS_LINUX && !dictLocation.exists()) { String sysDir = SpellCheckActivator.getConfigService().getString( SYSTEM_HUNSPELL_DIR); File systemDict = new File(sysDir, locale.getIcuLocale() + ".dic"); if (systemDict.exists()) { dictInput = new FileInputStream(systemDict); affInput = new FileInputStream(new File(sysDir, locale.getIcuLocale() + ".aff")); } } if (!dictLocation.exists() && dictInput == null) { boolean dictFound = false; // see if the requested locale is a built-in that doesn't // need to be downloaded @SuppressWarnings ("unchecked") Enumeration dictUrls = SpellCheckActivator.bundleContext.getBundle() .findEntries(DEFAULT_DICT_PATH, "*.zip", false); if (dictUrls != null) { while (dictUrls.hasMoreElements()) { URL dictUrl = dictUrls.nextElement(); if (new File(dictUrl.getFile()).getName().equals( new File(locale.getDictUrl().getFile()).getName())) { dictInput = dictUrl.openStream(); dictFound = true; break; } } } // downloads dictionary if unavailable (not cached) if (!dictFound) { copyDictionary(locale.getDictUrl().openStream(), dictLocation); } } if (dictInput == null) { dictInput = new FileInputStream(dictLocation); } // resets dictionary being used to include changes synchronized (this.attachedChats) { SpellDictionary dict; if (affInput == null) { dict = new OpenOfficeSpellDictionary(dictInput, this.personalDictLocation); } else { dict = new OpenOfficeSpellDictionary(affInput, dictInput, personalDictLocation, true); } this.dict = dict; this.dictLocation = dictLocation; Parameters.Locale oldLocale = this.locale; this.locale = locale; // saves locale choice to configuration properties SpellCheckActivator.getConfigService().setProperty( LOCALE_CONFIG_PARAM, locale.getIsoCode()); // updates chats for (ChatAttachments chat : this.attachedChats) chat.setDictionary(this.dict); firePropertyChangedEvent( LOCALE_CHANGED_PROP, oldLocale, this.locale); } } } /** * Gets the file object for user-installed dictionaries. * * @param locale The locale whose filename is needed. * @return The file object of the locale. * @throws Exception */ File getLocalDictForLocale(Parameters.Locale locale) throws Exception { String path = locale.getDictUrl().getFile(); int filenameStart = path.lastIndexOf('/') + 1; String filename = path.substring(filenameStart); File dictLocation = SpellCheckActivator.getFileAccessService() .getPrivatePersistentFile(DICT_DIR + filename, FileCategory.CACHE); return dictLocation; } /** * Removes the dictionary from the system, and sets the default locale * dictionary as the current dictionary * * @param locale locale to be removed */ void removeLocale(Parameters.Locale locale) throws Exception { synchronized (this.locale) { String path = locale.getDictUrl().getFile(); int filenameStart = path.lastIndexOf('/') + 1; String filename = path.substring(filenameStart); File dictLocation = SpellCheckActivator.getFileAccessService() .getPrivatePersistentFile(DICT_DIR + filename, FileCategory.CACHE); if (dictLocation.exists()) dictLocation.delete(); String localeIso = Parameters.getDefault(Parameters.Default.LOCALE); Parameters.Locale loc = Parameters.getLocale(localeIso); setLocale(loc); } } /** * Determines if locale's dictionary is locally available or a system. * * @param locale locale to be checked * @return true if local resources for dictionary are available and * accessible, false otherwise */ boolean isLocaleAvailable(Parameters.Locale locale) { try { if (getLocalDictForLocale(locale).exists()) { return true; } else { @SuppressWarnings ("unchecked") Enumeration dictUrls = SpellCheckActivator.bundleContext.getBundle() .findEntries(DEFAULT_DICT_PATH, "*.zip", false); if (dictUrls != null) { while (dictUrls.hasMoreElements()) { URL dictUrl = dictUrls.nextElement(); if (new File(dictUrl.getFile()).getName().equals( new File(locale.getDictUrl().getFile()).getName())) { return true; } } } } return false; } catch (Exception exc) { return false; } } /** * Determines if locale's dictionary is locally available or a system. * * @param locale locale to be checked * @return true if local resources for dictionary are available and * accessible, false otherwise */ boolean isUserLocale(Parameters.Locale locale) { try { return getLocalDictForLocale(locale).exists(); } catch (Exception exc) { return false; } } boolean isEnabled() { synchronized (this.attachedChats) { return enabled; } } void setEnabled(boolean enabled) { if (this.enabled != enabled) { synchronized (this.attachedChats) { this.enabled = enabled; for (ChatAttachments chatAttachment : this.attachedChats) chatAttachment.setEnabled(this.enabled); } } } // copies dictionary to appropriate location, closing the stream afterward private void copyDictionary(InputStream input, File dest) throws IOException, FileNotFoundException { byte[] buf = new byte[1024]; FileOutputStream output = new FileOutputStream(dest); int len; while ((len = input.read(buf)) > 0) output.write(buf, 0, len); input.close(); output.close(); } /** * Determines if spell checker dictionary works. Backend API often fails * when used so this tests that the current dictionary is able to process * words. * * @return true if current dictionary can check words, false otherwise */ private boolean isDictionaryValid(SpellDictionary dict) { try { // spell checker API often works until used dict.isCorrect("foo"); return true; } catch (Exception exc) { logger.error("Dictionary validation failed", exc); return false; } } /** * Adds a PropertyChangeListener to the listener list. *

* @param listener the PropertyChangeListener to be added */ public void addPropertyChangeListener(PropertyChangeListener listener) { synchronized (propertyListeners) { Iterator> i = propertyListeners.iterator(); boolean contains = false; while (i.hasNext()) { PropertyChangeListener l = i.next().get(); if (l == null) i.remove(); else if (l.equals(listener)) contains = true; } if (!contains) propertyListeners.add( new WeakReference(listener)); } } /** * Removes a PropertyChangeListener from the listener list. *

* @param listener the PropertyChangeListener to be removed */ public void removePropertyChangeListener(PropertyChangeListener listener) { synchronized (propertyListeners) { Iterator> i = propertyListeners.iterator(); while (i.hasNext()) { PropertyChangeListener l = i.next().get(); if ((l == null) || l.equals(listener)) i.remove(); } } } /** * Fires event. * @param property * @param oldValue * @param newValue */ private void firePropertyChangedEvent(String property, Object oldValue, Object newValue) { PropertyChangeEvent evt = new PropertyChangeEvent( this, property, oldValue, newValue); if (logger.isDebugEnabled()) logger.debug("Will dispatch the following plugin component event: " + evt); synchronized (propertyListeners) { Iterator> i = propertyListeners.iterator(); while (i.hasNext()) { PropertyChangeListener l = i.next().get(); if (l == null) i.remove(); else { l.propertyChange(evt); } } } } }