summaryrefslogtreecommitdiffstats
path: root/JLanguageTool/src/java/de/danielnaber/languagetool/openoffice/Main.java
blob: 3eaecda17ec1f433562351268e314c939940f5d7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
/* 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 de.danielnaber.languagetool.openoffice;

/** OpenOffice 3.x Integration
 * 
 * @author Marcin Miłkowski
 */
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Set;

import javax.swing.JOptionPane;
import javax.swing.UIManager;

import com.sun.star.awt.XWindow;
import com.sun.star.awt.XWindowPeer;
import com.sun.star.beans.PropertyValue;
import com.sun.star.beans.XPropertySet;
import com.sun.star.frame.XDesktop;
import com.sun.star.frame.XModel;
import com.sun.star.lang.IllegalArgumentException;
import com.sun.star.lang.Locale;
import com.sun.star.lang.XComponent;
import com.sun.star.lang.XMultiComponentFactory;
import com.sun.star.lang.XServiceDisplayName;
import com.sun.star.lang.XServiceInfo;
import com.sun.star.lang.XSingleComponentFactory;
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.SingleProofreadingError;
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.text.XTextViewCursor;
import com.sun.star.text.XTextViewCursorSupplier;
import com.sun.star.uno.UnoRuntime;
import com.sun.star.uno.XComponentContext;

import de.danielnaber.languagetool.JLanguageTool;
import de.danielnaber.languagetool.Language;
import de.danielnaber.languagetool.gui.Configuration;
import de.danielnaber.languagetool.rules.RuleMatch;
import de.danielnaber.languagetool.tools.StringTools;

public class Main extends WeakBase implements XJobExecutor,
    XServiceDisplayName, XServiceInfo, XProofreader,
    XLinguServiceEventBroadcaster {

  private Configuration config;
  private JLanguageTool langTool;
  private Language docLanguage;

  private String docID;

  /*
   * Rules disabled using the config dialog box rather than Spelling dialog box
   * or the context menu.
   */
  private Set<String> disabledRules;

  private Set<String> disabledRulesUI;

  private List<XLinguServiceEventListener> xEventListeners;

  /**
   * Make another instance of JLanguageTool and assign it to langTool if true.
   */
  private boolean recheck;

  /**
   * Sentence tokenization-related members.
   */

  private String currentPara;
  private List<String> tokenizedSentences;
  private int position;
  private List<RuleMatch> paragraphMatches;

  /**
   * Service name required by the OOo API && our own name.
   */
  private static final String[] SERVICE_NAMES = {
      "com.sun.star.linguistic2.Proofreader",
      "de.danielnaber.languagetool.openoffice.Main" };

  // use a different name than the stand-alone version to avoid conflicts:
  private static final String CONFIG_FILE = ".languagetool-ooo.cfg";

  private static final ResourceBundle MESSAGES = JLanguageTool
      .getMessageBundle();

  private XComponentContext xContext;

  public Main(final XComponentContext xCompContext) {
    try {
      changeContext(xCompContext);
      final File homeDir = getHomeDir();
      config = new Configuration(homeDir, CONFIG_FILE);
      disabledRules = config.getDisabledRuleIds();
      if (disabledRules == null) {
        disabledRules = new HashSet<String>();
      }
      disabledRulesUI = new HashSet<String>(disabledRules);
      xEventListeners = new ArrayList<XLinguServiceEventListener>();
    } catch (final Throwable t) {
      showError(t);
    }
  }

  public final void changeContext(final XComponentContext xCompContext) {
    xContext = xCompContext;
  }

  private XComponent getxComponent() {
    try {
      final XMultiComponentFactory xMCF = xContext.getServiceManager();
      final Object desktop = xMCF.createInstanceWithContext(
          "com.sun.star.frame.Desktop", xContext);
      final XDesktop xDesktop = (XDesktop) UnoRuntime.queryInterface(
          XDesktop.class, desktop);
      return xDesktop.getCurrentComponent();
    } catch (final Throwable t) {
      showError(t);
      return null;
    }
  }

  /**
   * Checks the language under the cursor. Used for opening the configuration
   * dialog.
   * 
   * @return Language - the language under the visible cursor.
   */
  private Language getLanguage() {
    final XComponent xComponent = getxComponent();
    if (xComponent == null) {
      return Language.ENGLISH; // for testing with local main() method only
    }
    final Locale charLocale;
    final XPropertySet xCursorProps;
    try {
      final XModel model = (XModel) UnoRuntime.queryInterface(XModel.class,
          xComponent);
      final XTextViewCursorSupplier xViewCursorSupplier = (XTextViewCursorSupplier) UnoRuntime
          .queryInterface(XTextViewCursorSupplier.class, model
              .getCurrentController());
      final XTextViewCursor xCursor = xViewCursorSupplier.getViewCursor();
      if (xCursor.isCollapsed()) { // no text selection
        xCursorProps = (XPropertySet) UnoRuntime.queryInterface(
            XPropertySet.class, xCursor);
      } else { // text is selected, need to create another cursor
        // as multiple languages can occur here - we care only
        // about character under the cursor, which might be wrong
        // but it applies only to the checking dialog to be removed
        xCursorProps = (XPropertySet) UnoRuntime.queryInterface(
            XPropertySet.class, xCursor.getText().createTextCursorByRange(
                xCursor.getStart()));
      }
      final Object obj = xCursorProps.getPropertyValue("CharLocale");
      if (obj == null) {
        return Language.ENGLISH; // fallback
      }
      charLocale = (Locale) obj;
      boolean langIsSupported = false;
      for (Language element : Language.LANGUAGES) {
        if (element.getShortName().equals(charLocale.Language)) {
          langIsSupported = true;
          break;
        }
      }
      if (!langIsSupported) {
        // FIXME: i18n
        JOptionPane.showMessageDialog(null,
            "Error: Sorry, the document language '" + charLocale.Language
                + "' is not supported by LanguageTool.");
        return null;
      }
    } catch (final Throwable t) {
      showError(t);
      return null;
    }
    return Language.getLanguageForShortName(charLocale.Language);
  }

  /**
   * 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
   * @param props - properties
   * @return ProofreadingResult containing the results of the check.
   * @throws IllegalArgumentException
   *           (not really, LT simply returns the ProofreadingResult with the
   *           values supplied)
   */
  public final ProofreadingResult doProofreading(final String docID,
      final String paraText, final Locale locale, final int startOfSentencePos,
      final int nSuggestedBehindEndOfSentencePosition,
      final PropertyValue[] props) {
    final ProofreadingResult paRes = new ProofreadingResult();
    try {
      paRes.nStartOfSentencePosition = startOfSentencePos;
      paRes.xProofreader = this;
      paRes.aLocale = locale;
      paRes.aDocumentIdentifier = docID;
      paRes.aText = paraText;
      paRes.aProperties = props;
      return doGrammarCheckingInternal(paraText, locale, paRes);
    } catch (final Throwable t) {
      showError(t);
      return paRes;
    }
  }

  synchronized private ProofreadingResult doGrammarCheckingInternal(
      final String paraText, final Locale locale, final ProofreadingResult paRes) {

    if (!StringTools.isEmpty(paraText)
        && hasLocale(locale)) {
        // caching the instance of LT
        if (!Language.getLanguageForShortName(locale.Language).equals(
            docLanguage)
            || langTool == null || recheck) {
          docLanguage = Language.getLanguageForShortName(locale.Language);
          if (docLanguage == null) {
            return paRes;
          }
          try {
            langTool = new JLanguageTool(docLanguage, config.getMotherTongue());
            langTool.activateDefaultPatternRules();
            langTool.activateDefaultFalseFriendRules();
            recheck = false;
          } catch (final Throwable t) {
            showError(t);
          }
        }

        if (config.getDisabledRuleIds() != null) {
          for (final String id : config.getDisabledRuleIds()) {
            langTool.disableRule(id);
          }
        }
        final Set<String> disabledCategories = config
            .getDisabledCategoryNames();
        if (disabledCategories != null) {
          for (final String categoryName : disabledCategories) {
            langTool.disableCategory(categoryName);
          }
        }
        final Set<String> enabledRules = config.getEnabledRuleIds();
        if (enabledRules != null) {
          for (String ruleName : enabledRules) {
            langTool.enableDefaultOffRule(ruleName);
            langTool.enableRule(ruleName);
          }
        }
        try {
          final String sentence = getSentence(paraText,
              paRes.nStartOfSentencePosition);
          paRes.nStartOfSentencePosition = position;
          paRes.nStartOfNextSentencePosition = position + sentence.length();
          paRes.nBehindEndOfSentencePosition = paRes.nStartOfNextSentencePosition;
          if (!StringTools.isEmpty(sentence)) {
            final List<RuleMatch> ruleMatches = langTool.check(sentence, false,
                JLanguageTool.paragraphHandling.ONLYNONPARA);
            final SingleProofreadingError[] pErrors = checkParaRules(paraText,
                locale, paRes.nStartOfSentencePosition,
                paRes.nStartOfNextSentencePosition, paRes.aDocumentIdentifier);
            int pErrorCount = 0;
            if (pErrors != null) {
              pErrorCount = pErrors.length;
            }
            if (!ruleMatches.isEmpty()) {
              final SingleProofreadingError[] errorArray = new SingleProofreadingError[ruleMatches
                  .size()
                  + pErrorCount];
              int i = 0;
              for (final RuleMatch myRuleMatch : ruleMatches) {
                errorArray[i] = createOOoError(myRuleMatch, paRes.nStartOfSentencePosition);
                i++;
              }
              // add para matches
              if (pErrors != null) {
                for (SingleProofreadingError paraError : pErrors) {
                  if (paraError != null) {
                    errorArray[i] = paraError;
                    i++;
                  }
                }
              }
              Arrays.sort(errorArray, new ErrorPositionComparator());
              paRes.aErrors = errorArray;

            } else {
              if (pErrors != null) {
                paRes.aErrors = pErrors;
              }
            }
          }
        } catch (final Throwable t) {
          showError(t);
          paRes.nBehindEndOfSentencePosition = paraText.length();
       }      
    }
    return paRes;
  }

  synchronized private String getSentence(final String paraText,
      final int startPos) {
    if (paraText.equals(currentPara) && tokenizedSentences != null) {
      int i = 0;
      int index = -1;
      while (index < startPos && i < tokenizedSentences.size()) {
        index += tokenizedSentences.get(i).length();
        if (index < startPos) {
          i++;
        }
      }
      position = index + 1;
      if (i < tokenizedSentences.size()) {
        position -= tokenizedSentences.get(i).length();
        return tokenizedSentences.get(i);
      }
      return "";
    }
    currentPara = paraText;
    tokenizedSentences = langTool.sentenceTokenize(paraText);
    position = 0;
    if (!tokenizedSentences.isEmpty()) {
      return tokenizedSentences.get(0);
    }
    return "";
  }

  synchronized private SingleProofreadingError[] checkParaRules(
      final String paraText, final Locale locale, final int startPos,
      final int endPos, final String docID) {
    if (startPos == 0) {
      try {
        paragraphMatches = langTool.check(paraText, false,
            JLanguageTool.paragraphHandling.ONLYPARA);
        this.docID = docID;
      } catch (final Throwable t) {
        showError(t);
      }
    }
    if (paragraphMatches != null && !paragraphMatches.isEmpty()
        && docID.equals(this.docID)) {
      final List<SingleProofreadingError> errorList = new ArrayList<SingleProofreadingError>(
          paragraphMatches.size());
      for (final RuleMatch myRuleMatch : paragraphMatches) {
        final int startErrPos = myRuleMatch.getFromPos();
        final int endErrPos = myRuleMatch.getToPos();
        if (startErrPos >= startPos && startErrPos < endPos
            && endErrPos >= startPos && endErrPos < endPos) {
          errorList.add(createOOoError(myRuleMatch, 0));
        }
      }
      if (!errorList.isEmpty()) {
        final SingleProofreadingError[] errorArray = errorList.toArray(new SingleProofreadingError[errorList.size()]);
        Arrays.sort(errorArray, new ErrorPositionComparator());
        return errorArray;
      }
    }
    return null;
  }

  /**
   * Creates a SingleGrammarError object for use in OOo.
   * @param myMatch
   *          ruleMatch - LT rule match
   * 
   * @return SingleGrammarError - object for OOo checker integration
   */
  private SingleProofreadingError createOOoError(final RuleMatch myMatch,
      final int startIndex) {
    final SingleProofreadingError aError = new SingleProofreadingError();
    aError.nErrorType = com.sun.star.text.TextMarkupType.PROOFREADING;
    // the API currently has no support for formatting text in comments
    final String comment = myMatch.getMessage()
        .replaceAll("<suggestion>", "\"").replaceAll("</suggestion>", "\"")
        .replaceAll("([\r]*\n)", " "); // convert line ends to spaces
    aError.aFullComment = comment;
    // not all rules have short comments
    if (!StringTools.isEmpty(myMatch.getShortMessage())) {
      aError.aShortComment = myMatch.getShortMessage();
    } else {
      aError.aShortComment = aError.aFullComment;
    }
    aError.aSuggestions = myMatch.getSuggestedReplacements().toArray(
        new String[myMatch.getSuggestedReplacements().size()]);
    aError.nErrorStart = myMatch.getFromPos() + startIndex;
    aError.nErrorLength = myMatch.getToPos() - myMatch.getFromPos();
    aError.aRuleIdentifier = myMatch.getRule().getId();
    aError.aProperties = new PropertyValue[0];
    return aError;
  }

  /**
   * LT does not support spell-checking, so we return false.
   * 
   * @return false
   */
  public final boolean isSpellChecker() {
    return false;
  }

  /**
   * Runs LT options dialog box.
   **/
  public final void runOptionsDialog() {
    final Language lang = getLanguage();
    if (lang == null) {
      return;
    }
    final ConfigThread configThread = new ConfigThread(lang, config, this);
    configThread.start();
  }

  /**
   * @return An array of Locales supported by LT.
   */
  public final Locale[] getLocales() {
    try {
      int dims = 0;
      for (final Language element : Language.LANGUAGES) {
        dims += element.getCountryVariants().length;
      }
      final Locale[] aLocales = new Locale[dims];
      int cnt = 0;
      for (final Language element : Language.LANGUAGES) {
        for (final String variant : element.getCountryVariants()) {
          aLocales[cnt] = new Locale(element.getShortName(), variant, "");
          cnt++;
        }
      }
      return aLocales;
    } catch (final Throwable t) {
      showError(t);
      return new Locale[0];
    }
  }

  /**
   * @return true if LT supports the language of a given locale.
   * @param locale
   *          The Locale to check.
   */
  public final boolean hasLocale(final Locale locale) {
    try {
      for (final Language element : Language.LANGUAGES) {
        if (element.getShortName().equals(locale.Language)) {
          return true;
        }
      }
    } catch (final Throwable t) {
      showError(t);    
    }
    return false;
  }

  /**
   * Add a listener that allow re-checking the document after changing the
   * options in the configuration dialog box.
   * 
   * @param xLinEvLis
   *          - the listener to be added
   * @return true if listener is non-null and has been added, false otherwise.
   */
  public final boolean addLinguServiceEventListener(
      final XLinguServiceEventListener xLinEvLis) {
    if (xLinEvLis == null) {
      return false;
    }
    xEventListeners.add(xLinEvLis);
    return true;
  }

  /**
   * Remove a listener from the event listeners list.
   * 
   * @param xLinEvLis
   *          - the listener to be removed
   * @return true if listener is non-null and has been removed, false otherwise.
   */
  public final boolean removeLinguServiceEventListener(
      final XLinguServiceEventListener xLinEvLis) {
    if (xLinEvLis == null) {
      return false;
    }
    if (xEventListeners.contains(xLinEvLis)) {
      xEventListeners.remove(xLinEvLis);
      return true;
    }
    return false;
  }

  /**
   * Inform listener (grammar checking iterator) that options have changed and
   * the doc should be rechecked.
   * 
   */
  public final void resetDocument() {
    if (!xEventListeners.isEmpty()) {
      for (final XLinguServiceEventListener xEvLis : xEventListeners) {
        if (xEvLis != null) {
          final com.sun.star.linguistic2.LinguServiceEvent xEvent = new com.sun.star.linguistic2.LinguServiceEvent();
          xEvent.nEvent = com.sun.star.linguistic2.LinguServiceEventFlags.PROOFREAD_AGAIN;
          xEvLis.processLinguServiceEvent(xEvent);
        }
      }
      recheck = true;
      disabledRules = config.getDisabledRuleIds();
      if (disabledRules == null) {
        disabledRules = new HashSet<String>();
      }
    }
  }

  public String[] getSupportedServiceNames() {
    return getServiceNames();
  }

  public static String[] getServiceNames() {
    return SERVICE_NAMES;
  }

  public boolean supportsService(final String sServiceName) {
    for (final String sName : SERVICE_NAMES) {
      if (sServiceName.equals(sName)) {
        return true;
      }
    }
    return false;
  }

  public String getImplementationName() {
    return Main.class.getName();
  }

  public static XSingleComponentFactory __getComponentFactory(
      final String sImplName) {
    SingletonFactory xFactory = null;
    if (sImplName.equals(Main.class.getName())) {
      xFactory = new SingletonFactory();
    }
    return xFactory;
  }

  public static boolean __writeRegistryServiceInfo(final XRegistryKey regKey) {
    return Factory.writeRegistryServiceInfo(Main.class.getName(), Main
        .getServiceNames(), regKey);
  }

  public void trigger(final String sEvent) {
    if (!javaVersionOkay()) {
      return;
    }
    try {
      if ("configure".equals(sEvent)) {
        runOptionsDialog();
      } else if ("about".equals(sEvent)) {
        final AboutDialogThread aboutThread = new AboutDialogThread(MESSAGES);
        aboutThread.start();
      } else {
        System.err.println("Sorry, don't know what to do, sEvent = " + sEvent);
      }
    } catch (final Throwable e) {
      showError(e);
    }
  }

  private boolean javaVersionOkay() {
    final 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"))) {
      final DialogThread dt = new DialogThread(
          "Error: LanguageTool requires Java 1.5 or later. Current version: "
              + version);
      dt.start();
      return false;
    }
    try {
      for (UIManager.LookAndFeelInfo info : UIManager
          .getInstalledLookAndFeels()) {
        if ("Nimbus".equals(info.getName())) {
          UIManager.setLookAndFeel(info.getClassName());
          break;
        }
      }
    } catch (Exception ex) {
      // Well, what can we do...
    }
    
    return true;
  }

  static void showError(final Throwable e) {
    final String metaInfo = "OS: " + System.getProperty("os.name")
      + " on " + System.getProperty("os.arch") + ", Java version "
      + System.getProperty("java.vm.version")
      + " from " + System.getProperty("java.vm.vendor");
    String msg = "An error has occurred in LanguageTool " + JLanguageTool.VERSION + ":\n" + e.toString()
        + "\nStacktrace:\n";
    final StackTraceElement[] elem = e.getStackTrace();
    for (final StackTraceElement element : elem) {
      msg += element.toString() + "\n";
    }
    msg += metaInfo;
    final DialogThread dt = new DialogThread(msg);
    dt.start();
    // e.printStackTrace();
    // OOo crashes when we throw an Exception :-(
    // throw new RuntimeException(e);
  }

  private File getHomeDir() {
    final String homeDir = System.getProperty("user.home");
    if (homeDir == null) {
      @SuppressWarnings({"ThrowableInstanceNeverThrown"})
      final RuntimeException ex = new RuntimeException("Could not get home directory");
      showError(ex);
    }
    return new File(homeDir);
  }

  private class AboutDialogThread extends Thread {

    private final ResourceBundle messages;

    AboutDialogThread(final ResourceBundle messages) {
      this.messages = messages;
    }

    @Override
    public void run() {
      final XModel model = (XModel) UnoRuntime.queryInterface(XModel.class,
          getxComponent());
      final XWindow parentWindow = model.getCurrentController().getFrame()
          .getContainerWindow();
      final XWindowPeer parentWindowPeer = (XWindowPeer) UnoRuntime
          .queryInterface(XWindowPeer.class, parentWindow);
      final OOoAboutDialog about = new OOoAboutDialog(messages,
          parentWindowPeer);
      about.show();
    }
  }

  public void ignoreRule(final String ruleId, final Locale locale)
      throws IllegalArgumentException {
    // TODO: config should be locale-dependent
    disabledRulesUI.add(ruleId);
    config.setDisabledRuleIds(disabledRulesUI);
    try {
      config.saveConfiguration();
    } catch (final Throwable t) {
      showError(t);
    }
    recheck = true;
  }

  /**
   * 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.
   */
  public void resetIgnoreRules() {
    config.setDisabledRuleIds(disabledRules);
    try {
      config.saveConfiguration();
    } catch (final Throwable t) {
      showError(t);
    }
    recheck = true;
  }
  
  public String getServiceDisplayName(Locale locale) {
    return "LanguageTool";
  }

}

/**
 * A simple comparator for sorting errors by their position.
 * 
 */
class ErrorPositionComparator implements Comparator<SingleProofreadingError> {

  public int compare(final SingleProofreadingError match1,
      final SingleProofreadingError match2) {
    if (match1.aSuggestions.length == 0 
        && match2.aSuggestions.length > 0) {
      return 1;
    }
    if (match2.aSuggestions.length == 0 
        && match1.aSuggestions.length > 0) {
      return -1;
    }    
    final int error1pos = match1.nErrorStart;
    final int error2pos = match2.nErrorStart;
    if (error1pos > error2pos)
      return 1;
    else if (error1pos < error2pos)
      return -1;
    else
      if (match1.aSuggestions.length != 0
          && match2.aSuggestions.length != 0
          && match1.aSuggestions.length 
          != match2.aSuggestions.length) {
      return ((Integer) (match1.aSuggestions.length))
          .compareTo(match2.aSuggestions.length);
      }
    return match1.aRuleIdentifier.compareTo(match2.aRuleIdentifier);
  }
}

class DialogThread extends Thread {
  final private String text;

  DialogThread(final String text) {
    this.text = text;
  }

  @Override
  public void run() {
    JOptionPane.showMessageDialog(null, text);
  }
}