Java tutorial
/* LanguageTool, a natural language style checker * Copyright (C) 2005 Daniel Naber (http://www.danielnaber.de) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 * USA */ package org.languagetool.openoffice; import java.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.ResourceBundle; import java.util.Set; import javax.swing.UIManager; import com.sun.star.lang.*; import com.sun.star.linguistic2.LinguServiceEvent; import com.sun.star.linguistic2.LinguServiceEventFlags; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.languagetool.JLanguageTool; import org.languagetool.Language; import org.languagetool.Languages; import org.languagetool.gui.AboutDialog; import org.languagetool.gui.Configuration; import com.sun.star.beans.PropertyValue; import com.sun.star.lib.uno.helper.Factory; import com.sun.star.lib.uno.helper.WeakBase; import com.sun.star.linguistic2.ProofreadingResult; import com.sun.star.linguistic2.XLinguServiceEventBroadcaster; import com.sun.star.linguistic2.XLinguServiceEventListener; import com.sun.star.linguistic2.XProofreader; import com.sun.star.registry.XRegistryKey; import com.sun.star.task.XJobExecutor; import com.sun.star.uno.UnoRuntime; import com.sun.star.uno.XComponentContext; /** * LibreOffice/OpenOffice integration. * * @author Marcin Mikowski, Fred Kruse */ public class Main extends WeakBase implements XJobExecutor, XServiceDisplayName, XServiceInfo, XProofreader, XLinguServiceEventBroadcaster, XEventListener { // Service name required by the OOo API && our own name. private static final String[] SERVICE_NAMES = { "com.sun.star.linguistic2.Proofreader", "org.languagetool.openoffice.Main" }; private static final String VENDOR_ID = "languagetool.org"; private static final String APPLICATION_ID = "LanguageTool"; private static final String OFFICE_EXTENSION_ID = "LibreOffice"; private static final String CONFIG_FILE = "Languagetool.cfg"; private static final String OLD_CONFIG_FILE = ".languagetool-ooo.cfg"; private static final String LOG_FILE = "LanguageTool.log"; private static final ResourceBundle MESSAGES = JLanguageTool.getMessageBundle(); // LibreOffice (since 4.2.0) special tag for locale with variant // e.g. language ="qlt" country="ES" variant="ca-ES-valencia": private static final String LIBREOFFICE_SPECIAL_LANGUAGE_TAG = "qlt"; private final List<XLinguServiceEventListener> xEventListeners; // Rules disabled using the config dialog box rather than Spelling dialog box // or the context menu. private Set<String> disabledRules = null; private Set<String> disabledRulesUI; private boolean docReset = false; private XComponentContext xContext; private MultiDocumentsHandler documents = null; public Main(XComponentContext xCompContext) { changeContext(xCompContext); xEventListeners = new ArrayList<>(); File homeDir = getHomeDir(); File configDir = getLOConfigDir(); String configDirName = configDir == null ? "." : configDir.toString(); File oldConfigFile = homeDir == null ? null : new File(homeDir, OLD_CONFIG_FILE); MessageHandler.init(configDirName, LOG_FILE); documents = new MultiDocumentsHandler(xContext, configDir, CONFIG_FILE, oldConfigFile, MESSAGES, this); } private Configuration prepareConfig() { try { Configuration config = documents.getConfiguration(); if (config != null) { disabledRules = config.getDisabledRuleIds(); } if (disabledRules == null) { disabledRules = new HashSet<>(); } disabledRulesUI = new HashSet<>(disabledRules); return config; } catch (Throwable t) { MessageHandler.showError(t); } return null; } void changeContext(XComponentContext xCompContext) { xContext = xCompContext; if (documents != null) { documents.setComponentContext(xCompContext); } } /** * Runs the grammar checker on paragraph text. * * @param docID document ID * @param paraText paragraph text * @param locale Locale the text Locale * @param startOfSentencePos start of sentence position * @param nSuggestedBehindEndOfSentencePosition end of sentence position * @return ProofreadingResult containing the results of the check. */ @Override public final ProofreadingResult doProofreading(String docID, String paraText, Locale locale, int startOfSentencePos, int nSuggestedBehindEndOfSentencePosition, PropertyValue[] propertyValues) { ProofreadingResult paRes = new ProofreadingResult(); paRes.nStartOfSentencePosition = startOfSentencePos; paRes.nStartOfNextSentencePosition = nSuggestedBehindEndOfSentencePosition; paRes.nBehindEndOfSentencePosition = paRes.nStartOfNextSentencePosition; paRes.xProofreader = this; paRes.aLocale = locale; paRes.aDocumentIdentifier = docID; paRes.aText = paraText; paRes.aProperties = propertyValues; try { int[] footnotePositions = getPropertyValues("FootnotePositions", propertyValues); // since LO 4.3 paRes = documents.getCheckResults(paraText, locale, paRes, footnotePositions, docReset); docReset = false; if (disabledRules == null) { prepareConfig(); } if (documents.doResetCheck()) { resetCheck(); documents.optimizeReset(); } } catch (Throwable t) { MessageHandler.showError(t); } return paRes; } private int[] getPropertyValues(String propName, PropertyValue[] propertyValues) { for (PropertyValue propertyValue : propertyValues) { if (propName.equals(propertyValue.Name)) { if (propertyValue.Value instanceof int[]) { return (int[]) propertyValue.Value; } else { MessageHandler.printToLogFile("Not of expected type int[]: " + propertyValue.Name + ": " + propertyValue.Value.getClass()); } } } return new int[] {}; // e.g. for LO/OO < 4.3 and the 'FootnotePositions' property } /** * We leave spell checking to OpenOffice/LibreOffice. * @return false */ @Override public final boolean isSpellChecker() { return false; } /** * Returns xContext */ public XComponentContext getContext() { return xContext; } /** * Runs LT options dialog box. */ private void runOptionsDialog() { Configuration config = prepareConfig(); Language lang = config.getDefaultLanguage(); if (lang == null) { lang = documents.getLanguage(); } if (lang == null) { return; } ConfigThread configThread = new ConfigThread(lang, config, this); configThread.start(); } /** * @return An array of Locales supported by LT */ @Override public final Locale[] getLocales() { try { List<Locale> locales = new ArrayList<>(); for (Language lang : Languages.get()) { if (lang.getCountries().length == 0) { // e.g. Esperanto if (lang.getVariant() != null) { locales.add(new Locale(LIBREOFFICE_SPECIAL_LANGUAGE_TAG, "", lang.getShortCodeWithCountryAndVariant())); } else { locales.add(new Locale(lang.getShortCode(), "", "")); } } else { for (String country : lang.getCountries()) { if (lang.getVariant() != null) { locales.add(new Locale(LIBREOFFICE_SPECIAL_LANGUAGE_TAG, country, lang.getShortCodeWithCountryAndVariant())); } else { locales.add(new Locale(lang.getShortCode(), country, "")); } } } } return locales.toArray(new Locale[0]); } catch (Throwable t) { MessageHandler.showError(t); return new Locale[0]; } } /** * @return true if LT supports the language of a given locale * @param locale The Locale to check */ @Override public final boolean hasLocale(Locale locale) { return documents.hasLocale(locale); } /** * Add a listener that allow re-checking the document after changing the * options in the configuration dialog box. * * @param eventListener the listener to be added * @return true if listener is non-null and has been added, false otherwise */ @Override public final boolean addLinguServiceEventListener(XLinguServiceEventListener eventListener) { if (eventListener == null) { return false; } xEventListeners.add(eventListener); return true; } /** * Remove a listener from the event listeners list. * * @param eventListener the listener to be removed * @return true if listener is non-null and has been removed, false otherwise */ @Override public final boolean removeLinguServiceEventListener(XLinguServiceEventListener eventListener) { if (eventListener == null) { return false; } if (xEventListeners.contains(eventListener)) { xEventListeners.remove(eventListener); return true; } return false; } /** * Inform listener that the doc should be rechecked. */ private boolean resetCheck() { if (!xEventListeners.isEmpty()) { for (XLinguServiceEventListener xEvLis : xEventListeners) { if (xEvLis != null) { LinguServiceEvent xEvent = new LinguServiceEvent(); xEvent.nEvent = LinguServiceEventFlags.PROOFREAD_AGAIN; xEvLis.processLinguServiceEvent(xEvent); } } return true; } return false; } /** * Inform listener (grammar checking iterator) that options have changed and * the doc should be rechecked. */ void resetDocument() { documents.setRecheck(); if (resetCheck()) { Configuration config = documents.getConfiguration(); disabledRules = config.getDisabledRuleIds(); if (disabledRules == null) { disabledRules = new HashSet<>(); } } } @Override public String[] getSupportedServiceNames() { return getServiceNames(); } static String[] getServiceNames() { return SERVICE_NAMES; } @Override public boolean supportsService(String sServiceName) { for (String sName : SERVICE_NAMES) { if (sServiceName.equals(sName)) { return true; } } return false; } @Override public String getImplementationName() { return Main.class.getName(); } public static XSingleComponentFactory __getComponentFactory(String sImplName) { SingletonFactory xFactory = null; if (sImplName.equals(Main.class.getName())) { xFactory = new SingletonFactory(); } return xFactory; } public static boolean __writeRegistryServiceInfo(XRegistryKey regKey) { return Factory.writeRegistryServiceInfo(Main.class.getName(), Main.getServiceNames(), regKey); } @Override public void trigger(String sEvent) { if (Thread.currentThread().getContextClassLoader() == null) { Thread.currentThread().setContextClassLoader(Main.class.getClassLoader()); } if (!javaVersionOkay()) { return; } try { if ("configure".equals(sEvent)) { runOptionsDialog(); } else if ("about".equals(sEvent)) { AboutDialogThread aboutThread = new AboutDialogThread(MESSAGES); aboutThread.start(); } else if ("switchOff".equals(sEvent)) { if (documents.toggleSwitchedOff()) { resetCheck(); } } else if ("ignoreOnce".equals(sEvent)) { documents.ignoreOnce(); resetCheck(); documents.optimizeReset(); } else { MessageHandler.printToLogFile("Sorry, don't know what to do, sEvent = " + sEvent); } } catch (Throwable e) { MessageHandler.showError(e); } } private boolean javaVersionOkay() { String version = System.getProperty("java.version"); if (version != null && (version.startsWith("1.0") || version.startsWith("1.1") || version.startsWith("1.2") || version.startsWith("1.3") || version.startsWith("1.4") || version.startsWith("1.5") || version.startsWith("1.6") || version.startsWith("1.7"))) { MessageHandler.showMessage("Error: LanguageTool requires Java 8 or later. Current version: " + version); return false; } try { // do not set look and feel for on Mac OS X as it causes the following error: // soffice[2149:2703] Apple AWT Java VM was loaded on first thread -- can't start AWT. if (!System.getProperty("os.name").contains("OS X")) { // Cross-Platform Look And Feel @since 3.7 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } } catch (Exception ignored) { // Well, what can we do... } return true; } private File getHomeDir() { String homeDir = System.getProperty("user.home"); if (homeDir == null) { MessageHandler.showError(new RuntimeException("Could not get home directory")); return null; } return new File(homeDir); } /** * Returns directory to store every information for LT office extension * @since 4.7 */ private File getLOConfigDir() { String userHome = null; File directory; try { userHome = System.getProperty("user.home"); } catch (SecurityException ex) { } if (userHome == null) { MessageHandler.showError(new RuntimeException("Could not get home directory")); directory = null; } else if (SystemUtils.IS_OS_WINDOWS) { File appDataDir = null; try { String appData = System.getenv("APPDATA"); if (!StringUtils.isEmpty(appData)) { appDataDir = new File(appData); } } catch (SecurityException ex) { } if (appDataDir != null && appDataDir.isDirectory()) { String path = VENDOR_ID + "\\" + APPLICATION_ID + "\\" + OFFICE_EXTENSION_ID + "\\"; directory = new File(appDataDir, path); } else { String path = "Application Data\\" + VENDOR_ID + "\\" + APPLICATION_ID + "\\" + OFFICE_EXTENSION_ID + "\\"; directory = new File(userHome, path); } } else if (SystemUtils.IS_OS_LINUX) { File appDataDir = null; try { String xdgConfigHome = System.getenv("XDG_CONFIG_HOME"); if (!StringUtils.isEmpty(xdgConfigHome)) { appDataDir = new File(xdgConfigHome); if (!appDataDir.isAbsolute()) { //https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html //All paths set in these environment variables must be absolute. //If an implementation encounters a relative path in any of these //variables it should consider the path invalid and ignore it. appDataDir = null; } } } catch (SecurityException ex) { } if (appDataDir != null && appDataDir.isDirectory()) { String path = APPLICATION_ID + "/" + OFFICE_EXTENSION_ID + "/"; directory = new File(appDataDir, path); } else { String path = ".config/" + APPLICATION_ID + "/" + OFFICE_EXTENSION_ID + "/"; directory = new File(userHome, path); } } else if (SystemUtils.IS_OS_MAC_OSX) { String path = "Library/Application Support/" + APPLICATION_ID + "/" + OFFICE_EXTENSION_ID + "/"; directory = new File(userHome, path); } else { String path = "." + APPLICATION_ID + "/" + OFFICE_EXTENSION_ID + "/"; directory = new File(userHome, path); } if (directory != null && !directory.exists()) { directory.mkdirs(); } return directory; } /** * Will throw exception instead of showing errors as dialogs - use only for test cases. * @since 2.9 */ void setTestMode(boolean mode) { documents.setTestMode(mode); MessageHandler.setTestMode(mode); } private static class AboutDialogThread extends Thread { private final ResourceBundle messages; AboutDialogThread(ResourceBundle messages) { this.messages = messages; } @Override public void run() { // Note: null can cause the dialog to appear on the wrong screen in a // multi-monitor setup, but we just don't have a proper java.awt.Component // here which we could use instead: AboutDialog about = new AboutDialog(messages, null); about.show(); } } /** * Called when "Ignore" is selected e.g. in the context menu for an error. */ @Override public void ignoreRule(String ruleId, Locale locale) { /* TODO: config should be locale-dependent */ Configuration config = documents.getConfiguration(); disabledRulesUI.add(ruleId); config.setDisabledRuleIds(disabledRulesUI); try { SwJLanguageTool langTool = documents.getLanguageTool(); documents.initCheck(); config.saveConfiguration(langTool.getLanguage()); } catch (Throwable t) { MessageHandler.showError(t); } documents.setRecheck(); } /** * Called on rechecking the document - resets the ignore status for rules that * was set in the spelling dialog box or in the context menu. * * The rules disabled in the config dialog box are left as intact. */ @Override public void resetIgnoreRules() { Configuration config = documents.getConfiguration(); config.setDisabledRuleIds(disabledRules); try { SwJLanguageTool langTool = documents.getLanguageTool(); documents.initCheck(); config.saveConfiguration(langTool.getLanguage()); } catch (Throwable t) { MessageHandler.showError(t); } documents.setRecheck(); docReset = true; } @Override public String getServiceDisplayName(Locale locale) { return "LanguageTool"; } /** * remove internal stored text if document disposes */ @Override public void disposing(EventObject source) { // the data of document will be removed by next call of getNumDocID // to finish checking thread without crashing XComponent goneContext = UnoRuntime.queryInterface(XComponent.class, source.Source); documents.setContextOfClosedDoc(goneContext); goneContext.removeEventListener(this); } }