aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java89
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java101
-rw-r--r--native/src/correction.cpp82
-rw-r--r--native/src/correction.h36
-rw-r--r--native/src/correction_state.h51
-rw-r--r--native/src/defines.h1
-rw-r--r--native/src/proximity_info.h1
-rw-r--r--native/src/unigram_dictionary.cpp10
-rw-r--r--tools/makedict2/Android.mk27
-rw-r--r--tools/makedict2/etc/Android.mk21
-rwxr-xr-xtools/makedict2/etc/makedict263
-rw-r--r--tools/makedict2/etc/manifest.txt1
-rw-r--r--tools/makedict2/src/com/android/inputmethod/latin/BinaryDictInputOutput.java1024
-rw-r--r--tools/makedict2/src/com/android/inputmethod/latin/CharGroupInfo.java45
-rw-r--r--tools/makedict2/src/com/android/inputmethod/latin/DictionaryMaker.java256
-rw-r--r--tools/makedict2/src/com/android/inputmethod/latin/FusionDictionary.java602
-rw-r--r--tools/makedict2/src/com/android/inputmethod/latin/MakedictLog.java40
-rw-r--r--tools/makedict2/src/com/android/inputmethod/latin/PendingAttribute.java32
-rw-r--r--tools/makedict2/src/com/android/inputmethod/latin/UnsupportedFormatException.java26
-rw-r--r--tools/makedict2/src/com/android/inputmethod/latin/Word.java65
-rw-r--r--tools/makedict2/src/com/android/inputmethod/latin/XmlDictInputOutput.java215
-rw-r--r--tools/makedict2/tests/com/android/inputmethod/latin/BinaryDictInputOutputTest.java55
22 files changed, 2693 insertions, 150 deletions
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index 2d50a6f46..3da670e2e 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -25,7 +25,6 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
-import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@@ -55,66 +54,6 @@ public class BinaryDictionaryFileDumper {
}
/**
- * Escapes a string for any characters that may be suspicious for a file or directory name.
- *
- * Concretely this does a sort of URL-encoding except it will encode everything that's not
- * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
- * we cannot allow here)
- */
- // TODO: create a unit test for this method
- private static String replaceFileNameDangerousCharacters(String name) {
- // This assumes '%' is fully available as a non-separator, normal
- // character in a file name. This is probably true for all file systems.
- final StringBuilder sb = new StringBuilder();
- for (int i = 0; i < name.length(); ++i) {
- final int codePoint = name.codePointAt(i);
- if (Character.isLetterOrDigit(codePoint) || '_' == codePoint) {
- sb.appendCodePoint(codePoint);
- } else {
- sb.append('%');
- sb.append(Integer.toHexString(codePoint));
- }
- }
- return sb.toString();
- }
-
- /**
- * Find out the cache directory associated with a specific locale.
- */
- private static String getCacheDirectoryForLocale(Locale locale, Context context) {
- final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale.toString());
- final String absoluteDirectoryName = context.getFilesDir() + File.separator
- + relativeDirectoryName;
- final File directory = new File(absoluteDirectoryName);
- if (!directory.exists()) {
- if (!directory.mkdirs()) {
- Log.e(TAG, "Could not create the directory for locale" + locale);
- }
- }
- return absoluteDirectoryName;
- }
-
- /**
- * Generates a file name for the id and locale passed as an argument.
- *
- * In the current implementation the file name returned will always be unique for
- * any id/locale pair, but please do not expect that the id can be the same for
- * different dictionaries with different locales. An id should be unique for any
- * dictionary.
- * The file name is pretty much an URL-encoded version of the id inside a directory
- * named like the locale, except it will also escape characters that look dangerous
- * to some file systems.
- * @param id the id of the dictionary for which to get a file name
- * @param locale the locale for which to get the file name
- * @param context the context to use for getting the directory
- * @return the name of the file to be created
- */
- private static String getCacheFileName(String id, Locale locale, Context context) {
- final String fileName = replaceFileNameDangerousCharacters(id);
- return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
- }
-
- /**
* Return for a given locale or dictionary id the provider URI to get the dictionary.
*/
private static Uri getProviderUri(String path) {
@@ -149,32 +88,32 @@ public class BinaryDictionaryFileDumper {
}
/**
- * Queries a content provider for dictionary data for some locale and returns the file addresses
+ * Queries a content provider for dictionary data for some locale and cache the returned files
*
- * This will query a content provider for dictionary data for a given locale, and return
- * the addresses of a file set the members of which are suitable to be mmap'ed. It will copy
- * them to local storage if needed.
- * It should also check the dictionary versions to avoid unnecessary copies but this is
- * still in TODO state.
- * This will make the data from the content provider the cached dictionary for this locale,
- * overwriting any previous cached data.
+ * This will query a content provider for dictionary data for a given locale, and copy the
+ * files locally so that they can be mmap'ed. This may overwrite previously cached dictionaries
+ * with newer versions if a newer version is made available by the content provider.
* @returns the addresses of the files, or null if no data could be obtained.
* @throw FileNotFoundException if the provider returns non-existent data.
* @throw IOException if the provider-returned data could not be read.
*/
- public static List<AssetFileAddress> getDictSetFromContentProvider(final Locale locale,
+ public static List<AssetFileAddress> cacheDictionariesFromContentProvider(final Locale locale,
final Context context) throws FileNotFoundException, IOException {
final ContentResolver resolver = context.getContentResolver();
final List<String> idList = getDictIdList(locale, context);
final List<AssetFileAddress> fileAddressList = new ArrayList<AssetFileAddress>();
for (String id : idList) {
- final Uri dictionaryPackUri = getProviderUri(id);
+ final Uri wordListUri = getProviderUri(id);
final AssetFileDescriptor afd =
- resolver.openAssetFileDescriptor(dictionaryPackUri, "r");
+ resolver.openAssetFileDescriptor(wordListUri, "r");
if (null == afd) continue;
final String fileName = copyFileTo(afd.createInputStream(),
- getCacheFileName(id, locale, context));
+ BinaryDictionaryGetter.getCacheFileName(id, locale, context));
afd.close();
+ if (0 >= resolver.delete(wordListUri, null, null)) {
+ // I'd rather not print the word list ID to the log here out of security concerns
+ Log.e(TAG, "Could not have the dictionary pack delete a word list");
+ }
fileAddressList.add(AssetFileAddress.makeFromFileName(fileName));
}
return fileAddressList;
@@ -192,7 +131,9 @@ public class BinaryDictionaryFileDumper {
final Locale savedLocale = Utils.setSystemLocale(res, locale);
final InputStream stream = res.openRawResource(resource);
Utils.setSystemLocale(res, savedLocale);
- return copyFileTo(stream, getCacheFileName(Integer.toString(resource), locale, context));
+ return copyFileTo(stream,
+ BinaryDictionaryGetter.getCacheFileName(Integer.toString(resource),
+ locale, context));
}
/**
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 4b1c05adf..170edad99 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -21,9 +21,11 @@ import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.util.Log;
+import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
+import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -41,6 +43,66 @@ class BinaryDictionaryGetter {
private BinaryDictionaryGetter() {}
/**
+ * Escapes a string for any characters that may be suspicious for a file or directory name.
+ *
+ * Concretely this does a sort of URL-encoding except it will encode everything that's not
+ * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
+ * we cannot allow here)
+ */
+ // TODO: create a unit test for this method
+ private static String replaceFileNameDangerousCharacters(String name) {
+ // This assumes '%' is fully available as a non-separator, normal
+ // character in a file name. This is probably true for all file systems.
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < name.length(); ++i) {
+ final int codePoint = name.codePointAt(i);
+ if (Character.isLetterOrDigit(codePoint) || '_' == codePoint) {
+ sb.appendCodePoint(codePoint);
+ } else {
+ sb.append('%');
+ sb.append(Integer.toHexString(codePoint));
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Find out the cache directory associated with a specific locale.
+ */
+ private static String getCacheDirectoryForLocale(Locale locale, Context context) {
+ final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale.toString());
+ final String absoluteDirectoryName = context.getFilesDir() + File.separator
+ + relativeDirectoryName;
+ final File directory = new File(absoluteDirectoryName);
+ if (!directory.exists()) {
+ if (!directory.mkdirs()) {
+ Log.e(TAG, "Could not create the directory for locale" + locale);
+ }
+ }
+ return absoluteDirectoryName;
+ }
+
+ /**
+ * Generates a file name for the id and locale passed as an argument.
+ *
+ * In the current implementation the file name returned will always be unique for
+ * any id/locale pair, but please do not expect that the id can be the same for
+ * different dictionaries with different locales. An id should be unique for any
+ * dictionary.
+ * The file name is pretty much an URL-encoded version of the id inside a directory
+ * named like the locale, except it will also escape characters that look dangerous
+ * to some file systems.
+ * @param id the id of the dictionary for which to get a file name
+ * @param locale the locale for which to get the file name
+ * @param context the context to use for getting the directory
+ * @return the name of the file to be created
+ */
+ public static String getCacheFileName(String id, Locale locale, Context context) {
+ final String fileName = replaceFileNameDangerousCharacters(id);
+ return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
+ }
+
+ /**
* Returns a file address from a resource, or null if it cannot be opened.
*/
private static AssetFileAddress loadFallbackResource(final Context context,
@@ -60,6 +122,30 @@ class BinaryDictionaryGetter {
}
/**
+ * Returns the list of cached files for a specific locale.
+ *
+ * @param locale the locale to find the dictionary files for.
+ * @param context the context on which to open the files upon.
+ * @return a list of binary dictionary files, which may be null but may not be empty.
+ */
+ private static List<AssetFileAddress> getCachedDictionaryList(final Locale locale,
+ final Context context) {
+ final String directoryName = getCacheDirectoryForLocale(locale, context);
+ final File[] cacheFiles = new File(directoryName).listFiles();
+ if (null == cacheFiles) return null;
+
+ final ArrayList<AssetFileAddress> fileList = new ArrayList<AssetFileAddress>();
+ for (File f : cacheFiles) {
+ if (f.canRead()) {
+ fileList.add(AssetFileAddress.makeFromFileName(f.getPath()));
+ } else {
+ Log.e(TAG, "Found a cached dictionary file but cannot read it");
+ }
+ }
+ return fileList.size() > 0 ? fileList : null;
+ }
+
+ /**
* Returns a list of file addresses for a given locale, trying relevant methods in order.
*
* Tries to get binary dictionaries from various sources, in order:
@@ -71,13 +157,16 @@ class BinaryDictionaryGetter {
* - Returns null.
* @return The address of a valid file, or null.
*/
- public static List<AssetFileAddress> getDictionaryFiles(Locale locale, Context context,
- int fallbackResId) {
+ public static List<AssetFileAddress> getDictionaryFiles(final Locale locale,
+ final Context context, final int fallbackResId) {
try {
- List<AssetFileAddress> listFromContentProvider =
- BinaryDictionaryFileDumper.getDictSetFromContentProvider(locale, context);
- if (null != listFromContentProvider) {
- return listFromContentProvider;
+ // cacheDictionariesFromContentProvider returns the list of files it copied to local
+ // storage, but we don't really care about what was copied NOW: what we want is the
+ // list of everything we ever cached, so we ignore the return value.
+ BinaryDictionaryFileDumper.cacheDictionariesFromContentProvider(locale, context);
+ List<AssetFileAddress> cachedDictionaryList = getCachedDictionaryList(locale, context);
+ if (null != cachedDictionaryList) {
+ return cachedDictionaryList;
}
// If the list is null, fall through and return the fallback
} catch (FileNotFoundException e) {
diff --git a/native/src/correction.cpp b/native/src/correction.cpp
index a931a61fb..a05be55c7 100644
--- a/native/src/correction.cpp
+++ b/native/src/correction.cpp
@@ -49,17 +49,21 @@ void Correction::initCorrection(const ProximityInfo *pi, const int inputLength,
mInputLength = inputLength;
mMaxDepth = maxDepth;
mMaxEditDistance = mInputLength < 5 ? 2 : mInputLength / 2;
- mSkippedOutputIndex = -1;
}
void Correction::initCorrectionState(
const int rootPos, const int childCount, const bool traverseAll) {
- mCorrectionStates[0].init(rootPos, childCount, traverseAll);
+ latinime::initCorrectionState(mCorrectionStates, rootPos, childCount, traverseAll);
+ // TODO: remove
+ mCorrectionStates[0].mSkipPos = mSkipPos;
}
void Correction::setCorrectionParams(const int skipPos, const int excessivePos,
const int transposedPos, const int spaceProximityPos, const int missingSpacePos) {
+ // TODO: remove
mSkipPos = skipPos;
+ // TODO: remove
+ mCorrectionStates[0].mSkipPos = skipPos;
mExcessivePos = excessivePos;
mTransposedPos = transposedPos;
mSpaceProximityPos = spaceProximityPos;
@@ -88,6 +92,12 @@ int Correction::getFinalFreq(const int freq, unsigned short **word, int *wordLen
if (mProximityInfo->sameAsTyped(mWord, outputIndex + 1) || outputIndex < MIN_SUGGEST_DEPTH) {
return -1;
}
+
+ // TODO: Remove this
+ if (mSkipPos >= 0 && mSkippedCount <= 0) {
+ return -1;
+ }
+
*word = mWord;
const bool sameLength = (mExcessivePos == mInputLength - 1) ? (mInputLength == inputIndex + 2)
: (mInputLength == inputIndex + 1);
@@ -103,8 +113,12 @@ bool Correction::initProcessState(const int outputIndex) {
--(mCorrectionStates[outputIndex].mChildCount);
mMatchedCharCount = mCorrectionStates[outputIndex].mMatchedCount;
mInputIndex = mCorrectionStates[outputIndex].mInputIndex;
- mTraverseAllNodes = mCorrectionStates[outputIndex].mTraverseAll;
+ mNeedsToTraverseAllNodes = mCorrectionStates[outputIndex].mNeedsToTraverseAllNodes;
mDiffs = mCorrectionStates[outputIndex].mDiffs;
+ mSkippedCount = mCorrectionStates[outputIndex].mSkippedCount;
+ mSkipPos = mCorrectionStates[outputIndex].mSkipPos;
+ mSkipping = false;
+ mMatching = false;
return true;
}
@@ -131,8 +145,8 @@ int Correction::getInputIndex() {
}
// TODO: remove
-bool Correction::needsToTraverseAll() {
- return mTraverseAllNodes;
+bool Correction::needsToTraverseAllNodes() {
+ return mNeedsToTraverseAllNodes;
}
void Correction::incrementInputIndex() {
@@ -146,12 +160,16 @@ void Correction::incrementOutputIndex() {
mCorrectionStates[mOutputIndex].mSiblingPos = mCorrectionStates[mOutputIndex - 1].mSiblingPos;
mCorrectionStates[mOutputIndex].mMatchedCount = mMatchedCharCount;
mCorrectionStates[mOutputIndex].mInputIndex = mInputIndex;
- mCorrectionStates[mOutputIndex].mTraverseAll = mTraverseAllNodes;
+ mCorrectionStates[mOutputIndex].mNeedsToTraverseAllNodes = mNeedsToTraverseAllNodes;
mCorrectionStates[mOutputIndex].mDiffs = mDiffs;
+ mCorrectionStates[mOutputIndex].mSkippedCount = mSkippedCount;
+ mCorrectionStates[mOutputIndex].mSkipping = mSkipping;
+ mCorrectionStates[mOutputIndex].mSkipPos = mSkipPos;
+ mCorrectionStates[mOutputIndex].mMatching = mMatching;
}
-void Correction::startTraverseAll() {
- mTraverseAllNodes = true;
+void Correction::startToTraverseAllNodes() {
+ mNeedsToTraverseAllNodes = true;
}
bool Correction::needsToPrune() const {
@@ -162,7 +180,7 @@ bool Correction::needsToPrune() const {
Correction::CorrectionType Correction::processSkipChar(
const int32_t c, const bool isTerminal) {
mWord[mOutputIndex] = c;
- if (needsToTraverseAll() && isTerminal) {
+ if (needsToTraverseAllNodes() && isTerminal) {
mTerminalInputIndex = mInputIndex;
mTerminalOutputIndex = mOutputIndex;
incrementOutputIndex();
@@ -184,10 +202,17 @@ Correction::CorrectionType Correction::processCharAndCalcState(
bool skip = false;
if (mSkipPos >= 0) {
+ if (mSkippedCount == 0 && mSkipPos < mOutputIndex) {
+ if (DEBUG_DICT) {
+ assert(mSkipPos == mOutputIndex - 1);
+ }
+ ++mSkipPos;
+ }
skip = mSkipPos == mOutputIndex;
+ mSkipping = true;
}
- if (mTraverseAllNodes || isQuote(c)) {
+ if (mNeedsToTraverseAllNodes || isQuote(c)) {
return processSkipChar(c, isTerminal);
} else {
int inputIndexForProximity = mInputIndex;
@@ -210,25 +235,18 @@ Correction::CorrectionType Correction::processCharAndCalcState(
if (unrelated) {
if (skip) {
// Skip this letter and continue deeper
- mSkippedOutputIndex = mOutputIndex;
+ ++mSkippedCount;
return processSkipChar(c, isTerminal);
} else {
return UNRELATED;
}
}
- // No need to skip. Finish traversing and increment skipPos.
- // TODO: Remove this?
- if (skip) {
- mWord[mOutputIndex] = c;
- incrementOutputIndex();
- return TRAVERSE_ALL_NOT_ON_TERMINAL;
- }
-
mWord[mOutputIndex] = c;
// If inputIndex is greater than mInputLength, that means there is no
// proximity chars. So, we don't need to check proximity.
if (ProximityInfo::SAME_OR_ACCENTED_OR_CAPITALIZED_CHAR == matchedProximityCharId) {
+ mMatching = true;
charMatched();
}
@@ -247,7 +265,7 @@ Correction::CorrectionType Correction::processCharAndCalcState(
}
// Start traversing all nodes after the index exceeds the user typed length
if (isSameAsUserTypedLength) {
- startTraverseAll();
+ startToTraverseAllNodes();
}
// Finally, we are ready to go to the next character, the next "virtual node".
@@ -317,6 +335,7 @@ inline static void multiplyRate(const int rate, int *freq) {
// RankingAlgorithm //
//////////////////////
+/* static */
int Correction::RankingAlgorithm::calculateFinalFreq(
const int inputIndex, const int outputIndex,
const int matchCount, const int freq, const bool sameLength,
@@ -329,6 +348,8 @@ int Correction::RankingAlgorithm::calculateFinalFreq(
const int fullWordMultiplier = correction->FULL_WORD_MULTIPLIER;
const ProximityInfo *proximityInfo = correction->mProximityInfo;
const int matchWeight = powerIntCapped(typedLetterMultiplier, matchCount);
+ const unsigned short* word = correction->mWord;
+ const int skippedCount = correction->mSkippedCount;
// TODO: Demote by edit distance
int finalFreq = freq * matchWeight;
@@ -382,9 +403,30 @@ int Correction::RankingAlgorithm::calculateFinalFreq(
LOGI("calc: %d, %d", outputIndex, sameLength);
}
if (sameLength) multiplyIntCapped(fullWordMultiplier, &finalFreq);
+
+ // TODO: check excessive count and transposed count
+ /*
+ If the last character of the user input word is the same as the next character
+ of the output word, and also all of characters of the user input are matched
+ to the output word, we'll promote that word a bit because
+ that word can be considered the combination of skipped and matched characters.
+ This means that the 'sm' pattern wins over the 'ma' pattern.
+ e.g.)
+ shel -> shell [mmmma] or [mmmsm]
+ hel -> hello [mmmaa] or [mmsma]
+ m ... matching
+ s ... skipping
+ a ... traversing all
+ */
+ if (matchCount == inputLength && matchCount >= 2 && skippedCount == 0
+ && word[matchCount] == word[matchCount - 1]) {
+ multiplyRate(WORDS_WITH_MATCH_SKIP_PROMOTION_RATE, &finalFreq);
+ }
+
return finalFreq;
}
+/* static */
int Correction::RankingAlgorithm::calcFreqForSplitTwoWords(
const int firstFreq, const int secondFreq, const Correction* correction) {
const int spaceProximityPos = correction->mSpaceProximityPos;
diff --git a/native/src/correction.h b/native/src/correction.h
index 590d62f62..0aa5660a6 100644
--- a/native/src/correction.h
+++ b/native/src/correction.h
@@ -52,7 +52,6 @@ public:
bool *traverseAllNodes, int *diffs);
int getOutputIndex();
int getInputIndex();
- bool needsToTraverseAll();
virtual ~Correction();
int getSpaceProximityPos() const {
@@ -101,45 +100,46 @@ public:
return mCorrectionStates[index].mParentIndex;
}
private:
- void charMatched();
- void incrementInputIndex();
- void incrementOutputIndex();
- void startTraverseAll();
+ inline void charMatched();
+ inline void incrementInputIndex();
+ inline void incrementOutputIndex();
+ inline bool needsToTraverseAllNodes();
+ inline void startToTraverseAllNodes();
+ inline bool isQuote(const unsigned short c);
+ inline CorrectionType processSkipChar(const int32_t c, const bool isTerminal);
// TODO: remove
-
- void incrementDiffs() {
+ inline void incrementDiffs() {
++mDiffs;
}
const int TYPED_LETTER_MULTIPLIER;
const int FULL_WORD_MULTIPLIER;
-
const ProximityInfo *mProximityInfo;
int mMaxEditDistance;
int mMaxDepth;
int mInputLength;
- int mSkipPos;
- int mSkippedOutputIndex;
int mExcessivePos;
int mTransposedPos;
int mSpaceProximityPos;
int mMissingSpacePos;
-
- int mMatchedCharCount;
- int mInputIndex;
- int mOutputIndex;
int mTerminalInputIndex;
int mTerminalOutputIndex;
- int mDiffs;
- bool mTraverseAllNodes;
unsigned short mWord[MAX_WORD_LENGTH_INTERNAL];
CorrectionState mCorrectionStates[MAX_WORD_LENGTH_INTERNAL];
- inline bool isQuote(const unsigned short c);
- inline CorrectionType processSkipChar(const int32_t c, const bool isTerminal);
+ // The following member variables are being used as cache values of the correction state.
+ int mOutputIndex;
+ int mInputIndex;
+ int mDiffs;
+ int mMatchedCharCount;
+ int mSkippedCount;
+ int mSkipPos;
+ bool mNeedsToTraverseAllNodes;
+ bool mMatching;
+ bool mSkipping;
class RankingAlgorithm {
public:
diff --git a/native/src/correction_state.h b/native/src/correction_state.h
index 1fe02b853..3ff8134e6 100644
--- a/native/src/correction_state.h
+++ b/native/src/correction_state.h
@@ -23,32 +23,35 @@
namespace latinime {
-class CorrectionState {
-public:
+struct CorrectionState {
int mParentIndex;
- int mMatchedCount;
- int mChildCount;
- int mInputIndex;
- int mDiffs;
int mSiblingPos;
- bool mTraverseAll;
-
- inline void init(const int rootPos, const int childCount, const bool traverseAll) {
- set(-1, 0, childCount, 0, 0, rootPos, traverseAll);
- }
-
-private:
- inline void set(const int parentIndex, const int matchedCount, const int childCount,
- const int inputIndex, const int diffs, const int siblingPos,
- const bool traverseAll) {
- mParentIndex = parentIndex;
- mMatchedCount = matchedCount;
- mChildCount = childCount;
- mInputIndex = inputIndex;
- mDiffs = diffs;
- mSiblingPos = siblingPos;
- mTraverseAll = traverseAll;
- }
+ uint16_t mChildCount;
+ uint8_t mInputIndex;
+ uint8_t mDiffs;
+ uint8_t mMatchedCount;
+ uint8_t mSkippedCount;
+ int8_t mSkipPos; // should be signed
+ bool mMatching;
+ bool mSkipping;
+ bool mNeedsToTraverseAllNodes;
+
};
+
+inline static void initCorrectionState(CorrectionState *state, const int rootPos,
+ const uint16_t childCount, const bool traverseAll) {
+ state->mParentIndex = -1;
+ state->mChildCount = childCount;
+ state->mInputIndex = 0;
+ state->mDiffs = 0;
+ state->mSiblingPos = rootPos;
+ state->mMatchedCount = 0;
+ state->mSkippedCount = 0;
+ state->mMatching = false;
+ state->mSkipping = false;
+ state->mNeedsToTraverseAllNodes = traverseAll;
+ state->mSkipPos = -1;
+}
+
} // namespace latinime
#endif // LATINIME_CORRECTION_STATE_H
diff --git a/native/src/defines.h b/native/src/defines.h
index 5a5d3ee0c..c1838d341 100644
--- a/native/src/defines.h
+++ b/native/src/defines.h
@@ -160,6 +160,7 @@ static void prof_out(void) {
#define WORDS_WITH_TRANSPOSED_CHARACTERS_DEMOTION_RATE 60
#define FULL_MATCHED_WORDS_PROMOTION_RATE 120
#define WORDS_WITH_PROXIMITY_CHARACTER_DEMOTION_RATE 90
+#define WORDS_WITH_MATCH_SKIP_PROMOTION_RATE 105
// This should be greater than or equal to MAX_WORD_LENGTH defined in BinaryDictionary.java
// This is only used for the size of array. Not to be used in c functions.
diff --git a/native/src/proximity_info.h b/native/src/proximity_info.h
index 5034c3b89..d9ed46f5b 100644
--- a/native/src/proximity_info.h
+++ b/native/src/proximity_info.h
@@ -46,6 +46,7 @@ public:
ProximityType getMatchedProximityId(
const int index, const unsigned short c, const bool checkProximityChars) const;
bool sameAsTyped(const unsigned short *word, int length) const;
+
private:
int getStartIndexFromCoordinates(const int x, const int y) const;
const int MAX_PROXIMITY_CHARS_SIZE;
diff --git a/native/src/unigram_dictionary.cpp b/native/src/unigram_dictionary.cpp
index bbfaea454..6517bc0b8 100644
--- a/native/src/unigram_dictionary.cpp
+++ b/native/src/unigram_dictionary.cpp
@@ -193,14 +193,8 @@ void UnigramDictionary::getWordSuggestions(ProximityInfo *proximityInfo,
PROF_START(2);
// Suggestion with missing character
- if (SUGGEST_WORDS_WITH_MISSING_CHARACTER) {
- for (int i = 0; i < codesSize; ++i) {
- if (DEBUG_DICT) {
- LOGI("--- Suggest missing characters %d", i);
- }
- getSuggestionCandidates(i, -1, -1);
- }
- }
+ LOGI("--- Suggest missing characters");
+ getSuggestionCandidates(0, -1, -1);
PROF_END(2);
PROF_START(3);
diff --git a/tools/makedict2/Android.mk b/tools/makedict2/Android.mk
new file mode 100644
index 000000000..e056168e9
--- /dev/null
+++ b/tools/makedict2/Android.mk
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2011 The Android Open Source Project
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+LOCAL_SRC_FILES += $(call all-java-files-under,tests)
+LOCAL_JAR_MANIFEST := etc/manifest.txt
+LOCAL_MODULE_TAGS := eng
+LOCAL_MODULE := makedict2
+LOCAL_JAVA_LIBRARIES := junit
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+include $(LOCAL_PATH)/etc/Android.mk
diff --git a/tools/makedict2/etc/Android.mk b/tools/makedict2/etc/Android.mk
new file mode 100644
index 000000000..c71377cdf
--- /dev/null
+++ b/tools/makedict2/etc/Android.mk
@@ -0,0 +1,21 @@
+# Copyright (C) 2011 The Android Open Source Project
+#
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := eng
+
+LOCAL_PREBUILT_EXECUTABLES := makedict2
+include $(BUILD_HOST_PREBUILT)
diff --git a/tools/makedict2/etc/makedict2 b/tools/makedict2/etc/makedict2
new file mode 100755
index 000000000..1bad825e1
--- /dev/null
+++ b/tools/makedict2/etc/makedict2
@@ -0,0 +1,63 @@
+#!/bin/sh
+# Copyright 2011, The Android Open Source Project
+#
+# 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.
+
+# Set up prog to be the path of this script, including following symlinks,
+# and set up progdir to be the fully-qualified pathname of its directory.
+prog="$0"
+while [ -h "${prog}" ]; do
+ newProg=`/bin/ls -ld "${prog}"`
+ newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
+ if expr "x${newProg}" : 'x/' >/dev/null; then
+ prog="${newProg}"
+ else
+ progdir=`dirname "${prog}"`
+ prog="${progdir}/${newProg}"
+ fi
+done
+oldwd=`pwd`
+progdir=`dirname "${prog}"`
+cd "${progdir}"
+progdir=`pwd`
+prog="${progdir}"/`basename "${prog}"`
+cd "${oldwd}"
+
+jarfile=makedict2.jar
+frameworkdir="$progdir"
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ frameworkdir=`dirname "$progdir"`/tools/lib
+ libdir=`dirname "$progdir"`/tools/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ frameworkdir=`dirname "$progdir"`/framework
+ libdir=`dirname "$progdir"`/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ echo `basename "$prog"`": can't find $jarfile"
+ exit 1
+fi
+
+if [ "$OSTYPE" = "cygwin" ] ; then
+ jarpath=`cygpath -w "$frameworkdir/$jarfile"`
+ progdir=`cygpath -w "$progdir"`
+else
+ jarpath="$frameworkdir/$jarfile"
+fi
+
+# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored
+# might need more memory, e.g. -Xmx128M
+exec java -ea -Djava.ext.dirs="$frameworkdir" -jar "$jarpath" "$@"
diff --git a/tools/makedict2/etc/manifest.txt b/tools/makedict2/etc/manifest.txt
new file mode 100644
index 000000000..948609da6
--- /dev/null
+++ b/tools/makedict2/etc/manifest.txt
@@ -0,0 +1 @@
+Main-Class: com.android.inputmethod.latin.DictionaryMaker
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/BinaryDictInputOutput.java b/tools/makedict2/src/com/android/inputmethod/latin/BinaryDictInputOutput.java
new file mode 100644
index 000000000..92f402d3e
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/BinaryDictInputOutput.java
@@ -0,0 +1,1024 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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 com.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.FusionDictionary.CharGroup;
+import com.android.inputmethod.latin.FusionDictionary.Node;
+import com.android.inputmethod.latin.FusionDictionary.WeightedString;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Reads and writes XML files for a FusionDictionary.
+ *
+ * All the methods in this class are static.
+ */
+public class BinaryDictInputOutput {
+
+ /* Node layout is as follows:
+ * | addressType xx : mask with MASK_GROUP_ADDRESS_TYPE
+ * 2 bits, 00 = no children : FLAG_GROUP_ADDRESS_TYPE_NOADDRESS
+ * f | 01 = 1 byte : FLAG_GROUP_ADDRESS_TYPE_ONEBYTE
+ * l | 10 = 2 bytes : FLAG_GROUP_ADDRESS_TYPE_TWOBYTES
+ * a | 11 = 3 bytes : FLAG_GROUP_ADDRESS_TYPE_THREEBYTES
+ * g | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS
+ * s | has a terminal ? 1 bit, 1 = yes, 0 = no : FLAG_IS_TERMINAL
+ * | reserved 1 bit, 1 = yes, 0 = no
+ * | has bigrams ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_BIGRAMS
+ *
+ * c | IF FLAG_HAS_MULTIPLE_CHARS
+ * h | char, char, char, char n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers
+ * a | end 1 byte, = 0
+ * r | ELSE
+ * s | char 1 or 3 bytes
+ * | END
+ *
+ * f |
+ * r | IF FLAG_IS_TERMINAL
+ * e | frequency 1 byte
+ * q |
+ *
+ * c | IF 00 = FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = addressType
+ * h | // nothing
+ * i | ELSIF 01 = FLAG_GROUP_ADDRESS_TYPE_ONEBYTE == addressType
+ * l | children address, 1 byte
+ * d | ELSIF 10 = FLAG_GROUP_ADDRESS_TYPE_TWOBYTES == addressType
+ * r | children address, 2 bytes
+ * e | ELSE // 11 = FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = addressType
+ * n | children address, 3 bytes
+ * A | END
+ * d
+ * dress
+ *
+ * | IF FLAG_IS_TERMINAL && FLAG_HAS_BIGRAMS
+ * | bigrams address list
+ *
+ * Char format is:
+ * 1 byte = bbbbbbbb match
+ * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte
+ * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because
+ * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with
+ * 00011111 would be outside unicode.
+ * else: iso-latin-1 code
+ * This allows for the whole unicode range to be encoded, including chars outside of
+ * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control
+ * characters which should never happen anyway (and still work, but take 3 bytes).
+ *
+ * bigram and shortcut address list is:
+ * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT
+ * | addressSign = 1 bit, : FLAG_ATTRIBUTE_OFFSET_NEGATIVE
+ * | 1 = must take -address, 0 = must take +address
+ * | xx : mask with MASK_ATTRIBUTE_ADDRESS_TYPE
+ * | addressFormat = 2 bits, 00 = unused : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
+ * | 01 = 1 byte : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
+ * | 10 = 2 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES
+ * | 11 = 3 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES
+ * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY
+ * <address> | IF (01 == FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE == addressFormat)
+ * | read 1 byte, add top 4 bits
+ * | ELSIF (10 == FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES == addressFormat)
+ * | read 2 bytes, add top 4 bits
+ * | ELSE // 11 == FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES == addressFormat
+ * | read 3 bytes, add top 4 bits
+ * | END
+ * | if (FLAG_ATTRIBUTE_OFFSET_NEGATIVE) then address = -address
+ * if (FLAG_ATTRIBUTE_HAS_NET) goto bigram_and_shortcut_address_list_is
+ *
+ */
+
+ private static final int MAGIC_NUMBER = 0x78B1;
+ private static final int VERSION = 1;
+ private static final int MAXIMUM_SUPPORTED_VERSION = VERSION;
+ // No options yet, reserved for future use.
+ private static final int OPTIONS = 0;
+
+ // TODO: Make this value adaptative to content data, store it in the header, and
+ // use it in the reading code.
+ private static final int MAX_WORD_LENGTH = 48;
+
+ private static final int MASK_GROUP_ADDRESS_TYPE = 0xC0;
+ private static final int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00;
+ private static final int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40;
+ private static final int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80;
+ private static final int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0;
+
+ private static final int FLAG_HAS_MULTIPLE_CHARS = 0x20;
+
+ private static final int FLAG_IS_TERMINAL = 0x10;
+ private static final int FLAG_HAS_BIGRAMS = 0x04;
+
+ private static final int FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
+ private static final int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
+ private static final int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
+ private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
+ private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
+ private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
+ private static final int FLAG_ATTRIBUTE_FREQUENCY = 0x0F;
+
+ private static final int GROUP_CHARACTERS_TERMINATOR = 0x1F;
+
+ private static final int GROUP_COUNT_SIZE = 1;
+ private static final int GROUP_TERMINATOR_SIZE = 1;
+ private static final int GROUP_FLAGS_SIZE = 1;
+ private static final int GROUP_FREQUENCY_SIZE = 1;
+ private static final int GROUP_MAX_ADDRESS_SIZE = 3;
+ private static final int GROUP_ATTRIBUTE_FLAGS_SIZE = 1;
+ private static final int GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
+
+ private static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE;
+ private static final int INVALID_CHARACTER = -1;
+
+ // Limiting to 127 for upward compatibility
+ // TODO: implement a scheme to be able to shoot 256 chargroups in a node
+ private static final int MAX_CHARGROUPS_IN_A_NODE = 127;
+
+ private static final int MAX_TERMINAL_FREQUENCY = 255;
+
+ /**
+ * A class grouping utility function for our specific character encoding.
+ */
+ private static class CharEncoding {
+
+ private static final int MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20;
+ private static final int MAXIMAL_ONE_BYTE_CHARACTER_VALUE = 0xFF;
+
+ /**
+ * Helper method to find out whether this code fits on one byte
+ */
+ private static boolean fitsOnOneByte(int character) {
+ return character >= MINIMAL_ONE_BYTE_CHARACTER_VALUE
+ && character <= MAXIMAL_ONE_BYTE_CHARACTER_VALUE;
+ }
+
+ /**
+ * Compute the size of a character given its character code.
+ *
+ * Char format is:
+ * 1 byte = bbbbbbbb match
+ * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte
+ * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because
+ * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with
+ * 00011111 would be outside unicode.
+ * else: iso-latin-1 code
+ * This allows for the whole unicode range to be encoded, including chars outside of
+ * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control
+ * characters which should never happen anyway (and still work, but take 3 bytes).
+ *
+ * @param character the character code.
+ * @return the size in binary encoded-form, either 1 or 3 bytes.
+ */
+ private static int getCharSize(int character) {
+ // See char encoding in FusionDictionary.java
+ if (fitsOnOneByte(character)) return 1;
+ if (INVALID_CHARACTER == character) return 1;
+ return 3;
+ }
+
+ /**
+ * Compute the byte size of a character array.
+ */
+ private static int getCharArraySize(final int[] chars) {
+ int size = 0;
+ for (int character : chars) size += getCharSize(character);
+ return size;
+ }
+
+ /**
+ * Writes a char array to a byte buffer.
+ *
+ * @param characters the character array to write.
+ * @param buffer the byte buffer to write to.
+ * @param index the index in buffer to write the character array to.
+ * @return the index after the last character.
+ */
+ private static int writeCharArray(int[] characters, byte[] buffer, int index) {
+ for (int character : characters) {
+ if (1 == getCharSize(character)) {
+ buffer[index++] = (byte)character;
+ } else {
+ buffer[index++] = (byte)(0xFF & (character >> 16));
+ buffer[index++] = (byte)(0xFF & (character >> 8));
+ buffer[index++] = (byte)(0xFF & character);
+ }
+ }
+ return index;
+ }
+
+ /**
+ * Reads a character from the file.
+ *
+ * This follows the character format documented earlier in this source file.
+ *
+ * @param source the file, positioned over an encoded character.
+ * @return the character code.
+ */
+ private static int readChar(RandomAccessFile source) throws IOException {
+ int character = source.readUnsignedByte();
+ if (!fitsOnOneByte(character)) {
+ if (GROUP_CHARACTERS_TERMINATOR == character)
+ return INVALID_CHARACTER;
+ character <<= 16;
+ character += source.readUnsignedShort();
+ }
+ return character;
+ }
+ }
+
+ /**
+ * Compute the binary size of the character array in a group
+ *
+ * If only one character, this is the size of this character. If many, it's the sum of their
+ * sizes + 1 byte for the terminator.
+ *
+ * @param group the group
+ * @return the size of the char array, including the terminator if any
+ */
+ private static int getGroupCharactersSize(CharGroup group) {
+ int size = CharEncoding.getCharArraySize(group.mChars);
+ if (group.hasSeveralChars()) size += GROUP_TERMINATOR_SIZE;
+ return size;
+ }
+
+ /**
+ * Compute the maximum size of a CharGroup, assuming 3-byte addresses for everything.
+ *
+ * @param group the CharGroup to compute the size of.
+ * @return the maximum size of the group.
+ */
+ private static int getCharGroupMaximumSize(CharGroup group) {
+ int size = getGroupCharactersSize(group) + GROUP_FLAGS_SIZE;
+ // If terminal, one byte for the frequency
+ if (group.isTerminal()) size += GROUP_FREQUENCY_SIZE;
+ size += GROUP_MAX_ADDRESS_SIZE; // For children address
+ if (null != group.mBigrams) {
+ for (WeightedString bigram : group.mBigrams) {
+ size += GROUP_ATTRIBUTE_FLAGS_SIZE + GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE;
+ }
+ }
+ return size;
+ }
+
+ /**
+ * Compute the maximum size of a node, assuming 3-byte addresses for everything, and caches
+ * it in the 'actualSize' member of the node.
+ *
+ * @param node the node to compute the maximum size of.
+ */
+ private static void setNodeMaximumSize(Node node) {
+ int size = GROUP_COUNT_SIZE;
+ for (CharGroup g : node.mData) {
+ final int groupSize = getCharGroupMaximumSize(g);
+ g.mCachedSize = groupSize;
+ size += groupSize;
+ }
+ node.mCachedSize = size;
+ }
+
+ /**
+ * Helper method to hide the actual value of the no children address.
+ */
+ private static boolean hasChildrenAddress(int address) {
+ return NO_CHILDREN_ADDRESS != address;
+ }
+
+ /**
+ * Compute the size, in bytes, that an address will occupy.
+ *
+ * This can be used either for children addresses (which are always positive) or for
+ * attribute, which may be positive or negative but
+ * store their sign bit separately.
+ *
+ * @param address the address
+ * @return the byte size.
+ */
+ private static int getByteSize(int address) {
+ assert(address < 0x1000000);
+ if (!hasChildrenAddress(address)) {
+ return 0;
+ } else if (Math.abs(address) < 0x100) {
+ return 1;
+ } else if (Math.abs(address) < 0x10000) {
+ return 2;
+ } else {
+ return 3;
+ }
+ }
+ // End utility methods.
+
+ // This method is responsible for finding a nice ordering of the nodes that favors run-time
+ // cache performance and dictionary size.
+ /* package for tests */ static ArrayList<Node> flattenTree(Node root) {
+ final int treeSize = FusionDictionary.countCharGroups(root);
+ MakedictLog.i("Counted nodes : " + treeSize);
+ final ArrayList<Node> flatTree = new ArrayList<Node>(treeSize);
+ return flattenTreeInner(flatTree, root);
+ }
+
+ private static ArrayList<Node> flattenTreeInner(ArrayList<Node> list, Node node) {
+ // Removing the node is necessary if the tails are merged, because we would then
+ // add the same node several times when we only want it once. A number of places in
+ // the code also depends on any node being only once in the list.
+ // Merging tails can only be done if there are no attributes. Searching for attributes
+ // in LatinIME code depends on a total breadth-first ordering, which merging tails
+ // breaks. If there are no attributes, it should be fine (and reduce the file size)
+ // to merge tails, and the following step would be necessary.
+ // If eventually the code runs on Android, searching through the whole array each time
+ // may be a performance concern.
+ list.remove(node);
+ list.add(node);
+ final ArrayList<CharGroup> branches = node.mData;
+ final int nodeSize = branches.size();
+ for (CharGroup group : branches) {
+ if (null != group.mChildren) flattenTreeInner(list, group.mChildren);
+ }
+ return list;
+ }
+
+ /**
+ * Finds the absolute address of a word in the dictionary.
+ *
+ * @param dict the dictionary in which to search.
+ * @param word the word we are searching for.
+ * @return the word address. If it is not found, an exception is thrown.
+ */
+ private static int findAddressOfWord(final FusionDictionary dict, final String word) {
+ return FusionDictionary.findWordInTree(dict.mRoot, word).mCachedAddress;
+ }
+
+ /**
+ * Computes the actual node size, based on the cached addresses of the children nodes.
+ *
+ * Each node stores its tentative address. During dictionary address computing, these
+ * are not final, but they can be used to compute the node size (the node size depends
+ * on the address of the children because the number of bytes necessary to store an
+ * address depends on its numeric value.
+ *
+ * @param node the node to compute the size of.
+ * @param dict the dictionary in which the word/attributes are to be found.
+ */
+ private static void computeActualNodeSize(Node node, FusionDictionary dict) {
+ int size = GROUP_COUNT_SIZE;
+ for (CharGroup group : node.mData) {
+ int groupSize = GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
+ if (group.isTerminal()) groupSize += GROUP_FREQUENCY_SIZE;
+ if (null != group.mChildren) {
+ final int offsetBasePoint= groupSize + node.mCachedAddress + size;
+ final int offset = group.mChildren.mCachedAddress - offsetBasePoint;
+ groupSize += getByteSize(offset);
+ }
+ if (null != group.mBigrams) {
+ for (WeightedString bigram : group.mBigrams) {
+ final int offsetBasePoint = groupSize + node.mCachedAddress + size
+ + GROUP_FLAGS_SIZE;
+ final int addressOfBigram = findAddressOfWord(dict, bigram.mWord);
+ final int offset = addressOfBigram - offsetBasePoint;
+ groupSize += getByteSize(offset) + GROUP_FLAGS_SIZE;
+ }
+ }
+ group.mCachedSize = groupSize;
+ size += groupSize;
+ }
+ node.mCachedSize = size;
+ }
+
+ /**
+ * Computes the byte size of a list of nodes and updates each node cached position.
+ *
+ * @param flatNodes the array of nodes.
+ * @return the byte size of the entire stack.
+ */
+ private static int stackNodes(ArrayList<Node> flatNodes) {
+ int nodeOffset = 0;
+ for (Node n : flatNodes) {
+ n.mCachedAddress = nodeOffset;
+ int groupOffset = 0;
+ for (CharGroup g : n.mData) {
+ g.mCachedAddress = GROUP_COUNT_SIZE + nodeOffset + groupOffset;
+ groupOffset += g.mCachedSize;
+ }
+ if (groupOffset + GROUP_COUNT_SIZE != n.mCachedSize) {
+ throw new RuntimeException("Bug : Stored and computed node size differ");
+ }
+ nodeOffset += n.mCachedSize;
+ }
+ return nodeOffset;
+ }
+
+ /**
+ * Compute the addresses and sizes of an ordered node array.
+ *
+ * This method takes a node array and will update its cached address and size values
+ * so that they can be written into a file. It determines the smallest size each of the
+ * nodes can be given the addresses of its children and attributes, and store that into
+ * each node.
+ * The order of the node is given by the order of the array. This method makes no effort
+ * to find a good order; it only mechanically computes the size this order results in.
+ *
+ * @param dict the dictionary
+ * @param flatNodes the ordered array of nodes
+ * @return the same array it was passed. The nodes have been updated for address and size.
+ */
+ private static ArrayList<Node> computeAddresses(FusionDictionary dict,
+ ArrayList<Node> flatNodes) {
+ // First get the worst sizes and offsets
+ for (Node n : flatNodes) setNodeMaximumSize(n);
+ final int offset = stackNodes(flatNodes);
+
+ MakedictLog.i("Compressing the array addresses. Original size : " + offset);
+ MakedictLog.i("(Recursively seen size : " + offset + ")");
+
+ int passes = 0;
+ boolean changesDone = false;
+ do {
+ changesDone = false;
+ for (Node n : flatNodes) {
+ final int oldNodeSize = n.mCachedSize;
+ computeActualNodeSize(n, dict);
+ final int newNodeSize = n.mCachedSize;
+ if (oldNodeSize < newNodeSize) throw new RuntimeException("Increased size ?!");
+ if (oldNodeSize != newNodeSize) changesDone = true;
+ }
+ stackNodes(flatNodes);
+ ++passes;
+ } while (changesDone);
+
+ final Node lastNode = flatNodes.get(flatNodes.size() - 1);
+ MakedictLog.i("Compression complete in " + passes + " passes.");
+ MakedictLog.i("After address compression : "
+ + (lastNode.mCachedAddress + lastNode.mCachedSize));
+
+ return flatNodes;
+ }
+
+ /**
+ * Sanity-checking method.
+ *
+ * This method checks an array of node for juxtaposition, that is, it will do
+ * nothing if each node's cached address is actually the previous node's address
+ * plus the previous node's size.
+ * If this is not the case, it will throw an exception.
+ *
+ * @param array the array node to check
+ */
+ private static void checkFlatNodeArray(ArrayList<Node> array) {
+ int offset = 0;
+ int index = 0;
+ for (Node n : array) {
+ if (n.mCachedAddress != offset) {
+ throw new RuntimeException("Wrong address for node " + index
+ + " : expected " + offset + ", got " + n.mCachedAddress);
+ }
+ ++index;
+ offset += n.mCachedSize;
+ }
+ }
+
+ /**
+ * Helper method to write a variable-size address to a file.
+ *
+ * @param buffer the buffer to write to.
+ * @param index the index in the buffer to write the address to.
+ * @param address the address to write.
+ * @return the size in bytes the address actually took.
+ */
+ private static int writeVariableAddress(byte[] buffer, int index, int address) {
+ switch (getByteSize(address)) {
+ case 1:
+ buffer[index++] = (byte)address;
+ return 1;
+ case 2:
+ buffer[index++] = (byte)(0xFF & (address >> 8));
+ buffer[index++] = (byte)(0xFF & address);
+ return 2;
+ case 3:
+ buffer[index++] = (byte)(0xFF & (address >> 16));
+ buffer[index++] = (byte)(0xFF & (address >> 8));
+ buffer[index++] = (byte)(0xFF & address);
+ return 3;
+ case 0:
+ return 0;
+ default:
+ throw new RuntimeException("Address " + address + " has a strange size");
+ }
+ }
+
+ private static byte makeCharGroupFlags(final CharGroup group, final int groupAddress,
+ final int childrenOffset) {
+ byte flags = 0;
+ if (group.mChars.length > 1) flags |= FLAG_HAS_MULTIPLE_CHARS;
+ if (group.mFrequency >= 0) {
+ flags |= FLAG_IS_TERMINAL;
+ }
+ if (null != group.mChildren) {
+ switch (getByteSize(childrenOffset)) {
+ case 1:
+ flags |= FLAG_GROUP_ADDRESS_TYPE_ONEBYTE;
+ break;
+ case 2:
+ flags |= FLAG_GROUP_ADDRESS_TYPE_TWOBYTES;
+ break;
+ case 3:
+ flags |= FLAG_GROUP_ADDRESS_TYPE_THREEBYTES;
+ break;
+ default:
+ throw new RuntimeException("Node with a strange address");
+ }
+ }
+ if (null != group.mBigrams) flags |= FLAG_HAS_BIGRAMS;
+ return flags;
+ }
+
+ /**
+ * Makes the flag value for an attribute.
+ *
+ * @param more whether there are more attributes after this one.
+ * @param offset the offset of the attribute.
+ * @param frequency the frequency of the attribute, 0..15
+ * @return the flags
+ */
+ private static final int makeAttributeFlags(final boolean more, final int offset,
+ final int frequency) {
+ int bigramFlags = (more ? FLAG_ATTRIBUTE_HAS_NEXT : 0)
+ + (offset < 0 ? FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0);
+ switch (getByteSize(offset)) {
+ case 1:
+ bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
+ break;
+ case 2:
+ bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
+ break;
+ case 3:
+ bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
+ break;
+ default:
+ throw new RuntimeException("Strange offset size");
+ }
+ bigramFlags += frequency & FLAG_ATTRIBUTE_FREQUENCY;
+ return bigramFlags;
+ }
+
+ /**
+ * Write a node to memory. The node is expected to have its final position cached.
+ *
+ * This can be an empty map, but the more is inside the faster the lookups will be. It can
+ * be carried on as long as nodes do not move.
+ *
+ * @param dict the dictionary the node is a part of (for relative offsets).
+ * @param buffer the memory buffer to write to.
+ * @param node the node to write.
+ * @return the address of the END of the node.
+ */
+ private static int writePlacedNode(FusionDictionary dict, byte[] buffer, Node node) {
+ int index = node.mCachedAddress;
+
+ final int size = node.mData.size();
+ if (size > MAX_CHARGROUPS_IN_A_NODE)
+ throw new RuntimeException("A node has a group count over 127 (" + size + ").");
+
+ buffer[index++] = (byte)size;
+ int groupAddress = index;
+ for (int i = 0; i < size; ++i) {
+ CharGroup group = node.mData.get(i);
+ if (index != group.mCachedAddress) throw new RuntimeException("Bug: write index is not "
+ + "the same as the cached address of the group");
+ groupAddress += GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
+ // Sanity checks.
+ if (group.mFrequency > MAX_TERMINAL_FREQUENCY) {
+ throw new RuntimeException("A node has a frequency > " + MAX_TERMINAL_FREQUENCY
+ + " : " + group.mFrequency);
+ }
+ if (group.mFrequency >= 0) groupAddress += GROUP_FREQUENCY_SIZE;
+ final int childrenOffset = null == group.mChildren
+ ? NO_CHILDREN_ADDRESS : group.mChildren.mCachedAddress - groupAddress;
+ byte flags = makeCharGroupFlags(group, groupAddress, childrenOffset);
+ buffer[index++] = flags;
+ index = CharEncoding.writeCharArray(group.mChars, buffer, index);
+ if (group.hasSeveralChars()) {
+ buffer[index++] = GROUP_CHARACTERS_TERMINATOR;
+ }
+ if (group.mFrequency >= 0) {
+ buffer[index++] = (byte) group.mFrequency;
+ }
+ final int shift = writeVariableAddress(buffer, index, childrenOffset);
+ index += shift;
+ groupAddress += shift;
+
+ // Write bigrams
+ if (null != group.mBigrams) {
+ int remainingBigrams = group.mBigrams.size();
+ for (WeightedString bigram : group.mBigrams) {
+ boolean more = remainingBigrams > 1;
+ final int addressOfBigram = findAddressOfWord(dict, bigram.mWord);
+ ++groupAddress;
+ final int offset = addressOfBigram - groupAddress;
+ int bigramFlags = makeAttributeFlags(more, offset, bigram.mFrequency);
+ buffer[index++] = (byte)bigramFlags;
+ final int bigramShift = writeVariableAddress(buffer, index, Math.abs(offset));
+ index += bigramShift;
+ groupAddress += bigramShift;
+ --remainingBigrams;
+ }
+ }
+
+ }
+ if (index != node.mCachedAddress + node.mCachedSize) throw new RuntimeException(
+ "Not the same size : written "
+ + (index - node.mCachedAddress) + " bytes out of a node that should have "
+ + node.mCachedSize + " bytes");
+ return index;
+ }
+
+ /**
+ * Dumps a collection of useful statistics about a node array.
+ *
+ * This prints purely informative stuff, like the total estimated file size, the
+ * number of nodes, of character groups, the repartition of each address size, etc
+ *
+ * @param nodes the node array.
+ */
+ private static void showStatistics(ArrayList<Node> nodes) {
+ int firstTerminalAddress = Integer.MAX_VALUE;
+ int lastTerminalAddress = Integer.MIN_VALUE;
+ int size = 0;
+ int charGroups = 0;
+ int maxGroups = 0;
+ int maxRuns = 0;
+ for (Node n : nodes) {
+ if (maxGroups < n.mData.size()) maxGroups = n.mData.size();
+ for (CharGroup cg : n.mData) {
+ ++charGroups;
+ if (cg.mChars.length > maxRuns) maxRuns = cg.mChars.length;
+ if (cg.mFrequency >= 0) {
+ if (n.mCachedAddress < firstTerminalAddress)
+ firstTerminalAddress = n.mCachedAddress;
+ if (n.mCachedAddress > lastTerminalAddress)
+ lastTerminalAddress = n.mCachedAddress;
+ }
+ }
+ if (n.mCachedAddress + n.mCachedSize > size) size = n.mCachedAddress + n.mCachedSize;
+ }
+ final int[] groupCounts = new int[maxGroups + 1];
+ final int[] runCounts = new int[maxRuns + 1];
+ for (Node n : nodes) {
+ ++groupCounts[n.mData.size()];
+ for (CharGroup cg : n.mData) {
+ ++runCounts[cg.mChars.length];
+ }
+ }
+
+ MakedictLog.i("Statistics:\n"
+ + " total file size " + size + "\n"
+ + " " + nodes.size() + " nodes\n"
+ + " " + charGroups + " groups (" + ((float)charGroups / nodes.size())
+ + " groups per node)\n"
+ + " first terminal at " + firstTerminalAddress + "\n"
+ + " last terminal at " + lastTerminalAddress + "\n"
+ + " Group stats : max = " + maxGroups);
+ for (int i = 0; i < groupCounts.length; ++i) {
+ MakedictLog.i(" " + i + " : " + groupCounts[i]);
+ }
+ MakedictLog.i(" Character run stats : max = " + maxRuns);
+ for (int i = 0; i < runCounts.length; ++i) {
+ MakedictLog.i(" " + i + " : " + runCounts[i]);
+ }
+ }
+
+ /**
+ * Dumps a FusionDictionary to a file.
+ *
+ * This is the public entry point to write a dictionary to a file.
+ *
+ * @param destination the stream to write the binary data to.
+ * @param dict the dictionary to write.
+ */
+ public static void writeDictionaryBinary(OutputStream destination, FusionDictionary dict)
+ throws IOException {
+
+ // Addresses are limited to 3 bytes, so we'll just make a 16MB buffer. Since addresses
+ // can be relative to each node, the structure itself is not limited to 16MB at all, but
+ // I doubt this will ever be shot. If it is, deciding the order of the nodes becomes
+ // a quite complicated problem, because though the dictionary itself does not have a
+ // size limit, each node must still be within 16MB of all its children and parents.
+ // As long as this is ensured, the dictionary file may grow to any size.
+ // Anyway, to make a dictionary bigger than 16MB just increase the size of this buffer.
+ final byte[] buffer = new byte[1 << 24];
+ int index = 0;
+
+ // Magic number in big-endian order.
+ buffer[index++] = (byte) (0xFF & (MAGIC_NUMBER >> 8));
+ buffer[index++] = (byte) (0xFF & MAGIC_NUMBER);
+ // Dictionary version.
+ buffer[index++] = (byte) (0xFF & VERSION);
+ // Options flags
+ buffer[index++] = (byte) (0xFF & (OPTIONS >> 8));
+ buffer[index++] = (byte) (0xFF & OPTIONS);
+
+ // Should we include the locale and title of the dictionary ?
+
+ destination.write(buffer, 0, index);
+ index = 0;
+
+ // Leave the choice of the optimal node order to the flattenTree function.
+ MakedictLog.i("Flattening the tree...");
+ ArrayList<Node> flatNodes = flattenTree(dict.mRoot);
+
+ MakedictLog.i("Computing addresses...");
+ computeAddresses(dict, flatNodes);
+ MakedictLog.i("Checking array...");
+ checkFlatNodeArray(flatNodes);
+
+ MakedictLog.i("Writing file...");
+ int dataEndOffset = 0;
+ for (Node n : flatNodes) {
+ dataEndOffset = writePlacedNode(dict, buffer, n);
+ }
+
+ showStatistics(flatNodes);
+
+ destination.write(buffer, 0, dataEndOffset);
+
+ destination.close();
+ MakedictLog.i("Done");
+ }
+
+
+ // Input methods: Read a binary dictionary to memory.
+ // readDictionaryBinary is the public entry point for them.
+
+ static final int[] characterBuffer = new int[MAX_WORD_LENGTH];
+ private static CharGroupInfo readCharGroup(RandomAccessFile source,
+ final int originalGroupAddress) throws IOException {
+ int addressPointer = originalGroupAddress;
+ final int flags = source.readUnsignedByte();
+ ++addressPointer;
+ final int characters[];
+ if (0 != (flags & FLAG_HAS_MULTIPLE_CHARS)) {
+ int index = 0;
+ int character = CharEncoding.readChar(source);
+ addressPointer += CharEncoding.getCharSize(character);
+ while (-1 != character) {
+ characterBuffer[index++] = character;
+ character = CharEncoding.readChar(source);
+ addressPointer += CharEncoding.getCharSize(character);
+ }
+ characters = Arrays.copyOfRange(characterBuffer, 0, index);
+ } else {
+ final int character = CharEncoding.readChar(source);
+ addressPointer += CharEncoding.getCharSize(character);
+ characters = new int[] { character };
+ }
+ final int frequency;
+ if (0 != (FLAG_IS_TERMINAL & flags)) {
+ ++addressPointer;
+ frequency = source.readUnsignedByte();
+ } else {
+ frequency = CharGroup.NOT_A_TERMINAL;
+ }
+ int childrenAddress = addressPointer;
+ switch (flags & MASK_GROUP_ADDRESS_TYPE) {
+ case FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
+ childrenAddress += source.readUnsignedByte();
+ addressPointer += 1;
+ break;
+ case FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
+ childrenAddress += source.readUnsignedShort();
+ addressPointer += 2;
+ break;
+ case FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
+ childrenAddress += (source.readUnsignedByte() << 16) + source.readUnsignedShort();
+ addressPointer += 3;
+ break;
+ case FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
+ default:
+ childrenAddress = NO_CHILDREN_ADDRESS;
+ break;
+ }
+ ArrayList<PendingAttribute> bigrams = null;
+ if (0 != (flags & FLAG_HAS_BIGRAMS)) {
+ bigrams = new ArrayList<PendingAttribute>();
+ boolean more = true;
+ while (more) {
+ int bigramFlags = source.readUnsignedByte();
+ ++addressPointer;
+ more = (0 != (bigramFlags & FLAG_ATTRIBUTE_HAS_NEXT));
+ final int sign = 0 == (bigramFlags & FLAG_ATTRIBUTE_OFFSET_NEGATIVE) ? 1 : -1;
+ int bigramAddress = addressPointer;
+ switch (bigramFlags & MASK_ATTRIBUTE_ADDRESS_TYPE) {
+ case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
+ bigramAddress += sign * source.readUnsignedByte();
+ addressPointer += 1;
+ break;
+ case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
+ bigramAddress += sign * source.readUnsignedShort();
+ addressPointer += 2;
+ break;
+ case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
+ final int offset = ((source.readUnsignedByte() << 16)
+ + source.readUnsignedShort());
+ bigramAddress += sign * offset;
+ addressPointer += 3;
+ break;
+ default:
+ throw new RuntimeException("Has attribute with no address");
+ }
+ bigrams.add(new PendingAttribute(bigramFlags & FLAG_ATTRIBUTE_FREQUENCY,
+ bigramAddress));
+ }
+ }
+ return new CharGroupInfo(originalGroupAddress, addressPointer, flags, characters, frequency,
+ childrenAddress, bigrams);
+ }
+
+ /**
+ * Finds, as a string, the word at the address passed as an argument.
+ *
+ * @param source the file to read from.
+ * @param headerSize the size of the header.
+ * @param address the address to seek.
+ * @return the word, as a string.
+ * @throws IOException if the file can't be read.
+ */
+ private static String getWordAtAddress(RandomAccessFile source, long headerSize,
+ int address) throws IOException {
+ final long originalPointer = source.getFilePointer();
+ source.seek(headerSize);
+ final int count = source.readUnsignedByte();
+ int groupOffset = 1; // 1 for the group count
+ final StringBuilder builder = new StringBuilder();
+ String result = null;
+
+ CharGroupInfo last = null;
+ for (int i = count - 1; i >= 0; --i) {
+ CharGroupInfo info = readCharGroup(source, groupOffset);
+ groupOffset = info.mEndAddress;
+ if (info.mOriginalAddress == address) {
+ builder.append(new String(info.mCharacters, 0, info.mCharacters.length));
+ result = builder.toString();
+ break; // and return
+ }
+ if (hasChildrenAddress(info.mChildrenAddress)) {
+ if (info.mChildrenAddress > address) {
+ if (null == last) continue;
+ builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
+ source.seek(last.mChildrenAddress + headerSize);
+ groupOffset = last.mChildrenAddress + 1;
+ i = source.readUnsignedByte();
+ last = null;
+ continue;
+ }
+ last = info;
+ }
+ if (0 == i && hasChildrenAddress(last.mChildrenAddress)) {
+ builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
+ source.seek(last.mChildrenAddress + headerSize);
+ groupOffset = last.mChildrenAddress + 1;
+ i = source.readUnsignedByte();
+ last = null;
+ continue;
+ }
+ }
+ source.seek(originalPointer);
+ return result;
+ }
+
+ /**
+ * Reads a single node from a binary file.
+ *
+ * This methods reads the file at the current position of its file pointer. A node is
+ * fully expected to start at the current position.
+ * This will recursively read other nodes into the structure, populating the reverse
+ * maps on the fly and using them to keep track of already read nodes.
+ *
+ * @param source the data file, correctly positioned at the start of a node.
+ * @param headerSize the size, in bytes, of the file header.
+ * @param reverseNodeMap a mapping from addresses to already read nodes.
+ * @param reverseGroupMap a mapping from addresses to already read character groups.
+ * @return the read node with all his children already read.
+ */
+ private static Node readNode(RandomAccessFile source, long headerSize,
+ Map<Integer, Node> reverseNodeMap, Map<Integer, CharGroup> reverseGroupMap)
+ throws IOException {
+ final int nodeOrigin = (int)(source.getFilePointer() - headerSize);
+ final int count = source.readUnsignedByte();
+ final ArrayList<CharGroup> nodeContents = new ArrayList<CharGroup>();
+ int groupOffset = nodeOrigin + 1; // 1 byte for the group count
+ for (int i = count; i > 0; --i) {
+ CharGroupInfo info = readCharGroup(source, groupOffset);
+ ArrayList<WeightedString> bigrams = null;
+ if (null != info.mBigrams) {
+ bigrams = new ArrayList<WeightedString>();
+ for (PendingAttribute bigram : info.mBigrams) {
+ final String word = getWordAtAddress(source, headerSize, bigram.mAddress);
+ bigrams.add(new WeightedString(word, bigram.mFrequency));
+ }
+ }
+ if (hasChildrenAddress(info.mChildrenAddress)) {
+ Node children = reverseNodeMap.get(info.mChildrenAddress);
+ if (null == children) {
+ final long currentPosition = source.getFilePointer();
+ source.seek(info.mChildrenAddress + headerSize);
+ children = readNode(source, headerSize, reverseNodeMap, reverseGroupMap);
+ source.seek(currentPosition);
+ }
+ nodeContents.add(
+ new CharGroup(info.mCharacters, bigrams, info.mFrequency,
+ children));
+ } else {
+ nodeContents.add(
+ new CharGroup(info.mCharacters, bigrams, info.mFrequency));
+ }
+ groupOffset = info.mEndAddress;
+ }
+ final Node node = new Node(nodeContents);
+ node.mCachedAddress = nodeOrigin;
+ reverseNodeMap.put(node.mCachedAddress, node);
+ return node;
+ }
+
+ /**
+ * Reads a random access file and returns the memory representation of the dictionary.
+ *
+ * This high-level method takes a binary file and reads its contents, populating a
+ * FusionDictionary structure. The optional dict argument is an existing dictionary to
+ * which words from the file should be added. If it is null, a new dictionary is created.
+ *
+ * @param source the file to read.
+ * @param dict an optional dictionary to add words to, or null.
+ * @return the created (or merged) dictionary.
+ */
+ public static FusionDictionary readDictionaryBinary(RandomAccessFile source,
+ FusionDictionary dict) throws IOException, UnsupportedFormatException {
+ // Check magic number
+ final int magic = source.readUnsignedShort();
+ if (MAGIC_NUMBER != magic) {
+ throw new UnsupportedFormatException("The magic number in this file does not match "
+ + "the expected value");
+ }
+
+ // Check file version
+ final int version = source.readUnsignedByte();
+ if (version > MAXIMUM_SUPPORTED_VERSION) {
+ throw new UnsupportedFormatException("This file has version " + version
+ + ", but this implementation does not support versions above "
+ + MAXIMUM_SUPPORTED_VERSION);
+ }
+
+ // Read options
+ source.readUnsignedShort();
+
+ long headerSize = source.getFilePointer();
+ Map<Integer, Node> reverseNodeMapping = new TreeMap<Integer, Node>();
+ Map<Integer, CharGroup> reverseGroupMapping = new TreeMap<Integer, CharGroup>();
+ final Node root = readNode(source, headerSize, reverseNodeMapping, reverseGroupMapping);
+
+ FusionDictionary newDict = new FusionDictionary(root,
+ new FusionDictionary.DictionaryOptions());
+ if (null != dict) {
+ for (Word w : dict) {
+ newDict.add(w.mWord, w.mFrequency, w.mBigrams);
+ }
+ }
+
+ return newDict;
+ }
+
+ /**
+ * Basic test to find out whether the file is a binary dictionary or not.
+ *
+ * Concretely this only tests the magic number.
+ *
+ * @param filename The name of the file to test.
+ * @return true if it's a binary dictionary, false otherwise
+ */
+ public static boolean isBinaryDictionary(String filename) {
+ try {
+ RandomAccessFile f = new RandomAccessFile(filename, "r");
+ return MAGIC_NUMBER == f.readUnsignedShort();
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/CharGroupInfo.java b/tools/makedict2/src/com/android/inputmethod/latin/CharGroupInfo.java
new file mode 100644
index 000000000..6badfd13a
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/CharGroupInfo.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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 com.android.inputmethod.latin;
+
+import java.util.ArrayList;
+
+/**
+ * Raw char group info straight out of a file. This will contain numbers for addresses.
+ */
+public class CharGroupInfo {
+
+ public final int mOriginalAddress;
+ public final int mEndAddress;
+ public final int mFlags;
+ public final int[] mCharacters;
+ public final int mFrequency;
+ public final int mChildrenAddress;
+ public final ArrayList<PendingAttribute> mBigrams;
+
+ public CharGroupInfo(final int originalAddress, final int endAddress, final int flags,
+ final int[] characters, final int frequency, final int childrenAddress,
+ final ArrayList<PendingAttribute> bigrams) {
+ mOriginalAddress = originalAddress;
+ mEndAddress = endAddress;
+ mFlags = flags;
+ mCharacters = characters;
+ mFrequency = frequency;
+ mChildrenAddress = childrenAddress;
+ mBigrams = bigrams;
+ }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/DictionaryMaker.java b/tools/makedict2/src/com/android/inputmethod/latin/DictionaryMaker.java
new file mode 100644
index 000000000..61944545a
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/DictionaryMaker.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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 com.android.inputmethod.latin;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.Arrays;
+import java.util.LinkedList;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.xml.sax.SAXException;
+
+/**
+ * Main class/method for DictionaryMaker.
+ */
+public class DictionaryMaker {
+
+ static class Arguments {
+ private final static String OPTION_VERSION_2 = "-2";
+ private final static String OPTION_INPUT_SOURCE = "-s";
+ private final static String OPTION_INPUT_BIGRAM_XML = "-b";
+ private final static String OPTION_OUTPUT_BINARY = "-d";
+ private final static String OPTION_OUTPUT_XML = "-x";
+ private final static String OPTION_HELP = "-h";
+ public final String mInputBinary;
+ public final String mInputUnigramXml;
+ public final String mInputBigramXml;
+ public final String mOutputBinary;
+ public final String mOutputXml;
+
+ private void checkIntegrity() {
+ checkHasExactlyOneInput();
+ checkHasAtLeastOneOutput();
+ }
+
+ private void checkHasExactlyOneInput() {
+ if (null == mInputUnigramXml && null == mInputBinary) {
+ throw new RuntimeException("No input file specified");
+ } else if (null != mInputUnigramXml && null != mInputBinary) {
+ throw new RuntimeException("Both input XML and binary specified");
+ } else if (null != mInputBinary && null != mInputBigramXml) {
+ throw new RuntimeException("Cannot specify a binary input and a separate bigram "
+ + "file");
+ }
+ }
+
+ private void checkHasAtLeastOneOutput() {
+ if (null == mOutputBinary && null == mOutputXml) {
+ throw new RuntimeException("No output specified");
+ }
+ }
+
+ private void displayHelp() {
+ MakedictLog.i("Usage: makedict2 "
+ + "[-s <unigrams.xml> [-b <bigrams.xml>] | -s <binary input>] "
+ + " [-d <binary output>] [-x <xml output>] [-2]\n"
+ + "\n"
+ + " Converts a source dictionary file to one or several outputs.\n"
+ + " Source can be an XML file, with an optional XML bigrams file, or a\n"
+ + " binary dictionary file.\n"
+ + " Both binary and XML outputs are supported. Both can be output at\n"
+ + " the same time but outputting several files of the same type is not\n"
+ + " supported.");
+ }
+
+ public Arguments(String[] argsArray) {
+ final LinkedList<String> args = new LinkedList<String>(Arrays.asList(argsArray));
+ if (args.isEmpty()) {
+ displayHelp();
+ }
+ String inputBinary = null;
+ String inputUnigramXml = null;
+ String inputBigramXml = null;
+ String outputBinary = null;
+ String outputXml = null;
+
+ while (!args.isEmpty()) {
+ final String arg = args.get(0);
+ args.remove(0);
+ if (arg.charAt(0) == '-') {
+ if (OPTION_VERSION_2.equals(arg)) {
+ // Do nothing, this is the default
+ } else if (OPTION_HELP.equals(arg)) {
+ displayHelp();
+ } else {
+ // All these options need an argument
+ if (args.isEmpty()) {
+ throw new RuntimeException("Option " + arg + " requires an argument");
+ }
+ String filename = args.get(0);
+ args.remove(0);
+ if (OPTION_INPUT_SOURCE.equals(arg)) {
+ if (BinaryDictInputOutput.isBinaryDictionary(filename)) {
+ inputBinary = filename;
+ } else {
+ inputUnigramXml = filename;
+ }
+ } else if (OPTION_INPUT_BIGRAM_XML.equals(arg)) {
+ inputBigramXml = filename;
+ } else if (OPTION_OUTPUT_BINARY.equals(arg)) {
+ outputBinary = filename;
+ } else if (OPTION_OUTPUT_XML.equals(arg)) {
+ outputXml = filename;
+ }
+ }
+ } else {
+ if (null == inputBinary && null == inputUnigramXml) {
+ if (BinaryDictInputOutput.isBinaryDictionary(arg)) {
+ inputBinary = arg;
+ } else {
+ inputUnigramXml = arg;
+ }
+ } else if (null == outputBinary) {
+ outputBinary = arg;
+ } else {
+ throw new RuntimeException("Several output binary files specified");
+ }
+ }
+ }
+
+ mInputBinary = inputBinary;
+ mInputUnigramXml = inputUnigramXml;
+ mInputBigramXml = inputBigramXml;
+ mOutputBinary = outputBinary;
+ mOutputXml = outputXml;
+ checkIntegrity();
+ }
+ }
+
+ public static void main(String[] args)
+ throws FileNotFoundException, ParserConfigurationException, SAXException, IOException,
+ UnsupportedFormatException {
+ final Arguments parsedArgs = new Arguments(args);
+ FusionDictionary dictionary = readInputFromParsedArgs(parsedArgs);
+ writeOutputToParsedArgs(parsedArgs, dictionary);
+ }
+
+ /**
+ * Invoke the right input method according to args.
+ *
+ * @param args the parsed command line arguments.
+ * @return the read dictionary.
+ */
+ private static FusionDictionary readInputFromParsedArgs(final Arguments args)
+ throws IOException, UnsupportedFormatException, ParserConfigurationException,
+ SAXException, FileNotFoundException {
+ if (null != args.mInputBinary) {
+ return readBinaryFile(args.mInputBinary);
+ } else if (null != args.mInputUnigramXml) {
+ return readXmlFile(args.mInputUnigramXml, args.mInputBigramXml);
+ } else {
+ throw new RuntimeException("No input file specified");
+ }
+ }
+
+ /**
+ * Read a dictionary from the name of a binary file.
+ *
+ * @param binaryFilename the name of the file in the binary dictionary format.
+ * @return the read dictionary.
+ * @throws FileNotFoundException if the file can't be found
+ * @throws IOException if the input file can't be read
+ * @throws UnsupportedFormatException if the binary file is not in the expected format
+ */
+ private static FusionDictionary readBinaryFile(final String binaryFilename)
+ throws FileNotFoundException, IOException, UnsupportedFormatException {
+ final RandomAccessFile inputFile = new RandomAccessFile(binaryFilename, "r");
+ return BinaryDictInputOutput.readDictionaryBinary(inputFile, null);
+ }
+
+ /**
+ * Read a dictionary from a unigram XML file, and optionally a bigram XML file.
+ *
+ * @param unigramXmlFilename the name of the unigram XML file. May not be null.
+ * @param bigramXmlFilename the name of the bigram XML file. Pass null if there are no bigrams.
+ * @return the read dictionary.
+ * @throws FileNotFoundException if one of the files can't be found
+ * @throws SAXException if one or more of the XML files is not well-formed
+ * @throws IOException if one the input files can't be read
+ * @throws ParserConfigurationException if the system can't create a SAX parser
+ */
+ private static FusionDictionary readXmlFile(final String unigramXmlFilename,
+ final String bigramXmlFilename) throws FileNotFoundException, SAXException,
+ IOException, ParserConfigurationException {
+ final FileInputStream unigrams = new FileInputStream(new File(unigramXmlFilename));
+ final FileInputStream bigrams = null == bigramXmlFilename ? null :
+ new FileInputStream(new File(bigramXmlFilename));
+ return XmlDictInputOutput.readDictionaryXml(unigrams, bigrams);
+ }
+
+ /**
+ * Invoke the right output method according to args.
+ *
+ * This will write the passed dictionary to the file(s) passed in the command line arguments.
+ * @param args the parsed arguments.
+ * @param dict the file to output.
+ * @throws FileNotFoundException if one of the output files can't be created.
+ * @throws IOException if one of the output files can't be written to.
+ */
+ private static void writeOutputToParsedArgs(final Arguments args, final FusionDictionary dict)
+ throws FileNotFoundException, IOException {
+ if (null != args.mOutputBinary) {
+ writeBinaryDictionary(args.mOutputBinary, dict);
+ }
+ if (null != args.mOutputXml) {
+ writeXmlDictionary(args.mOutputXml, dict);
+ }
+ }
+
+ /**
+ * Write the dictionary in binary format to the specified filename.
+ *
+ * @param outputFilename the name of the file to write to.
+ * @param dict the dictionary to write.
+ * @throws FileNotFoundException if the output file can't be created.
+ * @throws IOException if the output file can't be written to.
+ */
+ private static void writeBinaryDictionary(final String outputFilename,
+ final FusionDictionary dict) throws FileNotFoundException, IOException {
+ final File outputFile = new File(outputFilename);
+ BinaryDictInputOutput.writeDictionaryBinary(new FileOutputStream(outputFilename), dict);
+ }
+
+ /**
+ * Write the dictionary in XML format to the specified filename.
+ *
+ * @param outputFilename the name of the file to write to.
+ * @param dict the dictionary to write.
+ * @throws FileNotFoundException if the output file can't be created.
+ * @throws IOException if the output file can't be written to.
+ */
+ private static void writeXmlDictionary(final String outputFilename,
+ final FusionDictionary dict) throws FileNotFoundException, IOException {
+ XmlDictInputOutput.writeDictionaryXml(new FileWriter(outputFilename), dict);
+ }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/FusionDictionary.java b/tools/makedict2/src/com/android/inputmethod/latin/FusionDictionary.java
new file mode 100644
index 000000000..031f35dfe
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/FusionDictionary.java
@@ -0,0 +1,602 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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 com.android.inputmethod.latin;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A dictionary that can fusion heads and tails of words for more compression.
+ */
+public class FusionDictionary implements Iterable<Word> {
+
+ /**
+ * A node of the dictionary, containing several CharGroups.
+ *
+ * A node is but an ordered array of CharGroups, which essentially contain all the
+ * real information.
+ * This class also contains fields to cache size and address, to help with binary
+ * generation.
+ */
+ public static class Node {
+ ArrayList<CharGroup> mData;
+ // To help with binary generation
+ int mCachedSize;
+ int mCachedAddress;
+ public Node() {
+ mData = new ArrayList<CharGroup>();
+ mCachedSize = Integer.MIN_VALUE;
+ mCachedAddress = Integer.MIN_VALUE;
+ }
+ public Node(ArrayList<CharGroup> data) {
+ mData = data;
+ mCachedSize = Integer.MIN_VALUE;
+ mCachedAddress = Integer.MIN_VALUE;
+ }
+ }
+
+ /**
+ * A string with a frequency.
+ *
+ * This represents an "attribute", that is either a bigram or a shortcut.
+ */
+ public static class WeightedString {
+ final String mWord;
+ final int mFrequency;
+ public WeightedString(String word, int frequency) {
+ mWord = word;
+ mFrequency = frequency;
+ }
+ }
+
+ /**
+ * A group of characters, with a frequency, shortcuts, bigrams, and children.
+ *
+ * This is the central class of the in-memory representation. A CharGroup is what can
+ * be seen as a traditional "trie node", except it can hold several characters at the
+ * same time. A CharGroup essentially represents one or several characters in the middle
+ * of the trie trie; as such, it can be a terminal, and it can have children.
+ * In this in-memory representation, whether the CharGroup is a terminal or not is represented
+ * in the frequency, where NOT_A_TERMINAL (= -1) means this is not a terminal and any other
+ * value is the frequency of this terminal. A terminal may have non-null shortcuts and/or
+ * bigrams, but a non-terminal may not. Moreover, children, if present, are null.
+ */
+ public static class CharGroup {
+ public static final int NOT_A_TERMINAL = -1;
+ final int mChars[];
+ final ArrayList<WeightedString> mBigrams;
+ final int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal.
+ Node mChildren;
+ // The two following members to help with binary generation
+ int mCachedSize;
+ int mCachedAddress;
+
+ public CharGroup(final int[] chars,
+ final ArrayList<WeightedString> bigrams, final int frequency) {
+ mChars = chars;
+ mFrequency = frequency;
+ mBigrams = bigrams;
+ mChildren = null;
+ }
+
+ public CharGroup(final int[] chars,
+ final ArrayList<WeightedString> bigrams, final int frequency, final Node children) {
+ mChars = chars;
+ mFrequency = frequency;
+ mBigrams = bigrams;
+ mChildren = children;
+ }
+
+ public void addChild(CharGroup n) {
+ if (null == mChildren) {
+ mChildren = new Node();
+ }
+ mChildren.mData.add(n);
+ }
+
+ public boolean isTerminal() {
+ return NOT_A_TERMINAL != mFrequency;
+ }
+
+ public boolean hasSeveralChars() {
+ assert(mChars.length > 0);
+ return 1 < mChars.length;
+ }
+ }
+
+ /**
+ * Options global to the dictionary.
+ *
+ * There are no options at the moment, so this class is empty.
+ */
+ public static class DictionaryOptions {
+ }
+
+
+ public final DictionaryOptions mOptions;
+ public final Node mRoot;
+
+ public FusionDictionary() {
+ mOptions = new DictionaryOptions();
+ mRoot = new Node();
+ }
+
+ public FusionDictionary(final Node root, final DictionaryOptions options) {
+ mRoot = root;
+ mOptions = options;
+ }
+
+ /**
+ * Helper method to convert a String to an int array.
+ */
+ static private int[] getCodePoints(String word) {
+ final int wordLength = word.length();
+ int[] array = new int[word.codePointCount(0, wordLength)];
+ for (int i = 0; i < wordLength; ++i) {
+ array[i] = word.codePointAt(i);
+ }
+ return array;
+ }
+
+ /**
+ * Helper method to add a word as a string.
+ *
+ * This method adds a word to the dictionary with the given frequency. Optional
+ * lists of bigrams and shortcuts can be passed here. For each word inside,
+ * they will be added to the dictionary as necessary.
+ *
+ * @param word the word to add.
+ * @param frequency the frequency of the word, in the range [0..255].
+ * @param bigrams a list of bigrams, or null.
+ */
+ public void add(String word, int frequency, ArrayList<WeightedString> bigrams) {
+ if (null != bigrams) {
+ for (WeightedString bigram : bigrams) {
+ final CharGroup t = findWordInTree(mRoot, bigram.mWord);
+ if (null == t) {
+ add(getCodePoints(bigram.mWord), 0, null);
+ }
+ }
+ }
+ add(getCodePoints(word), frequency, bigrams);
+ }
+
+ /**
+ * Sanity check for a node.
+ *
+ * This method checks that all CharGroups in a node are ordered as expected.
+ * If they are, nothing happens. If they aren't, an exception is thrown.
+ */
+ private void checkStack(Node node) {
+ ArrayList<CharGroup> stack = node.mData;
+ int lastValue = -1;
+ for (int i = 0; i < stack.size(); ++i) {
+ int currentValue = stack.get(i).mChars[0];
+ if (currentValue <= lastValue)
+ throw new RuntimeException("Invalid stack");
+ else
+ lastValue = currentValue;
+ }
+ }
+
+ /**
+ * Add a word to this dictionary.
+ *
+ * The bigrams, if any, have to be in the dictionary already. If they aren't,
+ * an exception is thrown.
+ *
+ * @param word the word, as an int array.
+ * @param frequency the frequency of the word, in the range [0..255].
+ * @param bigrams an optional list of bigrams for this word (null if none).
+ */
+ private void add(int[] word, int frequency, ArrayList<WeightedString> bigrams) {
+ assert(frequency >= 0 && frequency <= 255);
+ Node currentNode = mRoot;
+ int charIndex = 0;
+
+ CharGroup currentGroup = null;
+ int differentCharIndex = 0; // Set by the loop to the index of the char that differs
+ int nodeIndex = findIndexOfChar(mRoot, word[charIndex]);
+ while (CHARACTER_NOT_FOUND != nodeIndex) {
+ currentGroup = currentNode.mData.get(nodeIndex);
+ differentCharIndex = compareArrays(currentGroup.mChars, word, charIndex) ;
+ if (ARRAYS_ARE_EQUAL != differentCharIndex
+ && differentCharIndex < currentGroup.mChars.length) break;
+ if (null == currentGroup.mChildren) break;
+ charIndex += currentGroup.mChars.length;
+ if (charIndex >= word.length) break;
+ currentNode = currentGroup.mChildren;
+ nodeIndex = findIndexOfChar(currentNode, word[charIndex]);
+ }
+
+ if (-1 == nodeIndex) {
+ // No node at this point to accept the word. Create one.
+ final int insertionIndex = findInsertionIndex(currentNode, word[charIndex]);
+ final CharGroup newGroup = new CharGroup(
+ Arrays.copyOfRange(word, charIndex, word.length), bigrams, frequency);
+ currentNode.mData.add(insertionIndex, newGroup);
+ checkStack(currentNode);
+ } else {
+ // There is a word with a common prefix.
+ if (differentCharIndex == currentGroup.mChars.length) {
+ if (charIndex + differentCharIndex >= word.length) {
+ // The new word is a prefix of an existing word, but the node on which it
+ // should end already exists as is.
+ if (currentGroup.mFrequency > 0) {
+ throw new RuntimeException("Such a word already exists in the dictionary : "
+ + new String(word, 0, word.length));
+ } else {
+ final CharGroup newNode = new CharGroup(currentGroup.mChars,
+ bigrams, frequency, currentGroup.mChildren);
+ currentNode.mData.set(nodeIndex, newNode);
+ checkStack(currentNode);
+ }
+ } else {
+ // The new word matches the full old word and extends past it.
+ // We only have to create a new node and add it to the end of this.
+ final CharGroup newNode = new CharGroup(
+ Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length),
+ bigrams, frequency);
+ currentGroup.mChildren = new Node();
+ currentGroup.mChildren.mData.add(newNode);
+ }
+ } else {
+ if (0 == differentCharIndex) {
+ // Exact same word. Check the frequency is 0 or -1, and update.
+ if (0 != frequency) {
+ if (0 < currentGroup.mFrequency) {
+ throw new RuntimeException("This word already exists with frequency "
+ + currentGroup.mFrequency + " : "
+ + new String(word, 0, word.length));
+ }
+ final CharGroup newGroup = new CharGroup(word,
+ currentGroup.mBigrams, frequency);
+ currentNode.mData.set(nodeIndex, newGroup);
+ }
+ } else {
+ // Partial prefix match only. We have to replace the current node with a node
+ // containing the current prefix and create two new ones for the tails.
+ Node newChildren = new Node();
+ final CharGroup newOldWord = new CharGroup(
+ Arrays.copyOfRange(currentGroup.mChars, differentCharIndex,
+ currentGroup.mChars.length),
+ currentGroup.mBigrams, currentGroup.mFrequency, currentGroup.mChildren);
+ newChildren.mData.add(newOldWord);
+
+ final CharGroup newParent;
+ if (charIndex + differentCharIndex >= word.length) {
+ newParent = new CharGroup(
+ Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
+ bigrams, frequency, newChildren);
+ } else {
+ newParent = new CharGroup(
+ Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
+ null, -1, newChildren);
+ final CharGroup newWord = new CharGroup(
+ Arrays.copyOfRange(word, charIndex + differentCharIndex,
+ word.length), bigrams, frequency);
+ final int addIndex = word[charIndex + differentCharIndex]
+ > currentGroup.mChars[differentCharIndex] ? 1 : 0;
+ newChildren.mData.add(addIndex, newWord);
+ }
+ currentNode.mData.set(nodeIndex, newParent);
+ }
+ checkStack(currentNode);
+ }
+ }
+ }
+
+ /**
+ * Custom comparison of two int arrays taken to contain character codes.
+ *
+ * This method compares the two arrays passed as an argument in a lexicographic way,
+ * with an offset in the dst string.
+ * This method does NOT test for the first character. It is taken to be equal.
+ * I repeat: this method starts the comparison at 1 <> dstOffset + 1.
+ * The index where the strings differ is returned. ARRAYS_ARE_EQUAL = 0 is returned if the
+ * strings are equal. This works BECAUSE we don't look at the first character.
+ *
+ * @param src the left-hand side string of the comparison.
+ * @param dst the right-hand side string of the comparison.
+ * @param dstOffset the offset in the right-hand side string.
+ * @return the index at which the strings differ, or ARRAYS_ARE_EQUAL = 0 if they don't.
+ */
+ private static int ARRAYS_ARE_EQUAL = 0;
+ private static int compareArrays(final int[] src, final int[] dst, int dstOffset) {
+ // We do NOT test the first char, because we come from a method that already
+ // tested it.
+ for (int i = 1; i < src.length; ++i) {
+ if (dstOffset + i >= dst.length) return i;
+ if (src[i] != dst[dstOffset + i]) return i;
+ }
+ if (dst.length > src.length) return src.length;
+ return ARRAYS_ARE_EQUAL;
+ }
+
+ /**
+ * Helper class that compares and sorts two chargroups according to their
+ * first element only. I repeat: ONLY the first element is considered, the rest
+ * is ignored.
+ * This comparator imposes orderings that are inconsistent with equals.
+ */
+ static private class CharGroupComparator implements java.util.Comparator {
+ public int compare(Object o1, Object o2) {
+ final CharGroup c1 = (CharGroup)o1;
+ final CharGroup c2 = (CharGroup)o2;
+ if (c1.mChars[0] == c2.mChars[0]) return 0;
+ return c1.mChars[0] < c2.mChars[0] ? -1 : 1;
+ }
+ public boolean equals(Object o) {
+ return o instanceof CharGroupComparator;
+ }
+ }
+ final static private CharGroupComparator CHARGROUP_COMPARATOR = new CharGroupComparator();
+
+ /**
+ * Finds the insertion index of a character within a node.
+ */
+ private static int findInsertionIndex(final Node node, int character) {
+ final List data = node.mData;
+ final CharGroup reference = new CharGroup(new int[] { character }, null, 0);
+ int result = Collections.binarySearch(data, reference, CHARGROUP_COMPARATOR);
+ return result >= 0 ? result : -result - 1;
+ }
+
+ /**
+ * Find the index of a char in a node, if it exists.
+ *
+ * @param node the node to search in.
+ * @param character the character to search for.
+ * @return the position of the character if it's there, or CHARACTER_NOT_FOUND = -1 else.
+ */
+ private static int CHARACTER_NOT_FOUND = -1;
+ private static int findIndexOfChar(final Node node, int character) {
+ final int insertionIndex = findInsertionIndex(node, character);
+ if (node.mData.size() <= insertionIndex) return CHARACTER_NOT_FOUND;
+ return character == node.mData.get(insertionIndex).mChars[0] ? insertionIndex
+ : CHARACTER_NOT_FOUND;
+ }
+
+ /**
+ * Helper method to find a word in a given branch.
+ */
+ public static CharGroup findWordInTree(Node node, final String s) {
+ int index = 0;
+ final StringBuilder checker = new StringBuilder();
+
+ CharGroup currentGroup;
+ do {
+ int indexOfGroup = findIndexOfChar(node, s.codePointAt(index));
+ if (CHARACTER_NOT_FOUND == indexOfGroup) return null;
+ currentGroup = node.mData.get(indexOfGroup);
+ checker.append(new String(currentGroup.mChars, 0, currentGroup.mChars.length));
+ index += currentGroup.mChars.length;
+ if (index < s.length()) {
+ node = currentGroup.mChildren;
+ }
+ } while (null != node && index < s.length());
+
+ if (!s.equals(checker.toString())) return null;
+ return currentGroup;
+ }
+
+ /**
+ * Recursively count the number of character groups in a given branch of the trie.
+ *
+ * @param node the parent node.
+ * @return the number of char groups in all the branch under this node.
+ */
+ public static int countCharGroups(final Node node) {
+ final int nodeSize = node.mData.size();
+ int size = nodeSize;
+ for (int i = nodeSize - 1; i >= 0; --i) {
+ CharGroup group = node.mData.get(i);
+ if (null != group.mChildren)
+ size += countCharGroups(group.mChildren);
+ }
+ return size;
+ }
+
+ /**
+ * Recursively count the number of nodes in a given branch of the trie.
+ *
+ * @param node the node to count.
+ * @result the number of nodes in this branch.
+ */
+ public static int countNodes(final Node node) {
+ int size = 1;
+ for (int i = node.mData.size() - 1; i >= 0; --i) {
+ CharGroup group = node.mData.get(i);
+ if (null != group.mChildren)
+ size += countNodes(group.mChildren);
+ }
+ return size;
+ }
+
+ // Historically, the tails of the words were going to be merged to save space.
+ // However, that would prevent the code to search for a specific address in log(n)
+ // time so this was abandoned.
+ // The code is still of interest as it does add some compression to any dictionary
+ // that has no need for attributes. Implementations that does not read attributes should be
+ // able to read a dictionary with merged tails.
+ // Also, the following code does support frequencies, as in, it will only merges
+ // tails that share the same frequency. Though it would result in the above loss of
+ // performance while searching by address, it is still technically possible to merge
+ // tails that contain attributes, but this code does not take that into account - it does
+ // not compare attributes and will merge terminals with different attributes regardless.
+ public void mergeTails() {
+ MakedictLog.i("Do not merge tails");
+ return;
+
+// MakedictLog.i("Merging nodes. Number of nodes : " + countNodes(root));
+// MakedictLog.i("Number of groups : " + countCharGroups(root));
+//
+// final HashMap<String, ArrayList<Node>> repository =
+// new HashMap<String, ArrayList<Node>>();
+// mergeTailsInner(repository, root);
+//
+// MakedictLog.i("Number of different pseudohashes : " + repository.size());
+// int size = 0;
+// for (ArrayList<Node> a : repository.values()) {
+// size += a.size();
+// }
+// MakedictLog.i("Number of nodes after merge : " + (1 + size));
+// MakedictLog.i("Recursively seen nodes : " + countNodes(root));
+ }
+
+ // The following methods are used by the deactivated mergeTails()
+// private static boolean isEqual(Node a, Node b) {
+// if (null == a && null == b) return true;
+// if (null == a || null == b) return false;
+// if (a.data.size() != b.data.size()) return false;
+// final int size = a.data.size();
+// for (int i = size - 1; i >= 0; --i) {
+// CharGroup aGroup = a.data.get(i);
+// CharGroup bGroup = b.data.get(i);
+// if (aGroup.frequency != bGroup.frequency) return false;
+// if (aGroup.alternates == null && bGroup.alternates != null) return false;
+// if (aGroup.alternates != null && !aGroup.equals(bGroup.alternates)) return false;
+// if (!Arrays.equals(aGroup.chars, bGroup.chars)) return false;
+// if (!isEqual(aGroup.children, bGroup.children)) return false;
+// }
+// return true;
+// }
+
+// static private HashMap<String, ArrayList<Node>> mergeTailsInner(
+// final HashMap<String, ArrayList<Node>> map, final Node node) {
+// final ArrayList<CharGroup> branches = node.data;
+// final int nodeSize = branches.size();
+// for (int i = 0; i < nodeSize; ++i) {
+// CharGroup group = branches.get(i);
+// if (null != group.children) {
+// String pseudoHash = getPseudoHash(group.children);
+// ArrayList<Node> similarList = map.get(pseudoHash);
+// if (null == similarList) {
+// similarList = new ArrayList<Node>();
+// map.put(pseudoHash, similarList);
+// }
+// boolean merged = false;
+// for (Node similar : similarList) {
+// if (isEqual(group.children, similar)) {
+// group.children = similar;
+// merged = true;
+// break;
+// }
+// }
+// if (!merged) {
+// similarList.add(group.children);
+// }
+// mergeTailsInner(map, group.children);
+// }
+// }
+// return map;
+// }
+
+// private static String getPseudoHash(final Node node) {
+// StringBuilder s = new StringBuilder();
+// for (CharGroup g : node.data) {
+// s.append(g.frequency);
+// for (int ch : g.chars){
+// s.append(Character.toChars(ch));
+// }
+// }
+// return s.toString();
+// }
+
+ /**
+ * Iterator to walk through a dictionary.
+ *
+ * This is purely for convenience.
+ */
+ public static class DictionaryIterator implements Iterator<Word> {
+
+ private static class Position {
+ public Iterator<CharGroup> pos;
+ public int length;
+ public Position(ArrayList<CharGroup> groups) {
+ pos = groups.iterator();
+ length = 0;
+ }
+ }
+ final StringBuilder mCurrentString;
+ final LinkedList<Position> mPositions;
+
+ public DictionaryIterator(ArrayList<CharGroup> root) {
+ mCurrentString = new StringBuilder();
+ mPositions = new LinkedList<Position>();
+ final Position rootPos = new Position(root);
+ mPositions.add(rootPos);
+ }
+
+ @Override
+ public boolean hasNext() {
+ for (Position p : mPositions) {
+ if (p.pos.hasNext()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Word next() {
+ Position currentPos = mPositions.getLast();
+ mCurrentString.setLength(mCurrentString.length() - currentPos.length);
+
+ do {
+ if (currentPos.pos.hasNext()) {
+ final CharGroup currentGroup = currentPos.pos.next();
+ currentPos.length = currentGroup.mChars.length;
+ for (int i : currentGroup.mChars)
+ mCurrentString.append(Character.toChars(i));
+ if (null != currentGroup.mChildren) {
+ currentPos = new Position(currentGroup.mChildren.mData);
+ mPositions.addLast(currentPos);
+ }
+ if (currentGroup.mFrequency >= 0)
+ return new Word(mCurrentString.toString(), currentGroup.mFrequency,
+ currentGroup.mBigrams);
+ } else {
+ mPositions.removeLast();
+ currentPos = mPositions.getLast();
+ mCurrentString.setLength(mCurrentString.length() - mPositions.getLast().length);
+ }
+ } while(true);
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("Unsupported yet");
+ }
+
+ }
+
+ /**
+ * Method to return an iterator.
+ *
+ * This method enables Java's enhanced for loop. With this you can have a FusionDictionary x
+ * and say : for (Word w : x) {}
+ */
+ @Override
+ public Iterator<Word> iterator() {
+ return new DictionaryIterator(mRoot.mData);
+ }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/MakedictLog.java b/tools/makedict2/src/com/android/inputmethod/latin/MakedictLog.java
new file mode 100644
index 000000000..badb2ff3d
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/MakedictLog.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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 com.android.inputmethod.latin;
+
+/**
+ * Wrapper to redirect log events to the right output medium.
+ */
+public class MakedictLog {
+
+ private static void print(String message) {
+ System.out.println(message);
+ }
+
+ public static void d(String message) {
+ print(message);
+ }
+ public static void e(String message) {
+ print(message);
+ }
+ public static void i(String message) {
+ print(message);
+ }
+ public static void w(String message) {
+ print(message);
+ }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/PendingAttribute.java b/tools/makedict2/src/com/android/inputmethod/latin/PendingAttribute.java
new file mode 100644
index 000000000..e50202184
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/PendingAttribute.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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 com.android.inputmethod.latin;
+
+/**
+ * A not-yet-resolved attribute.
+ *
+ * An attribute is either a bigram or an shortcut.
+ * All instances of this class are always immutable.
+ */
+public class PendingAttribute {
+ public final int mFrequency;
+ public final int mAddress;
+ public PendingAttribute(final int frequency, final int address) {
+ mFrequency = frequency;
+ mAddress = address;
+ }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/UnsupportedFormatException.java b/tools/makedict2/src/com/android/inputmethod/latin/UnsupportedFormatException.java
new file mode 100644
index 000000000..511ad5675
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/UnsupportedFormatException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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 com.android.inputmethod.latin;
+
+/**
+ * Simple exception thrown when a file format is not recognized.
+ */
+public class UnsupportedFormatException extends Exception {
+ public UnsupportedFormatException(String description) {
+ super(description);
+ }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/Word.java b/tools/makedict2/src/com/android/inputmethod/latin/Word.java
new file mode 100644
index 000000000..916165a41
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/Word.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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 com.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.FusionDictionary.WeightedString;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class for a word with a frequency.
+ *
+ * This is chiefly used to iterate a dictionary.
+ */
+public class Word implements Comparable<Word> {
+ final String mWord;
+ final int mFrequency;
+ final ArrayList<WeightedString> mBigrams;
+
+ public Word(String word, int frequency, ArrayList<WeightedString> bigrams) {
+ mWord = word;
+ mFrequency = frequency;
+ mBigrams = bigrams;
+ }
+
+ /**
+ * Three-way comparison.
+ *
+ * A Word x is greater than a word y if x has a higher frequency. If they have the same
+ * frequency, they are sorted in lexicographic order.
+ */
+ @Override
+ public int compareTo(Word w) {
+ if (mFrequency < w.mFrequency) return 1;
+ if (mFrequency > w.mFrequency) return -1;
+ return mWord.compareTo(w.mWord);
+ }
+
+ /**
+ * Equality test.
+ *
+ * Words are equal if they have the same frequency, the same spellings, and the same
+ * attributes.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Word)) return false;
+ Word w = (Word)o;
+ return mFrequency == w.mFrequency && mWord.equals(w.mWord)
+ && mBigrams.equals(w.mBigrams);
+ }
+}
diff --git a/tools/makedict2/src/com/android/inputmethod/latin/XmlDictInputOutput.java b/tools/makedict2/src/com/android/inputmethod/latin/XmlDictInputOutput.java
new file mode 100644
index 000000000..096bfd182
--- /dev/null
+++ b/tools/makedict2/src/com/android/inputmethod/latin/XmlDictInputOutput.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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 com.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.FusionDictionary.WeightedString;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.TreeSet;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Reads and writes XML files for a FusionDictionary.
+ *
+ * All functions in this class are static.
+ */
+public class XmlDictInputOutput {
+
+ private static final String WORD_TAG = "w";
+ private static final String BIGRAM_TAG = "bigram";
+ private static final String FREQUENCY_ATTR = "f";
+ private static final String WORD_ATTR = "word";
+
+ /**
+ * SAX handler for a unigram XML file.
+ */
+ static private class UnigramHandler extends DefaultHandler {
+ // Parser states
+ private static final int NONE = 0;
+ private static final int START = 1;
+ private static final int WORD = 2;
+ private static final int BIGRAM = 4;
+ private static final int END = 5;
+ private static final int UNKNOWN = 6;
+
+ final FusionDictionary mDictionary;
+ int mState; // the state of the parser
+ int mFreq; // the currently read freq
+ final HashMap<String, ArrayList<WeightedString>> mBigramsMap;
+
+ /**
+ * Create the handler.
+ *
+ * @param dict the dictionary to construct.
+ * @param bigrams the bigrams as a map. This may be empty, but may not be null.
+ */
+ public UnigramHandler(FusionDictionary dict,
+ HashMap<String, ArrayList<WeightedString>> bigrams) {
+ mDictionary = dict;
+ mBigramsMap = bigrams;
+ mState = START;
+ mFreq = 0;
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attrs) {
+ if (WORD_TAG.equals(localName)) {
+ mState = WORD;
+ for (int attrIndex = 0; attrIndex < attrs.getLength(); ++attrIndex) {
+ final String attrName = attrs.getLocalName(attrIndex);
+ if (FREQUENCY_ATTR.equals(attrName)) {
+ mFreq = Integer.parseInt(attrs.getValue(attrIndex));
+ }
+ }
+ } else {
+ mState = UNKNOWN;
+ }
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) {
+ if (WORD == mState) {
+ final String word = String.copyValueOf(ch, start, length);
+ mDictionary.add(word, mFreq, mBigramsMap.get(word));
+ }
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) {
+ if (WORD == mState) mState = START;
+ }
+ }
+
+ /**
+ * SAX handler for a bigram XML file.
+ */
+ static private class BigramHandler extends DefaultHandler {
+ private final static String BIGRAM_W1_TAG = "bi";
+ private final static String BIGRAM_W2_TAG = "w";
+ private final static String BIGRAM_W1_ATTRIBUTE = "w1";
+ private final static String BIGRAM_W2_ATTRIBUTE = "w2";
+ private final static String BIGRAM_FREQ_ATTRIBUTE = "p";
+
+ String mW1;
+ final HashMap<String, ArrayList<WeightedString>> mBigramsMap;
+
+ public BigramHandler() {
+ mW1 = null;
+ mBigramsMap = new HashMap<String, ArrayList<WeightedString>>();
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attrs) {
+ if (BIGRAM_W1_TAG.equals(localName)) {
+ mW1 = attrs.getValue(uri, BIGRAM_W1_ATTRIBUTE);
+ } else if (BIGRAM_W2_TAG.equals(localName)) {
+ String w2 = attrs.getValue(uri, BIGRAM_W2_ATTRIBUTE);
+ int freq = Integer.parseInt(attrs.getValue(uri, BIGRAM_FREQ_ATTRIBUTE));
+ WeightedString bigram = new WeightedString(w2, freq / 8);
+ ArrayList<WeightedString> bigramList = mBigramsMap.get(mW1);
+ if (null == bigramList) bigramList = new ArrayList<WeightedString>();
+ bigramList.add(bigram);
+ mBigramsMap.put(mW1, bigramList);
+ }
+ }
+
+ public HashMap<String, ArrayList<WeightedString>> getBigramMap() {
+ return mBigramsMap;
+ }
+ }
+
+ /**
+ * Reads a dictionary from an XML file.
+ *
+ * This is the public method that will parse an XML file and return the corresponding memory
+ * representation.
+ *
+ * @param unigrams the file to read the data from.
+ * @return the in-memory representation of the dictionary.
+ */
+ public static FusionDictionary readDictionaryXml(InputStream unigrams, InputStream bigrams)
+ throws SAXException, IOException, ParserConfigurationException {
+ final SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ final SAXParser parser = factory.newSAXParser();
+ final BigramHandler bigramHandler = new BigramHandler();
+ if (null != bigrams) parser.parse(bigrams, bigramHandler);
+
+ final FusionDictionary dict = new FusionDictionary();
+ final UnigramHandler unigramHandler =
+ new UnigramHandler(dict, bigramHandler.getBigramMap());
+ parser.parse(unigrams, unigramHandler);
+ return dict;
+ }
+
+ /**
+ * Reads a dictionary in the first, legacy XML format
+ *
+ * This method reads data from the parser and creates a new FusionDictionary with it.
+ * The format parsed by this method is the format used before Ice Cream Sandwich,
+ * which has no support for bigrams or shortcuts.
+ * It is important to note that this method expects the parser to have already eaten
+ * the first, all-encompassing tag.
+ *
+ * @param xpp the parser to read the data from.
+ * @return the parsed dictionary.
+ */
+
+ /**
+ * Writes a dictionary to an XML file.
+ *
+ * The output format is the "second" format, which supports bigrams and shortcuts.
+ *
+ * @param destination a destination stream to write to.
+ * @param dict the dictionary to write.
+ */
+ public static void writeDictionaryXml(Writer destination, FusionDictionary dict)
+ throws IOException {
+ final TreeSet<Word> set = new TreeSet<Word>();
+ for (Word word : dict) {
+ set.add(word);
+ }
+ // TODO: use an XMLSerializer if this gets big
+ destination.write("<wordlist format=\"2\">\n");
+ for (Word word : set) {
+ destination.write(" <" + WORD_TAG + " " + WORD_ATTR + "=\"" + word.mWord + "\" "
+ + FREQUENCY_ATTR + "=\"" + word.mFrequency + "\">");
+ if (null != word.mBigrams) {
+ destination.write("\n");
+ for (WeightedString bigram : word.mBigrams) {
+ destination.write(" <" + BIGRAM_TAG + " " + FREQUENCY_ATTR + "=\""
+ + bigram.mFrequency + "\">" + bigram.mWord + "</" + BIGRAM_TAG + ">\n");
+ }
+ destination.write(" ");
+ }
+ destination.write("</" + WORD_TAG + ">\n");
+ }
+ destination.write("</wordlist>\n");
+ destination.close();
+ }
+}
diff --git a/tools/makedict2/tests/com/android/inputmethod/latin/BinaryDictInputOutputTest.java b/tools/makedict2/tests/com/android/inputmethod/latin/BinaryDictInputOutputTest.java
new file mode 100644
index 000000000..79cf14b2b
--- /dev/null
+++ b/tools/makedict2/tests/com/android/inputmethod/latin/BinaryDictInputOutputTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * 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 com.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.FusionDictionary.Node;
+
+import java.util.ArrayList;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for BinaryDictInputOutput.
+ */
+public class BinaryDictInputOutputTest extends TestCase {
+
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ // Test the flattened array contains the expected number of nodes, and
+ // that it does not contain any duplicates.
+ public void testFlattenNodes() {
+ final FusionDictionary dict = new FusionDictionary();
+ dict.add("foo", 1, null);
+ dict.add("fta", 1, null);
+ dict.add("ftb", 1, null);
+ dict.add("bar", 1, null);
+ dict.add("fool", 1, null);
+ final ArrayList<Node> result = BinaryDictInputOutput.flattenTree(dict.mRoot);
+ assertEquals(4, result.size());
+ while (!result.isEmpty()) {
+ final Node n = result.remove(0);
+ assertFalse("Flattened array contained the same node twice", result.contains(n));
+ }
+ }
+
+}