package com.psddev.cms.hunspell;
import com.atlascopco.hunspell.Hunspell;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import com.psddev.cms.nlp.SpellChecker;
import javax.annotation.ParametersAreNonnullByDefault;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
/**
* Spell checker implementation using
* <a href="http://hunspell.sourceforge.net/">Hunspell</a>.
*
* <p>The dictionary files should be in the classpath with their names
* starting with {@code HunspellDictionary} and ending with
* {@link #AFFIX_FILE_SUFFIX} or {@link #DICTIONARY_FILE_SUFFIX}.</p>
*
* <p>For example, if the locale is {@code ko-KR}, the affix file should be
* named {@code HunspellDictionary_ko_KR.aff}, and the dictionary file should
* be named {@code HunspellDictionary_ko_KR.dic}.</p>
*/
public class HunspellSpellChecker implements SpellChecker {
/**
* Affix file suffix/extension.
*
* @see <a href="http://sourceforge.net/projects/hunspell/files/Hunspell/Documentation/">Hunspell Manual</a>
*/
public static final String AFFIX_FILE_SUFFIX = ".aff";
/**
* Dictionary file suffix/extension.
*
* @see <a href="http://sourceforge.net/projects/hunspell/files/Hunspell/Documentation/">Hunspell Manual</a>
*/
public static final String DICTIONARY_FILE_SUFFIX = ".dic";
private final LoadingCache<String, Optional<Hunspell>> hunspells = CacheBuilder
.newBuilder()
.removalListener(new RemovalListener<String, Optional<Hunspell>>() {
@Override
@ParametersAreNonnullByDefault
public void onRemoval(RemovalNotification<String, Optional<Hunspell>> removalNotification) {
Optional<Hunspell> hunspellOptional = removalNotification.getValue();
if (hunspellOptional != null) {
hunspellOptional.ifPresent(Hunspell::close);
}
}
})
.build(new CacheLoader<String, Optional<Hunspell>>() {
@Override
@ParametersAreNonnullByDefault
public Optional<Hunspell> load(String name) throws IOException {
try (InputStream affixInput = getClass().getResourceAsStream("/" + name + AFFIX_FILE_SUFFIX)) {
if (affixInput != null) {
try (InputStream dictionaryInput = getClass().getResourceAsStream("/" + name + DICTIONARY_FILE_SUFFIX)) {
if (dictionaryInput != null) {
String tmpdir = System.getProperty("java.io.tmpdir");
Path affixPath = Paths.get(tmpdir, name + AFFIX_FILE_SUFFIX);
Path dictionaryPath = Paths.get(tmpdir, name + DICTIONARY_FILE_SUFFIX);
Files.copy(affixInput, affixPath, StandardCopyOption.REPLACE_EXISTING);
Files.copy(dictionaryInput, dictionaryPath, StandardCopyOption.REPLACE_EXISTING);
return Optional.of(new Hunspell(dictionaryPath.toString(), affixPath.toString()));
}
}
}
}
return Optional.empty();
}
});
private Hunspell findHunspell(Locale locale) {
return SpellChecker.createDictionaryNames("HunspellDictionary", locale)
.stream()
.map(l -> hunspells.getUnchecked(l).orElse(null))
.filter(h -> h != null)
.findFirst()
.orElse(null);
}
@Override
public boolean isSupported(Locale locale) {
Preconditions.checkNotNull(locale);
return findHunspell(locale) != null;
}
@Override
public boolean isPreferred(Locale locale) {
Preconditions.checkNotNull(locale);
return false;
}
@Override
public List<String> suggest(Locale locale, String word) {
Preconditions.checkNotNull(locale);
Preconditions.checkNotNull(word);
Hunspell hunspell = findHunspell(locale);
if (hunspell == null) {
throw new UnsupportedOperationException();
} else if (hunspell.spell(word)) {
return null;
} else {
return hunspell.suggest(word);
}
}
}