~slub.team/goobi-indexserver/3.x

« back to all changes in this revision

Viewing changes to lucene/contrib/analyzers/common/src/java/org/apache/lucene/analysis/hunspell/HunspellDictionary.java

  • Committer: Sebastian Meyer
  • Date: 2012-08-03 09:12:40 UTC
  • Revision ID: sebastian.meyer@slub-dresden.de-20120803091240-x6861b0vabq1xror
Remove Lucene and Solr source code and add patches instead
Fix Bug #985487: Auto-suggestion for the search interface

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
package org.apache.lucene.analysis.hunspell;
2
 
 
3
 
/**
4
 
 * Licensed to the Apache Software Foundation (ASF) under one or more
5
 
 * contributor license agreements.  See the NOTICE file distributed with
6
 
 * this work for additional information regarding copyright ownership.
7
 
 * The ASF licenses this file to You under the Apache License, Version 2.0
8
 
 * (the "License"); you may not use this file except in compliance with
9
 
 * the License.  You may obtain a copy of the License at
10
 
 *
11
 
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 
 *
13
 
 * Unless required by applicable law or agreed to in writing, software
14
 
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 
 * See the License for the specific language governing permissions and
17
 
 * limitations under the License.
18
 
 */
19
 
 
20
 
import org.apache.lucene.analysis.CharArrayMap;
21
 
import org.apache.lucene.util.Version;
22
 
 
23
 
import java.io.*;
24
 
import java.nio.charset.Charset;
25
 
import java.nio.charset.CharsetDecoder;
26
 
import java.text.ParseException;
27
 
import java.util.ArrayList;
28
 
import java.util.Arrays;
29
 
import java.util.List;
30
 
import java.util.Locale;
31
 
 
32
 
public class HunspellDictionary {
33
 
 
34
 
  static final HunspellWord NOFLAGS = new HunspellWord();
35
 
  
36
 
  private static final String PREFIX_KEY = "PFX";
37
 
  private static final String SUFFIX_KEY = "SFX";
38
 
  private static final String FLAG_KEY = "FLAG";
39
 
 
40
 
  private static final String NUM_FLAG_TYPE = "num";
41
 
  private static final String UTF8_FLAG_TYPE = "UTF-8";
42
 
  private static final String LONG_FLAG_TYPE = "long";
43
 
  
44
 
  private static final String PREFIX_CONDITION_REGEX_PATTERN = "%s.*";
45
 
  private static final String SUFFIX_CONDITION_REGEX_PATTERN = ".*%s";
46
 
 
47
 
  private static final boolean IGNORE_CASE_DEFAULT = false;
48
 
 
49
 
  private CharArrayMap<List<HunspellWord>> words;
50
 
  private CharArrayMap<List<HunspellAffix>> prefixes;
51
 
  private CharArrayMap<List<HunspellAffix>> suffixes;
52
 
 
53
 
  private FlagParsingStrategy flagParsingStrategy = new SimpleFlagParsingStrategy(); // Default flag parsing strategy
54
 
  private boolean ignoreCase = IGNORE_CASE_DEFAULT;
55
 
 
56
 
  private final Version version;
57
 
 
58
 
  /**
59
 
   * Creates a new HunspellDictionary containing the information read from the provided InputStreams to hunspell affix
60
 
   * and dictionary files
61
 
   *
62
 
   * @param affix InputStream for reading the hunspell affix file
63
 
   * @param dictionary InputStream for reading the hunspell dictionary file
64
 
   * @param version Lucene Version
65
 
   * @throws IOException Can be thrown while reading from the InputStreams
66
 
   * @throws ParseException Can be thrown if the content of the files does not meet expected formats
67
 
   */
68
 
  public HunspellDictionary(InputStream affix, InputStream dictionary, Version version) throws IOException, ParseException {
69
 
    this(affix, Arrays.asList(dictionary), version, IGNORE_CASE_DEFAULT);
70
 
  }
71
 
 
72
 
  /**
73
 
   * Creates a new HunspellDictionary containing the information read from the provided InputStreams to hunspell affix
74
 
   * and dictionary files
75
 
   *
76
 
   * @param affix InputStream for reading the hunspell affix file
77
 
   * @param dictionary InputStream for reading the hunspell dictionary file
78
 
   * @param version Lucene Version
79
 
   * @param ignoreCase If true, dictionary matching will be case insensitive
80
 
   * @throws IOException Can be thrown while reading from the InputStreams
81
 
   * @throws ParseException Can be thrown if the content of the files does not meet expected formats
82
 
   */
83
 
  public HunspellDictionary(InputStream affix, InputStream dictionary, Version version, boolean ignoreCase) throws IOException, ParseException {
84
 
    this(affix, Arrays.asList(dictionary), version, ignoreCase);
85
 
  }
86
 
 
87
 
  /**
88
 
   * Creates a new HunspellDictionary containing the information read from the provided InputStreams to hunspell affix
89
 
   * and dictionary files
90
 
   *
91
 
   * @param affix InputStream for reading the hunspell affix file
92
 
   * @param dictionaries InputStreams for reading the hunspell dictionary file
93
 
   * @param version Lucene Version
94
 
   * @param ignoreCase If true, dictionary matching will be case insensitive
95
 
   * @throws IOException Can be thrown while reading from the InputStreams
96
 
   * @throws ParseException Can be thrown if the content of the files does not meet expected formats
97
 
   */
98
 
  public HunspellDictionary(InputStream affix, List<InputStream> dictionaries, Version version, boolean ignoreCase) throws IOException, ParseException {
99
 
    this.version = version;
100
 
    this.ignoreCase = ignoreCase;
101
 
    String encoding = getDictionaryEncoding(affix);
102
 
    CharsetDecoder decoder = getJavaEncoding(encoding);
103
 
    readAffixFile(affix, decoder);
104
 
    words = new CharArrayMap<List<HunspellWord>>(version, 65535 /* guess */, this.ignoreCase);
105
 
    for (InputStream dictionary : dictionaries) {
106
 
      readDictionaryFile(dictionary, decoder);
107
 
    }
108
 
  }
109
 
 
110
 
  /**
111
 
   * Looks up HunspellWords that match the String created from the given char array, offset and length
112
 
   *
113
 
   * @param word Char array to generate the String from
114
 
   * @param offset Offset in the char array that the String starts at
115
 
   * @param length Length from the offset that the String is
116
 
   * @return List of HunspellWords that match the generated String, or {@code null} if none are found
117
 
   */
118
 
  public List<HunspellWord> lookupWord(char word[], int offset, int length) {
119
 
    return words.get(word, offset, length);
120
 
  }
121
 
 
122
 
  /**
123
 
   * Looks up HunspellAffix prefixes that have an append that matches the String created from the given char array, offset and length
124
 
   *
125
 
   * @param word Char array to generate the String from
126
 
   * @param offset Offset in the char array that the String starts at
127
 
   * @param length Length from the offset that the String is
128
 
   * @return List of HunspellAffix prefixes with an append that matches the String, or {@code null} if none are found
129
 
   */
130
 
  public List<HunspellAffix> lookupPrefix(char word[], int offset, int length) {
131
 
    return prefixes.get(word, offset, length);
132
 
  }
133
 
 
134
 
  /**
135
 
   * Looks up HunspellAffix suffixes that have an append that matches the String created from the given char array, offset and length
136
 
   *
137
 
   * @param word Char array to generate the String from
138
 
   * @param offset Offset in the char array that the String starts at
139
 
   * @param length Length from the offset that the String is
140
 
   * @return List of HunspellAffix suffixes with an append that matches the String, or {@code null} if none are found
141
 
   */
142
 
  public List<HunspellAffix> lookupSuffix(char word[], int offset, int length) {
143
 
    return suffixes.get(word, offset, length);
144
 
  }
145
 
 
146
 
  /**
147
 
   * Reads the affix file through the provided InputStream, building up the prefix and suffix maps
148
 
   *
149
 
   * @param affixStream InputStream to read the content of the affix file from
150
 
   * @param decoder CharsetDecoder to decode the content of the file
151
 
   * @throws IOException Can be thrown while reading from the InputStream
152
 
   */
153
 
  private void readAffixFile(InputStream affixStream, CharsetDecoder decoder) throws IOException {
154
 
    prefixes = new CharArrayMap<List<HunspellAffix>>(version, 8, ignoreCase);
155
 
    suffixes = new CharArrayMap<List<HunspellAffix>>(version, 8, ignoreCase);
156
 
    
157
 
    BufferedReader reader = new BufferedReader(new InputStreamReader(affixStream, decoder));
158
 
    String line = null;
159
 
    while ((line = reader.readLine()) != null) {
160
 
      if (line.startsWith(PREFIX_KEY)) {
161
 
        parseAffix(prefixes, line, reader, PREFIX_CONDITION_REGEX_PATTERN);
162
 
      } else if (line.startsWith(SUFFIX_KEY)) {
163
 
        parseAffix(suffixes, line, reader, SUFFIX_CONDITION_REGEX_PATTERN);
164
 
      } else if (line.startsWith(FLAG_KEY)) {
165
 
        // Assume that the FLAG line comes before any prefix or suffixes
166
 
        // Store the strategy so it can be used when parsing the dic file
167
 
        flagParsingStrategy = getFlagParsingStrategy(line);
168
 
      }
169
 
    }
170
 
    reader.close();
171
 
  }
172
 
 
173
 
  /**
174
 
   * Parses a specific affix rule putting the result into the provided affix map
175
 
   * 
176
 
   * @param affixes Map where the result of the parsing will be put
177
 
   * @param header Header line of the affix rule
178
 
   * @param reader BufferedReader to read the content of the rule from
179
 
   * @param conditionPattern {@link String#format(String, Object...)} pattern to be used to generate the condition regex
180
 
   *                         pattern
181
 
   * @throws IOException Can be thrown while reading the rule
182
 
   */
183
 
  private void parseAffix(CharArrayMap<List<HunspellAffix>> affixes,
184
 
                          String header,
185
 
                          BufferedReader reader,
186
 
                          String conditionPattern) throws IOException {
187
 
    String args[] = header.split("\\s+");
188
 
 
189
 
    boolean crossProduct = args[2].equals("Y");
190
 
    
191
 
    int numLines = Integer.parseInt(args[3]);
192
 
    for (int i = 0; i < numLines; i++) {
193
 
      String line = reader.readLine();
194
 
      String ruleArgs[] = line.split("\\s+");
195
 
 
196
 
      HunspellAffix affix = new HunspellAffix();
197
 
      
198
 
      affix.setFlag(flagParsingStrategy.parseFlag(ruleArgs[1]));
199
 
      affix.setStrip(ruleArgs[2].equals("0") ? "" : ruleArgs[2]);
200
 
 
201
 
      String affixArg = ruleArgs[3];
202
 
      
203
 
      int flagSep = affixArg.lastIndexOf('/');
204
 
      if (flagSep != -1) {
205
 
        char appendFlags[] = flagParsingStrategy.parseFlags(affixArg.substring(flagSep + 1));
206
 
        Arrays.sort(appendFlags);
207
 
        affix.setAppendFlags(appendFlags);
208
 
        affix.setAppend(affixArg.substring(0, flagSep));
209
 
      } else {
210
 
        affix.setAppend(affixArg);
211
 
      }
212
 
 
213
 
      String condition = ruleArgs[4];
214
 
      affix.setCondition(condition, String.format(conditionPattern, condition));
215
 
      affix.setCrossProduct(crossProduct);
216
 
      
217
 
      List<HunspellAffix> list = affixes.get(affix.getAppend());
218
 
      if (list == null) {
219
 
        list = new ArrayList<HunspellAffix>();
220
 
        affixes.put(affix.getAppend(), list);
221
 
      }
222
 
      
223
 
      list.add(affix);
224
 
    }
225
 
  }
226
 
 
227
 
  /**
228
 
   * Parses the encoding specificed in the affix file readable through the provided InputStream
229
 
   *
230
 
   * @param affix InputStream for reading the affix file
231
 
   * @return Encoding specified in the affix file
232
 
   * @throws IOException Can be thrown while reading from the InputStream
233
 
   * @throws ParseException Thrown if the first non-empty non-comment line read from the file does not adhere to the format {@code SET <encoding>}
234
 
   */
235
 
  private String getDictionaryEncoding(InputStream affix) throws IOException, ParseException {
236
 
    final StringBuilder encoding = new StringBuilder();
237
 
    for (;;) {
238
 
      encoding.setLength(0);
239
 
      int ch;
240
 
      while ((ch = affix.read()) >= 0) {
241
 
        if (ch == '\n') {
242
 
          break;
243
 
        }
244
 
        if (ch != '\r') {
245
 
          encoding.append((char)ch);
246
 
        }
247
 
      }
248
 
      if (
249
 
          encoding.length() == 0 || encoding.charAt(0) == '#' ||
250
 
          // this test only at the end as ineffective but would allow lines only containing spaces:
251
 
          encoding.toString().trim().length() == 0
252
 
      ) {
253
 
        if (ch < 0) {
254
 
          throw new ParseException("Unexpected end of affix file.", 0);
255
 
        }
256
 
        continue;
257
 
      }
258
 
      if ("SET ".equals(encoding.substring(0, 4))) {
259
 
        // cleanup the encoding string, too (whitespace)
260
 
        return encoding.substring(4).trim();
261
 
      }
262
 
      throw new ParseException("The first non-comment line in the affix file must "+
263
 
          "be a 'SET charset', was: '" + encoding +"'", 0);
264
 
    }
265
 
  }
266
 
 
267
 
  /**
268
 
   * Retrieves the CharsetDecoder for the given encoding.  Note, This isn't perfect as I think ISCII-DEVANAGARI and
269
 
   * MICROSOFT-CP1251 etc are allowed...
270
 
   *
271
 
   * @param encoding Encoding to retrieve the CharsetDecoder for
272
 
   * @return CharSetDecoder for the given encoding
273
 
   */
274
 
  private CharsetDecoder getJavaEncoding(String encoding) {
275
 
    Charset charset = Charset.forName(encoding);
276
 
    return charset.newDecoder();
277
 
  }
278
 
 
279
 
  /**
280
 
   * Determines the appropriate {@link FlagParsingStrategy} based on the FLAG definiton line taken from the affix file
281
 
   *
282
 
   * @param flagLine Line containing the flag information
283
 
   * @return FlagParsingStrategy that handles parsing flags in the way specified in the FLAG definiton
284
 
   */
285
 
  private FlagParsingStrategy getFlagParsingStrategy(String flagLine) {
286
 
    String flagType = flagLine.substring(5);
287
 
 
288
 
    if (NUM_FLAG_TYPE.equals(flagType)) {
289
 
      return new NumFlagParsingStrategy();
290
 
    } else if (UTF8_FLAG_TYPE.equals(flagType)) {
291
 
      return new SimpleFlagParsingStrategy();
292
 
    } else if (LONG_FLAG_TYPE.equals(flagType)) {
293
 
      return new DoubleASCIIFlagParsingStrategy();
294
 
    }
295
 
 
296
 
    throw new IllegalArgumentException("Unknown flag type: " + flagType);
297
 
  }
298
 
 
299
 
  /**
300
 
   * Reads the dictionary file through the provided InputStream, building up the words map
301
 
   *
302
 
   * @param dictionary InputStream to read the dictionary file through
303
 
   * @param decoder CharsetDecoder used to decode the contents of the file
304
 
   * @throws IOException Can be thrown while reading from the file
305
 
   */
306
 
  private void readDictionaryFile(InputStream dictionary, CharsetDecoder decoder) throws IOException {
307
 
    BufferedReader reader = new BufferedReader(new InputStreamReader(dictionary, decoder));
308
 
    // TODO: don't create millions of strings.
309
 
    String line = reader.readLine(); // first line is number of entries
310
 
    int numEntries = Integer.parseInt(line);
311
 
    
312
 
    // TODO: the flags themselves can be double-chars (long) or also numeric
313
 
    // either way the trick is to encode them as char... but they must be parsed differently
314
 
    while ((line = reader.readLine()) != null) {
315
 
      String entry;
316
 
      HunspellWord wordForm;
317
 
      
318
 
      int flagSep = line.lastIndexOf('/');
319
 
      if (flagSep == -1) {
320
 
        wordForm = NOFLAGS;
321
 
        entry = line;
322
 
      } else {
323
 
        // note, there can be comments (morph description) after a flag.
324
 
        // we should really look for any whitespace
325
 
        int end = line.indexOf('\t', flagSep);
326
 
        if (end == -1)
327
 
          end = line.length();
328
 
        
329
 
        
330
 
        wordForm = new HunspellWord(flagParsingStrategy.parseFlags(line.substring(flagSep + 1, end)));
331
 
        Arrays.sort(wordForm.getFlags());
332
 
        entry = line.substring(0, flagSep);
333
 
        if(ignoreCase) {
334
 
          entry = entry.toLowerCase(Locale.ENGLISH);
335
 
        }
336
 
      }
337
 
      
338
 
      List<HunspellWord> entries = words.get(entry);
339
 
      if (entries == null) {
340
 
        entries = new ArrayList<HunspellWord>();
341
 
        words.put(entry, entries);
342
 
      }
343
 
      entries.add(wordForm);
344
 
    }
345
 
  }
346
 
 
347
 
  public Version getVersion() {
348
 
    return version;
349
 
  }
350
 
 
351
 
  /**
352
 
   * Abstraction of the process of parsing flags taken from the affix and dic files
353
 
   */
354
 
  private static abstract class FlagParsingStrategy {
355
 
 
356
 
    /**
357
 
     * Parses the given String into a single flag
358
 
     *
359
 
     * @param rawFlag String to parse into a flag
360
 
     * @return Parsed flag
361
 
     */
362
 
    char parseFlag(String rawFlag) {
363
 
      return parseFlags(rawFlag)[0];
364
 
    }
365
 
 
366
 
    /**
367
 
     * Parses the given String into multiple flags
368
 
     *
369
 
     * @param rawFlags String to parse into flags
370
 
     * @return Parsed flags
371
 
     */
372
 
    abstract char[] parseFlags(String rawFlags);
373
 
  }
374
 
 
375
 
  /**
376
 
   * Simple implementation of {@link FlagParsingStrategy} that treats the chars in each String as a individual flags.
377
 
   * Can be used with both the ASCII and UTF-8 flag types.
378
 
   */
379
 
  private static class SimpleFlagParsingStrategy extends FlagParsingStrategy {
380
 
    /**
381
 
     * {@inheritDoc}
382
 
     */
383
 
    public char[] parseFlags(String rawFlags) {
384
 
      return rawFlags.toCharArray();
385
 
    }
386
 
  }
387
 
 
388
 
  /**
389
 
   * Implementation of {@link FlagParsingStrategy} that assumes each flag is encoded in its numerical form.  In the case
390
 
   * of multiple flags, each number is separated by a comma.
391
 
   */
392
 
  private static class NumFlagParsingStrategy extends FlagParsingStrategy {
393
 
    /**
394
 
     * {@inheritDoc}
395
 
     */
396
 
    public char[] parseFlags(String rawFlags) {
397
 
      String[] rawFlagParts = rawFlags.trim().split(",");
398
 
      char[] flags = new char[rawFlagParts.length];
399
 
 
400
 
      for (int i = 0; i < rawFlagParts.length; i++) {
401
 
        // note, removing the trailing X/leading I for nepali... what is the rule here?! 
402
 
        flags[i] = (char) Integer.parseInt(rawFlagParts[i].replaceAll("[^0-9]", ""));
403
 
      }
404
 
 
405
 
      return flags;
406
 
    }
407
 
  }
408
 
 
409
 
  /**
410
 
   * Implementation of {@link FlagParsingStrategy} that assumes each flag is encoded as two ASCII characters whose codes
411
 
   * must be combined into a single character.
412
 
   *
413
 
   * TODO (rmuir) test
414
 
   */
415
 
  private static class DoubleASCIIFlagParsingStrategy extends FlagParsingStrategy {
416
 
 
417
 
    /**
418
 
     * {@inheritDoc}
419
 
     */
420
 
    public char[] parseFlags(String rawFlags) {
421
 
      if (rawFlags.length() == 0) {
422
 
        return new char[0];
423
 
      }
424
 
 
425
 
      StringBuilder builder = new StringBuilder();
426
 
      for (int i = 0; i < rawFlags.length(); i+=2) {
427
 
        char cookedFlag = (char) ((int) rawFlags.charAt(i) + (int) rawFlags.charAt(i + 1));
428
 
        builder.append(cookedFlag);
429
 
      }
430
 
      
431
 
      char flags[] = new char[builder.length()];
432
 
      builder.getChars(0, builder.length(), flags, 0);
433
 
      return flags;
434
 
    }
435
 
  }
436
 
 
437
 
  public boolean isIgnoreCase() {
438
 
    return ignoreCase;
439
 
  }
440
 
}