2012-03-08 08:07:02 +00:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2012 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.
|
|
|
|
*/
|
|
|
|
|
2013-06-23 16:11:32 +00:00
|
|
|
package com.android.inputmethod.latin.utils;
|
2012-03-08 08:07:02 +00:00
|
|
|
|
|
|
|
import android.text.TextUtils;
|
|
|
|
|
2013-08-14 11:38:32 +00:00
|
|
|
import com.android.inputmethod.annotations.UsedForTesting;
|
2013-06-23 16:11:32 +00:00
|
|
|
import com.android.inputmethod.latin.Constants;
|
2013-08-14 08:34:25 +00:00
|
|
|
import com.android.inputmethod.latin.settings.SettingsValues;
|
2013-06-23 16:11:32 +00:00
|
|
|
|
2012-03-08 08:07:02 +00:00
|
|
|
import java.util.ArrayList;
|
2012-04-04 05:30:42 +00:00
|
|
|
import java.util.Locale;
|
2012-03-08 08:07:02 +00:00
|
|
|
|
2012-08-29 08:26:00 +00:00
|
|
|
public final class StringUtils {
|
2013-04-04 12:14:37 +00:00
|
|
|
public static final int CAPITALIZE_NONE = 0; // No caps, or mixed case
|
|
|
|
public static final int CAPITALIZE_FIRST = 1; // First only
|
|
|
|
public static final int CAPITALIZE_ALL = 2; // All caps
|
|
|
|
|
2012-03-08 08:07:02 +00:00
|
|
|
private StringUtils() {
|
|
|
|
// This utility class is not publicly instantiable.
|
|
|
|
}
|
|
|
|
|
2012-10-03 06:19:43 +00:00
|
|
|
public static int codePointCount(final String text) {
|
2012-03-08 08:07:02 +00:00
|
|
|
if (TextUtils.isEmpty(text)) return 0;
|
|
|
|
return text.codePointCount(0, text.length());
|
|
|
|
}
|
|
|
|
|
2013-05-28 04:19:16 +00:00
|
|
|
public static boolean containsInArray(final String text, final String[] array) {
|
2012-04-19 07:39:25 +00:00
|
|
|
for (final String element : array) {
|
2013-05-28 04:19:16 +00:00
|
|
|
if (text.equals(element)) return true;
|
2012-03-08 08:07:02 +00:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2013-05-28 10:12:12 +00:00
|
|
|
/**
|
|
|
|
* Comma-Splittable Text is similar to Comma-Separated Values (CSV) but has much simpler syntax.
|
|
|
|
* Unlike CSV, Comma-Splittable Text has no escaping mechanism, so that the text can't contain
|
|
|
|
* a comma character in it.
|
|
|
|
*/
|
|
|
|
private static final String SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT = ",";
|
2013-05-28 04:19:16 +00:00
|
|
|
|
2013-05-28 10:12:12 +00:00
|
|
|
public static boolean containsInCommaSplittableText(final String text,
|
2013-05-28 04:19:16 +00:00
|
|
|
final String extraValues) {
|
|
|
|
if (TextUtils.isEmpty(extraValues)) {
|
|
|
|
return false;
|
|
|
|
}
|
2013-05-28 10:12:12 +00:00
|
|
|
return containsInArray(text, extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT));
|
2012-04-19 07:39:25 +00:00
|
|
|
}
|
|
|
|
|
2013-05-28 10:12:12 +00:00
|
|
|
public static String appendToCommaSplittableTextIfNotExists(final String text,
|
2013-05-28 04:19:16 +00:00
|
|
|
final String extraValues) {
|
|
|
|
if (TextUtils.isEmpty(extraValues)) {
|
|
|
|
return text;
|
|
|
|
}
|
2013-05-28 10:12:12 +00:00
|
|
|
if (containsInCommaSplittableText(text, extraValues)) {
|
2013-05-28 04:19:16 +00:00
|
|
|
return extraValues;
|
|
|
|
}
|
2013-05-28 10:12:12 +00:00
|
|
|
return extraValues + SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT + text;
|
2012-04-19 07:39:25 +00:00
|
|
|
}
|
|
|
|
|
2013-05-28 10:12:12 +00:00
|
|
|
public static String removeFromCommaSplittableTextIfExists(final String text,
|
2013-05-28 04:19:16 +00:00
|
|
|
final String extraValues) {
|
|
|
|
if (TextUtils.isEmpty(extraValues)) {
|
|
|
|
return "";
|
|
|
|
}
|
2013-05-28 10:12:12 +00:00
|
|
|
final String[] elements = extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT);
|
2013-05-28 04:19:16 +00:00
|
|
|
if (!containsInArray(text, elements)) {
|
|
|
|
return extraValues;
|
|
|
|
}
|
2012-08-21 07:34:55 +00:00
|
|
|
final ArrayList<String> result = CollectionUtils.newArrayList(elements.length - 1);
|
2012-04-19 07:39:25 +00:00
|
|
|
for (final String element : elements) {
|
2013-05-28 04:19:16 +00:00
|
|
|
if (!text.equals(element)) {
|
|
|
|
result.add(element);
|
|
|
|
}
|
2012-04-19 07:39:25 +00:00
|
|
|
}
|
2013-05-28 10:12:12 +00:00
|
|
|
return TextUtils.join(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT, result);
|
2012-03-08 08:07:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove duplicates from an array of strings.
|
|
|
|
*
|
2012-04-06 02:49:48 +00:00
|
|
|
* This method will always keep the first occurrence of all strings at their position
|
2012-03-08 08:07:02 +00:00
|
|
|
* in the array, removing the subsequent ones.
|
|
|
|
*/
|
2012-10-03 06:19:43 +00:00
|
|
|
public static void removeDupes(final ArrayList<String> suggestions) {
|
2012-03-08 08:07:02 +00:00
|
|
|
if (suggestions.size() < 2) return;
|
|
|
|
int i = 1;
|
|
|
|
// Don't cache suggestions.size(), since we may be removing items
|
|
|
|
while (i < suggestions.size()) {
|
2012-10-03 06:19:43 +00:00
|
|
|
final String cur = suggestions.get(i);
|
2012-03-08 08:07:02 +00:00
|
|
|
// Compare each suggestion with each previous suggestion
|
|
|
|
for (int j = 0; j < i; j++) {
|
2012-10-03 06:19:43 +00:00
|
|
|
final String previous = suggestions.get(j);
|
2012-03-08 08:07:02 +00:00
|
|
|
if (TextUtils.equals(cur, previous)) {
|
2012-03-13 07:52:42 +00:00
|
|
|
suggestions.remove(i);
|
2012-03-08 08:07:02 +00:00
|
|
|
i--;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
}
|
2012-04-04 05:30:42 +00:00
|
|
|
|
2013-04-10 08:13:26 +00:00
|
|
|
public static String capitalizeFirstCodePoint(final String s, final Locale locale) {
|
2013-04-10 03:07:41 +00:00
|
|
|
if (s.length() <= 1) {
|
|
|
|
return s.toUpperCase(locale);
|
|
|
|
}
|
2013-04-10 08:13:26 +00:00
|
|
|
// Please refer to the comment below in
|
|
|
|
// {@link #capitalizeFirstAndDowncaseRest(String,Locale)} as this has the same shortcomings
|
2013-04-10 03:07:41 +00:00
|
|
|
final int cutoff = s.offsetByCodePoints(0, 1);
|
|
|
|
return s.substring(0, cutoff).toUpperCase(locale) + s.substring(cutoff);
|
|
|
|
}
|
|
|
|
|
2013-04-10 08:13:26 +00:00
|
|
|
public static String capitalizeFirstAndDowncaseRest(final String s, final Locale locale) {
|
2012-04-04 05:30:42 +00:00
|
|
|
if (s.length() <= 1) {
|
2013-04-10 03:07:41 +00:00
|
|
|
return s.toUpperCase(locale);
|
2012-04-04 05:30:42 +00:00
|
|
|
}
|
|
|
|
// TODO: fix the bugs below
|
|
|
|
// - This does not work for Greek, because it returns upper case instead of title case.
|
|
|
|
// - It does not work for Serbian, because it fails to account for the "lj" character,
|
|
|
|
// which should be "Lj" in title case and "LJ" in upper case.
|
2013-04-05 03:07:16 +00:00
|
|
|
// - It does not work for Dutch, because it fails to account for the "ij" digraph when it's
|
|
|
|
// written as two separate code points. They are two different characters but both should
|
|
|
|
// be capitalized as "IJ" as if they were a single letter in most words (not all). If the
|
|
|
|
// unicode char for the ligature is used however, it works.
|
|
|
|
final int cutoff = s.offsetByCodePoints(0, 1);
|
|
|
|
return s.substring(0, cutoff).toUpperCase(locale) + s.substring(cutoff).toLowerCase(locale);
|
2012-04-04 05:30:42 +00:00
|
|
|
}
|
2012-04-17 06:55:17 +00:00
|
|
|
|
2012-10-03 06:19:43 +00:00
|
|
|
private static final int[] EMPTY_CODEPOINTS = {};
|
|
|
|
|
2012-04-17 06:55:17 +00:00
|
|
|
public static int[] toCodePointArray(final String string) {
|
2012-10-03 06:19:43 +00:00
|
|
|
final int length = string.length();
|
2012-06-25 18:17:48 +00:00
|
|
|
if (length <= 0) {
|
2012-10-03 06:19:43 +00:00
|
|
|
return EMPTY_CODEPOINTS;
|
2012-06-25 18:17:48 +00:00
|
|
|
}
|
2012-10-03 06:19:43 +00:00
|
|
|
final int[] codePoints = new int[string.codePointCount(0, length)];
|
|
|
|
int destIndex = 0;
|
|
|
|
for (int index = 0; index < length; index = string.offsetByCodePoints(index, 1)) {
|
|
|
|
codePoints[destIndex] = string.codePointAt(index);
|
|
|
|
destIndex++;
|
2012-04-17 06:55:17 +00:00
|
|
|
}
|
|
|
|
return codePoints;
|
|
|
|
}
|
2012-09-10 10:27:45 +00:00
|
|
|
|
2013-04-05 03:07:16 +00:00
|
|
|
// This method assumes the text is not null. For the empty string, it returns CAPITALIZE_NONE.
|
2013-04-04 12:14:37 +00:00
|
|
|
public static int getCapitalizationType(final String text) {
|
|
|
|
// If the first char is not uppercase, then the word is either all lower case or
|
|
|
|
// camel case, and in either case we return CAPITALIZE_NONE.
|
|
|
|
final int len = text.length();
|
2013-04-05 03:07:16 +00:00
|
|
|
int index = 0;
|
|
|
|
for (; index < len; index = text.offsetByCodePoints(index, 1)) {
|
|
|
|
if (Character.isLetter(text.codePointAt(index))) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (index == len) return CAPITALIZE_NONE;
|
|
|
|
if (!Character.isUpperCase(text.codePointAt(index))) {
|
|
|
|
return CAPITALIZE_NONE;
|
|
|
|
}
|
2013-04-04 12:14:37 +00:00
|
|
|
int capsCount = 1;
|
|
|
|
int letterCount = 1;
|
2013-04-05 03:07:16 +00:00
|
|
|
for (index = text.offsetByCodePoints(index, 1); index < len;
|
|
|
|
index = text.offsetByCodePoints(index, 1)) {
|
2013-04-04 12:14:37 +00:00
|
|
|
if (1 != capsCount && letterCount != capsCount) break;
|
2013-04-05 03:07:16 +00:00
|
|
|
final int codePoint = text.codePointAt(index);
|
2013-04-04 12:14:37 +00:00
|
|
|
if (Character.isUpperCase(codePoint)) {
|
|
|
|
++capsCount;
|
|
|
|
++letterCount;
|
|
|
|
} else if (Character.isLetter(codePoint)) {
|
|
|
|
// We need to discount non-letters since they may not be upper-case, but may
|
|
|
|
// still be part of a word (e.g. single quote or dash, as in "IT'S" or "FULL-TIME")
|
|
|
|
++letterCount;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// We know the first char is upper case. So we want to test if either every letter other
|
|
|
|
// than the first is lower case, or if they are all upper case. If the string is exactly
|
|
|
|
// one char long, then we will arrive here with letterCount 1, and this is correct, too.
|
|
|
|
if (1 == capsCount) return CAPITALIZE_FIRST;
|
|
|
|
return (letterCount == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE);
|
|
|
|
}
|
2013-04-05 06:52:41 +00:00
|
|
|
|
|
|
|
public static boolean isIdenticalAfterUpcase(final String text) {
|
2013-08-14 08:34:25 +00:00
|
|
|
final int length = text.length();
|
|
|
|
int i = 0;
|
|
|
|
while (i < length) {
|
2013-04-05 06:52:41 +00:00
|
|
|
final int codePoint = text.codePointAt(i);
|
|
|
|
if (Character.isLetter(codePoint) && !Character.isUpperCase(codePoint)) {
|
|
|
|
return false;
|
|
|
|
}
|
2013-08-14 08:34:25 +00:00
|
|
|
i += Character.charCount(codePoint);
|
2013-04-05 06:52:41 +00:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static boolean isIdenticalAfterDowncase(final String text) {
|
2013-08-14 08:34:25 +00:00
|
|
|
final int length = text.length();
|
|
|
|
int i = 0;
|
|
|
|
while (i < length) {
|
2013-04-05 06:52:41 +00:00
|
|
|
final int codePoint = text.codePointAt(i);
|
|
|
|
if (Character.isLetter(codePoint) && !Character.isLowerCase(codePoint)) {
|
|
|
|
return false;
|
|
|
|
}
|
2013-08-14 08:34:25 +00:00
|
|
|
i += Character.charCount(codePoint);
|
2013-04-05 06:52:41 +00:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2013-08-14 11:38:32 +00:00
|
|
|
@UsedForTesting
|
2013-08-14 08:34:25 +00:00
|
|
|
public static boolean looksValidForDictionaryInsertion(final CharSequence text,
|
|
|
|
final SettingsValues settings) {
|
|
|
|
if (TextUtils.isEmpty(text)) return false;
|
|
|
|
final int length = text.length();
|
|
|
|
int i = 0;
|
|
|
|
int digitCount = 0;
|
|
|
|
while (i < length) {
|
|
|
|
final int codePoint = Character.codePointAt(text, i);
|
|
|
|
final int charCount = Character.charCount(codePoint);
|
|
|
|
i += charCount;
|
|
|
|
if (Character.isDigit(codePoint)) {
|
|
|
|
// Count digits: see below
|
|
|
|
digitCount += charCount;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (!settings.isWordCodePoint(codePoint)) return false;
|
|
|
|
}
|
|
|
|
// We reject strings entirely comprised of digits to avoid using PIN codes or credit
|
|
|
|
// card numbers. It would come in handy for word prediction though; a good example is
|
|
|
|
// when writing one's address where the street number is usually quite discriminative,
|
|
|
|
// as well as the postal code.
|
|
|
|
return digitCount < length;
|
|
|
|
}
|
|
|
|
|
2013-04-05 06:52:41 +00:00
|
|
|
public static boolean isIdenticalAfterCapitalizeEachWord(final String text,
|
|
|
|
final String separators) {
|
|
|
|
boolean needCapsNext = true;
|
|
|
|
final int len = text.length();
|
|
|
|
for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
|
|
|
|
final int codePoint = text.codePointAt(i);
|
|
|
|
if (Character.isLetter(codePoint)) {
|
|
|
|
if ((needCapsNext && !Character.isUpperCase(codePoint))
|
|
|
|
|| (!needCapsNext && !Character.isLowerCase(codePoint))) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// We need a capital letter next if this is a separator.
|
|
|
|
needCapsNext = (-1 != separators.indexOf(codePoint));
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: like capitalizeFirst*, this does not work perfectly for Dutch because of the IJ digraph
|
|
|
|
// which should be capitalized together in *some* cases.
|
|
|
|
public static String capitalizeEachWord(final String text, final String separators,
|
|
|
|
final Locale locale) {
|
|
|
|
final StringBuilder builder = new StringBuilder();
|
|
|
|
boolean needCapsNext = true;
|
|
|
|
final int len = text.length();
|
|
|
|
for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
|
|
|
|
final String nextChar = text.substring(i, text.offsetByCodePoints(i, 1));
|
|
|
|
if (needCapsNext) {
|
|
|
|
builder.append(nextChar.toUpperCase(locale));
|
|
|
|
} else {
|
|
|
|
builder.append(nextChar.toLowerCase(locale));
|
|
|
|
}
|
|
|
|
// We need a capital letter next if this is a separator.
|
|
|
|
needCapsNext = (-1 != separators.indexOf(nextChar.codePointAt(0)));
|
|
|
|
}
|
|
|
|
return builder.toString();
|
|
|
|
}
|
2013-04-15 08:33:48 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Approximates whether the text before the cursor looks like a URL.
|
|
|
|
*
|
|
|
|
* This is not foolproof, but it should work well in the practice.
|
|
|
|
* Essentially it walks backward from the cursor until it finds something that's not a letter,
|
|
|
|
* digit, or common URL symbol like underscore. If it hasn't found a period yet, then it
|
|
|
|
* does not look like a URL.
|
|
|
|
* If the text:
|
|
|
|
* - starts with www and contains a period
|
|
|
|
* - starts with a slash preceded by either a slash, whitespace, or start-of-string
|
|
|
|
* Then it looks like a URL and we return true. Otherwise, we return false.
|
|
|
|
*
|
|
|
|
* Note: this method is called quite often, and should be fast.
|
|
|
|
*
|
|
|
|
* TODO: This will return that "abc./def" and ".abc/def" look like URLs to keep down the
|
|
|
|
* code complexity, but ideally it should not. It's acceptable for now.
|
|
|
|
*/
|
|
|
|
public static boolean lastPartLooksLikeURL(final CharSequence text) {
|
|
|
|
int i = text.length();
|
|
|
|
if (0 == i) return false;
|
|
|
|
int wCount = 0;
|
|
|
|
int slashCount = 0;
|
|
|
|
boolean hasSlash = false;
|
|
|
|
boolean hasPeriod = false;
|
|
|
|
int codePoint = 0;
|
|
|
|
while (i > 0) {
|
|
|
|
codePoint = Character.codePointBefore(text, i);
|
|
|
|
if (codePoint < Constants.CODE_PERIOD || codePoint > 'z') {
|
|
|
|
// Handwavy heuristic to see if that's a URL character. Anything between period
|
|
|
|
// and z. This includes all lower- and upper-case ascii letters, period,
|
|
|
|
// underscore, arrobase, question mark, equal sign. It excludes spaces, exclamation
|
|
|
|
// marks, double quotes...
|
|
|
|
// Anything that's not a URL-like character causes us to break from here and
|
|
|
|
// evaluate normally.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (Constants.CODE_PERIOD == codePoint) {
|
|
|
|
hasPeriod = true;
|
|
|
|
}
|
|
|
|
if (Constants.CODE_SLASH == codePoint) {
|
|
|
|
hasSlash = true;
|
|
|
|
if (2 == ++slashCount) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
slashCount = 0;
|
|
|
|
}
|
|
|
|
if ('w' == codePoint) {
|
|
|
|
++wCount;
|
|
|
|
} else {
|
|
|
|
wCount = 0;
|
|
|
|
}
|
|
|
|
i = Character.offsetByCodePoints(text, i, -1);
|
|
|
|
}
|
|
|
|
// End of the text run.
|
|
|
|
// If it starts with www and includes a period, then it looks like a URL.
|
|
|
|
if (wCount >= 3 && hasPeriod) return true;
|
|
|
|
// If it starts with a slash, and the code point before is whitespace, it looks like an URL.
|
|
|
|
if (1 == slashCount && (0 == i || Character.isWhitespace(codePoint))) return true;
|
|
|
|
// If it has both a period and a slash, it looks like an URL.
|
|
|
|
if (hasPeriod && hasSlash) return true;
|
|
|
|
// Otherwise, it doesn't look like an URL.
|
|
|
|
return false;
|
|
|
|
}
|
2013-08-14 05:46:49 +00:00
|
|
|
|
|
|
|
public static boolean isEmptyStringOrWhiteSpaces(String s) {
|
|
|
|
final int N = codePointCount(s);
|
|
|
|
for (int i = 0; i < N; ++i) {
|
|
|
|
if (!Character.isWhitespace(s.codePointAt(i))) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
2013-08-16 05:35:56 +00:00
|
|
|
|
|
|
|
@UsedForTesting
|
|
|
|
public static String byteArrayToHexString(byte[] bytes) {
|
|
|
|
if (bytes == null || bytes.length == 0) {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
final StringBuilder sb = new StringBuilder();
|
|
|
|
for (byte b : bytes) {
|
|
|
|
sb.append(String.format("%02x", b & 0xff));
|
|
|
|
}
|
|
|
|
return sb.toString();
|
|
|
|
}
|
|
|
|
|
2013-08-16 07:55:49 +00:00
|
|
|
/**
|
|
|
|
* Convert hex string to byte array. The string length must be an even number.
|
|
|
|
*/
|
2013-08-16 05:35:56 +00:00
|
|
|
@UsedForTesting
|
|
|
|
public static byte[] hexStringToByteArray(String hexString) {
|
|
|
|
if (TextUtils.isEmpty(hexString)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
final int N = hexString.length();
|
2013-08-16 07:55:49 +00:00
|
|
|
if (N % 2 != 0) {
|
|
|
|
throw new NumberFormatException("Input hex string length must be an even number."
|
|
|
|
+ " Length = " + N);
|
|
|
|
}
|
2013-08-16 05:35:56 +00:00
|
|
|
final byte[] bytes = new byte[N / 2];
|
|
|
|
for (int i = 0; i < N; i += 2) {
|
|
|
|
bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
|
|
|
|
+ Character.digit(hexString.charAt(i + 1), 16));
|
|
|
|
}
|
|
|
|
return bytes;
|
|
|
|
}
|
2012-03-08 08:07:02 +00:00
|
|
|
}
|