BinaryDictionary.java revision 75715f7d9fb0b19c2ddaf73bf62148c7d19f0a99
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.latin;
18
19import android.text.TextUtils;
20import android.util.Log;
21import android.util.SparseArray;
22
23import com.android.inputmethod.annotations.UsedForTesting;
24import com.android.inputmethod.keyboard.ProximityInfo;
25import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
26import com.android.inputmethod.latin.makedict.DictionaryHeader;
27import com.android.inputmethod.latin.makedict.FormatSpec;
28import com.android.inputmethod.latin.makedict.FormatSpec.DictionaryOptions;
29import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
30import com.android.inputmethod.latin.makedict.WordProperty;
31import com.android.inputmethod.latin.personalization.PersonalizationHelper;
32import com.android.inputmethod.latin.settings.NativeSuggestOptions;
33import com.android.inputmethod.latin.utils.CollectionUtils;
34import com.android.inputmethod.latin.utils.JniUtils;
35import com.android.inputmethod.latin.utils.LanguageModelParam;
36import com.android.inputmethod.latin.utils.StringUtils;
37
38import java.io.File;
39import java.util.ArrayList;
40import java.util.Arrays;
41import java.util.HashMap;
42import java.util.Locale;
43
44/**
45 * Implements a static, compacted, binary dictionary of standard words.
46 */
47// TODO: All methods which should be locked need to have a suffix "Locked".
48public final class BinaryDictionary extends Dictionary {
49    private static final String TAG = BinaryDictionary.class.getSimpleName();
50
51    // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h
52    private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
53    // Must be equal to MAX_RESULTS in native/jni/src/defines.h
54    private static final int MAX_RESULTS = 18;
55    // The cutoff returned by native for auto-commit confidence.
56    // Must be equal to CONFIDENCE_TO_AUTO_COMMIT in native/jni/src/defines.h
57    private static final int CONFIDENCE_TO_AUTO_COMMIT = 1000000;
58
59    @UsedForTesting
60    public static final String UNIGRAM_COUNT_QUERY = "UNIGRAM_COUNT";
61    @UsedForTesting
62    public static final String BIGRAM_COUNT_QUERY = "BIGRAM_COUNT";
63    @UsedForTesting
64    public static final String MAX_UNIGRAM_COUNT_QUERY = "MAX_UNIGRAM_COUNT";
65    @UsedForTesting
66    public static final String MAX_BIGRAM_COUNT_QUERY = "MAX_BIGRAM_COUNT";
67
68    public static final int NOT_A_VALID_TIMESTAMP = -1;
69
70    // Format to get unigram flags from native side via getWordPropertyNative().
71    private static final int FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT = 4;
72    private static final int FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX = 0;
73    private static final int FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX = 1;
74    private static final int FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX = 2;
75    private static final int FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX = 3;
76
77    // Format to get probability and historical info from native side via getWordPropertyNative().
78    public static final int FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT = 4;
79    public static final int FORMAT_WORD_PROPERTY_PROBABILITY_INDEX = 0;
80    public static final int FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX = 1;
81    public static final int FORMAT_WORD_PROPERTY_LEVEL_INDEX = 2;
82    public static final int FORMAT_WORD_PROPERTY_COUNT_INDEX = 3;
83
84    private long mNativeDict;
85    private final Locale mLocale;
86    private final long mDictSize;
87    private final String mDictFilePath;
88    private final boolean mIsUpdatable;
89    private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH];
90    private final int[] mOutputSuggestionCount = new int[1];
91    private final int[] mOutputCodePoints = new int[MAX_WORD_LENGTH * MAX_RESULTS];
92    private final int[] mSpaceIndices = new int[MAX_RESULTS];
93    private final int[] mOutputScores = new int[MAX_RESULTS];
94    private final int[] mOutputTypes = new int[MAX_RESULTS];
95    // Only one result is ever used
96    private final int[] mOutputAutoCommitFirstWordConfidence = new int[1];
97
98    private final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions();
99
100    private final SparseArray<DicTraverseSession> mDicTraverseSessions =
101            CollectionUtils.newSparseArray();
102
103    // TODO: There should be a way to remove used DicTraverseSession objects from
104    // {@code mDicTraverseSessions}.
105    private DicTraverseSession getTraverseSession(final int traverseSessionId) {
106        synchronized(mDicTraverseSessions) {
107            DicTraverseSession traverseSession = mDicTraverseSessions.get(traverseSessionId);
108            if (traverseSession == null) {
109                traverseSession = mDicTraverseSessions.get(traverseSessionId);
110                if (traverseSession == null) {
111                    traverseSession = new DicTraverseSession(mLocale, mNativeDict, mDictSize);
112                    mDicTraverseSessions.put(traverseSessionId, traverseSession);
113                }
114            }
115            return traverseSession;
116        }
117    }
118
119    /**
120     * Constructor for the binary dictionary. This is supposed to be called from the
121     * dictionary factory.
122     * @param filename the name of the file to read through native code.
123     * @param offset the offset of the dictionary data within the file.
124     * @param length the length of the binary data.
125     * @param useFullEditDistance whether to use the full edit distance in suggestions
126     * @param dictType the dictionary type, as a human-readable string
127     * @param isUpdatable whether to open the dictionary file in writable mode.
128     */
129    public BinaryDictionary(final String filename, final long offset, final long length,
130            final boolean useFullEditDistance, final Locale locale, final String dictType,
131            final boolean isUpdatable) {
132        super(dictType);
133        mLocale = locale;
134        mDictSize = length;
135        mDictFilePath = filename;
136        mIsUpdatable = isUpdatable;
137        mNativeSuggestOptions.setUseFullEditDistance(useFullEditDistance);
138        loadDictionary(filename, offset, length, isUpdatable);
139    }
140
141    static {
142        JniUtils.loadNativeLibrary();
143    }
144
145    private static native long openNative(String sourceDir, long dictOffset, long dictSize,
146            boolean isUpdatable);
147    private static native void getHeaderInfoNative(long dict, int[] outHeaderSize,
148            int[] outFormatVersion, ArrayList<int[]> outAttributeKeys,
149            ArrayList<int[]> outAttributeValues);
150    private static native void flushNative(long dict, String filePath);
151    private static native boolean needsToRunGCNative(long dict, boolean mindsBlockByGC);
152    private static native void flushWithGCNative(long dict, String filePath);
153    private static native void closeNative(long dict);
154    private static native int getFormatVersionNative(long dict);
155    private static native int getProbabilityNative(long dict, int[] word);
156    private static native int getBigramProbabilityNative(long dict, int[] word0, int[] word1);
157    private static native void getWordPropertyNative(long dict, int[] word,
158            int[] outCodePoints, boolean[] outFlags, int[] outProbabilityInfo,
159            ArrayList<int[]> outBigramTargets, ArrayList<int[]> outBigramProbabilityInfo,
160            ArrayList<int[]> outShortcutTargets, ArrayList<Integer> outShortcutProbabilities);
161    private static native int getNextWordNative(long dict, int token, int[] outCodePoints);
162    private static native void getSuggestionsNative(long dict, long proximityInfo,
163            long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times,
164            int[] pointerIds, int[] inputCodePoints, int inputSize, int[] suggestOptions,
165            int[] prevWordCodePointArray, int[] outputSuggestionCount, int[] outputCodePoints,
166            int[] outputScores, int[] outputIndices, int[] outputTypes,
167            int[] outputAutoCommitFirstWordConfidence);
168    private static native void addUnigramWordNative(long dict, int[] word, int probability,
169            int[] shortcutTarget, int shortcutProbability, boolean isNotAWord,
170            boolean isBlacklisted, int timestamp);
171    private static native void addBigramWordsNative(long dict, int[] word0, int[] word1,
172            int probability, int timestamp);
173    private static native void removeBigramWordsNative(long dict, int[] word0, int[] word1);
174    private static native int addMultipleDictionaryEntriesNative(long dict,
175            LanguageModelParam[] languageModelParams, int startIndex);
176    private static native int calculateProbabilityNative(long dict, int unigramProbability,
177            int bigramProbability);
178    private static native String getPropertyNative(long dict, String query);
179    private static native boolean isCorruptedNative(long dict);
180
181    // TODO: Move native dict into session
182    private final void loadDictionary(final String path, final long startOffset,
183            final long length, final boolean isUpdatable) {
184        mNativeDict = openNative(path, startOffset, length, isUpdatable);
185    }
186
187    // TODO: Check isCorrupted() for main dictionaries.
188    public boolean isCorrupted() {
189        if (!isValidDictionary()) {
190            return false;
191        }
192        if (!isCorruptedNative(mNativeDict)) {
193            return false;
194        }
195        // TODO: Record the corruption.
196        Log.e(TAG, "BinaryDictionary (" + mDictFilePath + ") is corrupted.");
197        Log.e(TAG, "locale: " + mLocale);
198        Log.e(TAG, "dict size: " + mDictSize);
199        Log.e(TAG, "updatable: " + mIsUpdatable);
200        return true;
201    }
202
203    public DictionaryHeader getHeader() throws UnsupportedFormatException {
204        if (mNativeDict == 0) {
205            return null;
206        }
207        final int[] outHeaderSize = new int[1];
208        final int[] outFormatVersion = new int[1];
209        final ArrayList<int[]> outAttributeKeys = CollectionUtils.newArrayList();
210        final ArrayList<int[]> outAttributeValues = CollectionUtils.newArrayList();
211        getHeaderInfoNative(mNativeDict, outHeaderSize, outFormatVersion, outAttributeKeys,
212                outAttributeValues);
213        final HashMap<String, String> attributes = new HashMap<String, String>();
214        for (int i = 0; i < outAttributeKeys.size(); i++) {
215            final String attributeKey = StringUtils.getStringFromNullTerminatedCodePointArray(
216                    outAttributeKeys.get(i));
217            final String attributeValue = StringUtils.getStringFromNullTerminatedCodePointArray(
218                    outAttributeValues.get(i));
219            attributes.put(attributeKey, attributeValue);
220        }
221        final boolean hasHistoricalInfo = DictionaryHeader.ATTRIBUTE_VALUE_TRUE.equals(
222                attributes.get(DictionaryHeader.HAS_HISTORICAL_INFO_KEY));
223        return new DictionaryHeader(outHeaderSize[0], new DictionaryOptions(attributes),
224                new FormatSpec.FormatOptions(outFormatVersion[0], hasHistoricalInfo));
225    }
226
227
228    @Override
229    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
230            final String prevWord, final ProximityInfo proximityInfo,
231            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
232        return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords,
233                additionalFeaturesOptions, 0 /* sessionId */);
234    }
235
236    @Override
237    public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
238            final String prevWord, final ProximityInfo proximityInfo,
239            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
240            final int sessionId) {
241        if (!isValidDictionary()) return null;
242
243        Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE);
244        // TODO: toLowerCase in the native code
245        final int[] prevWordCodePointArray = (null == prevWord)
246                ? null : StringUtils.toCodePointArray(prevWord);
247        final int composerSize = composer.size();
248
249        final boolean isGesture = composer.isBatchMode();
250        if (composerSize <= 1 || !isGesture) {
251            if (composerSize > MAX_WORD_LENGTH - 1) return null;
252            for (int i = 0; i < composerSize; i++) {
253                mInputCodePoints[i] = composer.getCodeAt(i);
254            }
255        }
256
257        final InputPointers ips = composer.getInputPointers();
258        final int inputSize = isGesture ? ips.getPointerSize() : composerSize;
259        mNativeSuggestOptions.setIsGesture(isGesture);
260        mNativeSuggestOptions.setAdditionalFeaturesOptions(additionalFeaturesOptions);
261        // proximityInfo and/or prevWordForBigrams may not be null.
262        getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(),
263                getTraverseSession(sessionId).getSession(), ips.getXCoordinates(),
264                ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(), mInputCodePoints,
265                inputSize, mNativeSuggestOptions.getOptions(),
266                prevWordCodePointArray, mOutputSuggestionCount, mOutputCodePoints, mOutputScores,
267                mSpaceIndices, mOutputTypes, mOutputAutoCommitFirstWordConfidence);
268        final int count = mOutputSuggestionCount[0];
269        final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
270        for (int j = 0; j < count; ++j) {
271            final int start = j * MAX_WORD_LENGTH;
272            int len = 0;
273            while (len < MAX_WORD_LENGTH && mOutputCodePoints[start + len] != 0) {
274                ++len;
275            }
276            if (len > 0) {
277                final int flags = mOutputTypes[j] & SuggestedWordInfo.KIND_MASK_FLAGS;
278                if (blockOffensiveWords
279                        && 0 != (flags & SuggestedWordInfo.KIND_FLAG_POSSIBLY_OFFENSIVE)
280                        && 0 == (flags & SuggestedWordInfo.KIND_FLAG_EXACT_MATCH)) {
281                    // If we block potentially offensive words, and if the word is possibly
282                    // offensive, then we don't output it unless it's also an exact match.
283                    continue;
284                }
285                final int kind = mOutputTypes[j] & SuggestedWordInfo.KIND_MASK_KIND;
286                final int score = SuggestedWordInfo.KIND_WHITELIST == kind
287                        ? SuggestedWordInfo.MAX_SCORE : mOutputScores[j];
288                // TODO: check that all users of the `kind' parameter are ready to accept
289                // flags too and pass mOutputTypes[j] instead of kind
290                suggestions.add(new SuggestedWordInfo(new String(mOutputCodePoints, start, len),
291                        score, kind, this /* sourceDict */,
292                        mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */,
293                        mOutputAutoCommitFirstWordConfidence[0]));
294            }
295        }
296        return suggestions;
297    }
298
299    public boolean isValidDictionary() {
300        return mNativeDict != 0;
301    }
302
303    public int getFormatVersion() {
304        return getFormatVersionNative(mNativeDict);
305    }
306
307    @Override
308    public boolean isValidWord(final String word) {
309        return getFrequency(word) != NOT_A_PROBABILITY;
310    }
311
312    @Override
313    public int getFrequency(final String word) {
314        if (word == null) return NOT_A_PROBABILITY;
315        int[] codePoints = StringUtils.toCodePointArray(word);
316        return getProbabilityNative(mNativeDict, codePoints);
317    }
318
319    // TODO: Add a batch process version (isValidBigramMultiple?) to avoid excessive numbers of jni
320    // calls when checking for changes in an entire dictionary.
321    public boolean isValidBigram(final String word0, final String word1) {
322        return getBigramProbability(word0, word1) != NOT_A_PROBABILITY;
323    }
324
325    public int getBigramProbability(final String word0, final String word1) {
326        if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) return NOT_A_PROBABILITY;
327        final int[] codePoints0 = StringUtils.toCodePointArray(word0);
328        final int[] codePoints1 = StringUtils.toCodePointArray(word1);
329        return getBigramProbabilityNative(mNativeDict, codePoints0, codePoints1);
330    }
331
332    public WordProperty getWordProperty(final String word) {
333        if (TextUtils.isEmpty(word)) {
334            return null;
335        }
336        final int[] codePoints = StringUtils.toCodePointArray(word);
337        final int[] outCodePoints = new int[MAX_WORD_LENGTH];
338        final boolean[] outFlags = new boolean[FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT];
339        final int[] outProbabilityInfo =
340                new int[FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT];
341        final ArrayList<int[]> outBigramTargets = CollectionUtils.newArrayList();
342        final ArrayList<int[]> outBigramProbabilityInfo = CollectionUtils.newArrayList();
343        final ArrayList<int[]> outShortcutTargets = CollectionUtils.newArrayList();
344        final ArrayList<Integer> outShortcutProbabilities = CollectionUtils.newArrayList();
345        getWordPropertyNative(mNativeDict, codePoints, outCodePoints, outFlags, outProbabilityInfo,
346                outBigramTargets, outBigramProbabilityInfo, outShortcutTargets,
347                outShortcutProbabilities);
348        return new WordProperty(codePoints,
349                outFlags[FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX],
350                outFlags[FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX],
351                outFlags[FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX],
352                outFlags[FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX], outProbabilityInfo,
353                outBigramTargets, outBigramProbabilityInfo, outShortcutTargets,
354                outShortcutProbabilities);
355    }
356
357    public static class GetNextWordPropertyResult {
358        public WordProperty mWordProperty;
359        public int mNextToken;
360
361        public GetNextWordPropertyResult(final WordProperty wordPreperty, final int nextToken) {
362            mWordProperty = wordPreperty;
363            mNextToken = nextToken;
364        }
365    }
366
367    /**
368     * Method to iterate all words in the dictionary for makedict.
369     * If token is 0, this method newly starts iterating the dictionary.
370     */
371    public GetNextWordPropertyResult getNextWordProperty(final int token) {
372        final int[] codePoints = new int[MAX_WORD_LENGTH];
373        final int nextToken = getNextWordNative(mNativeDict, token, codePoints);
374        final String word = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints);
375        return new GetNextWordPropertyResult(getWordProperty(word), nextToken);
376    }
377
378    // Add a unigram entry to binary dictionary with unigram attributes in native code.
379    public void addUnigramWord(final String word, final int probability,
380            final String shortcutTarget, final int shortcutProbability, final boolean isNotAWord,
381            final boolean isBlacklisted, final int timestamp) {
382        if (TextUtils.isEmpty(word)) {
383            return;
384        }
385        final int[] codePoints = StringUtils.toCodePointArray(word);
386        final int[] shortcutTargetCodePoints = (shortcutTarget != null) ?
387                StringUtils.toCodePointArray(shortcutTarget) : null;
388        addUnigramWordNative(mNativeDict, codePoints, probability, shortcutTargetCodePoints,
389                shortcutProbability, isNotAWord, isBlacklisted, timestamp);
390    }
391
392    // Add a bigram entry to binary dictionary with timestamp in native code.
393    public void addBigramWords(final String word0, final String word1, final int probability,
394            final int timestamp) {
395        if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) {
396            return;
397        }
398        final int[] codePoints0 = StringUtils.toCodePointArray(word0);
399        final int[] codePoints1 = StringUtils.toCodePointArray(word1);
400        addBigramWordsNative(mNativeDict, codePoints0, codePoints1, probability, timestamp);
401    }
402
403    // Remove a bigram entry form binary dictionary in native code.
404    public void removeBigramWords(final String word0, final String word1) {
405        if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) {
406            return;
407        }
408        final int[] codePoints0 = StringUtils.toCodePointArray(word0);
409        final int[] codePoints1 = StringUtils.toCodePointArray(word1);
410        removeBigramWordsNative(mNativeDict, codePoints0, codePoints1);
411    }
412
413    public void addMultipleDictionaryEntries(final LanguageModelParam[] languageModelParams) {
414        if (!isValidDictionary()) return;
415        int processedParamCount = 0;
416        while (processedParamCount < languageModelParams.length) {
417            if (needsToRunGC(true /* mindsBlockByGC */)) {
418                flushWithGC();
419            }
420            processedParamCount = addMultipleDictionaryEntriesNative(mNativeDict,
421                    languageModelParams, processedParamCount);
422            if (processedParamCount <= 0) {
423                return;
424            }
425        }
426    }
427
428    private void reopen() {
429        close();
430        final File dictFile = new File(mDictFilePath);
431        // WARNING: Because we pass 0 as the offset and file.length() as the length, this can
432        // only be called for actual files. Right now it's only called by the flush() family of
433        // functions, which require an updatable dictionary, so it's okay. But beware.
434        loadDictionary(dictFile.getAbsolutePath(), 0 /* startOffset */,
435                dictFile.length(), mIsUpdatable);
436    }
437
438    public void flush() {
439        if (!isValidDictionary()) return;
440        flushNative(mNativeDict, mDictFilePath);
441        reopen();
442    }
443
444    public void flushWithGC() {
445        if (!isValidDictionary()) return;
446        flushWithGCNative(mNativeDict, mDictFilePath);
447        reopen();
448    }
449
450    /**
451     * Checks whether GC is needed to run or not.
452     * @param mindsBlockByGC Whether to mind operations blocked by GC. We don't need to care about
453     * the blocking in some situations such as in idle time or just before closing.
454     * @return whether GC is needed to run or not.
455     */
456    public boolean needsToRunGC(final boolean mindsBlockByGC) {
457        if (!isValidDictionary()) return false;
458        return needsToRunGCNative(mNativeDict, mindsBlockByGC);
459    }
460
461    @UsedForTesting
462    public int calculateProbability(final int unigramProbability, final int bigramProbability) {
463        if (!isValidDictionary()) return NOT_A_PROBABILITY;
464        return calculateProbabilityNative(mNativeDict, unigramProbability, bigramProbability);
465    }
466
467    @UsedForTesting
468    public String getPropertyForTest(final String query) {
469        if (!isValidDictionary()) return "";
470        return getPropertyNative(mNativeDict, query);
471    }
472
473    @Override
474    public boolean shouldAutoCommit(final SuggestedWordInfo candidate) {
475        return candidate.mAutoCommitFirstWordConfidence > CONFIDENCE_TO_AUTO_COMMIT;
476    }
477
478    @Override
479    public void close() {
480        synchronized (mDicTraverseSessions) {
481            final int sessionsSize = mDicTraverseSessions.size();
482            for (int index = 0; index < sessionsSize; ++index) {
483                final DicTraverseSession traverseSession = mDicTraverseSessions.valueAt(index);
484                if (traverseSession != null) {
485                    traverseSession.close();
486                }
487            }
488            mDicTraverseSessions.clear();
489        }
490        closeInternalLocked();
491    }
492
493    private synchronized void closeInternalLocked() {
494        if (mNativeDict != 0) {
495            closeNative(mNativeDict);
496            mNativeDict = 0;
497        }
498    }
499
500    // TODO: Manage BinaryDictionary instances without using WeakReference or something.
501    @Override
502    protected void finalize() throws Throwable {
503        try {
504            closeInternalLocked();
505        } finally {
506            super.finalize();
507        }
508    }
509}
510