| /* |
| * Copyright (C) 2014 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.tools.lint.checks; |
| |
| import static com.android.tools.lint.checks.PluralsDatabase.FLAG_FEW; |
| import static com.android.tools.lint.checks.PluralsDatabase.FLAG_MANY; |
| import static com.android.tools.lint.checks.PluralsDatabase.FLAG_MULTIPLE_ONE; |
| import static com.android.tools.lint.checks.PluralsDatabase.FLAG_MULTIPLE_TWO; |
| import static com.android.tools.lint.checks.PluralsDatabase.FLAG_MULTIPLE_ZERO; |
| import static com.android.tools.lint.checks.PluralsDatabase.FLAG_ONE; |
| import static com.android.tools.lint.checks.PluralsDatabase.FLAG_TWO; |
| import static com.android.tools.lint.checks.PluralsDatabase.FLAG_ZERO; |
| import static com.android.tools.lint.checks.PluralsDatabase.Quantity; |
| import static com.android.tools.lint.checks.PluralsDatabase.Quantity.few; |
| import static com.android.tools.lint.checks.PluralsDatabase.Quantity.many; |
| import static com.android.tools.lint.checks.PluralsDatabase.Quantity.one; |
| import static com.android.tools.lint.checks.PluralsDatabase.Quantity.two; |
| import static com.android.tools.lint.checks.PluralsDatabase.Quantity.zero; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.resources.LocaleManager; |
| import com.google.common.base.Charsets; |
| import com.google.common.base.Objects; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import com.google.common.io.ByteStreams; |
| |
| import junit.framework.TestCase; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.EnumSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| |
| public class PluralsDatabaseTest extends TestCase { |
| public void testGetRelevant() { |
| PluralsDatabase db = PluralsDatabase.get(); |
| assertNull(db.getRelevant("unknown")); |
| EnumSet<Quantity> relevant = db.getRelevant("en"); |
| assertNotNull(relevant); |
| assertEquals(1, relevant.size()); |
| assertSame(Quantity.one, relevant.iterator().next()); |
| |
| relevant = db.getRelevant("cs"); |
| assertNotNull(relevant); |
| assertEquals(EnumSet.of(Quantity.few, Quantity.one), relevant); |
| } |
| |
| public void testFindExamples() { |
| PluralsDatabase db = PluralsDatabase.get(); |
| |
| //noinspection ConstantConditions |
| assertEquals("1, 101, 201, 301, 401, 501, 601, 701, 1001, \u2026", |
| db.findIntegerExamples("sl", Quantity.one)); |
| |
| assertEquals("1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026", |
| db.findIntegerExamples("ru", Quantity.one)); |
| } |
| |
| public void testHasMultiValue() { |
| PluralsDatabase db = PluralsDatabase.get(); |
| |
| assertFalse(db.hasMultipleValuesForQuantity("en", Quantity.one)); |
| assertFalse(db.hasMultipleValuesForQuantity("en", Quantity.two)); |
| assertFalse(db.hasMultipleValuesForQuantity("en", Quantity.few)); |
| assertFalse(db.hasMultipleValuesForQuantity("en", Quantity.many)); |
| |
| assertTrue(db.hasMultipleValuesForQuantity("br", Quantity.two)); |
| assertTrue(db.hasMultipleValuesForQuantity("mk", Quantity.one)); |
| assertTrue(db.hasMultipleValuesForQuantity("lv", Quantity.zero)); |
| } |
| |
| /** |
| * If the lint unit test data/ folder contains a plurals.txt database file, |
| * this test will parse that file and ensure that our current database produces |
| * exactly the same results as those inferred from the file. If not, it will |
| * dump out updated data structures for the database. |
| */ |
| public void testDatabaseAccurate() { |
| List<String> languages = new ArrayList<String>(LocaleManager.getLanguageCodes()); |
| Collections.sort(languages); |
| PluralsTextDatabase db = PluralsTextDatabase.get(); |
| db.ensureInitialized(); |
| |
| if (db.getSetName("en") == null) { |
| // plurals.txt not found |
| System.out.println("No plurals.txt database included; not checking consistency"); |
| return; |
| } |
| |
| // Ensure that the two databases (the plurals.txt backed one and our actual |
| // database) fully agree on everything |
| PluralsDatabase pdb = PluralsDatabase.get(); |
| for (String language : languages) { |
| if (!Objects.equal(pdb.getRelevant(language), db.getRelevant(language))) { |
| dumpDatabaseTables(); |
| assertEquals(language, pdb.getRelevant(language), db.getRelevant(language)); |
| } |
| if (db.getSetName(language) == null) { |
| continue; |
| } |
| for (Quantity q : Quantity.values()) { |
| boolean mv1 = pdb.hasMultipleValuesForQuantity(language, q); |
| boolean mv2 = db.hasMultipleValuesForQuantity(language, q); |
| if (mv1 != mv2) { |
| dumpDatabaseTables(); |
| assertEquals(language, mv1, mv2); |
| } |
| if (mv2) { |
| String e1 = pdb.findIntegerExamples(language, q); |
| String e2 = db.findIntegerExamples(language, q); |
| if (!Objects.equal(e1, e2)) { |
| dumpDatabaseTables(); |
| assertEquals(language, e1, e2); |
| } |
| } |
| } |
| } |
| } |
| |
| private static void dumpDatabaseTables() { |
| List<String> languages = new ArrayList<String>(LocaleManager.getLanguageCodes()); |
| Collections.sort(languages); |
| PluralsTextDatabase db = PluralsTextDatabase.get(); |
| db.ensureInitialized(); |
| |
| db.getRelevant("en"); // ensure initialized |
| Map<String,String> languageMap = Maps.newHashMap(); |
| Map<String,EnumSet<Quantity>> setMap = Maps.newHashMap(); |
| for (String language : languages) { |
| String set = db.getSetName(language); |
| if (set == null) { |
| continue; |
| } |
| EnumSet<Quantity> quantitySet = db.getRelevant(language); |
| if (quantitySet == null) { |
| // No plurals data for this language. For example, in ICU 52, no |
| // plurals data for the "nv" language (Navajo). |
| continue; |
| } |
| assertNotNull(language, quantitySet); |
| setMap.put(set, quantitySet); |
| languageMap.put(set, language); // Could be multiple |
| } |
| |
| List<String> setNames = Lists.newArrayList(setMap.keySet()); |
| Collections.sort(setNames); |
| |
| // Compute uniqueness |
| Map<String,String> sameAs = Maps.newHashMap(); |
| for (int i = 0, n = setNames.size(); i < n; i++) { |
| for (int j = i + 1; j < n; j++) { |
| String iSetName = setNames.get(i); |
| String jSetName = setNames.get(j); |
| assertNotNull(iSetName); |
| assertNotNull(jSetName); |
| EnumSet<Quantity> iSet = setMap.get(iSetName); |
| EnumSet<Quantity> jSet = setMap.get(jSetName); |
| assertNotNull(iSet); |
| assertNotNull(jSet); |
| if (iSet.equals(jSet)) { |
| String alias = sameAs.get(iSetName); |
| if (alias != null) { |
| iSetName = alias; |
| } |
| sameAs.put(jSetName, iSetName); |
| break; |
| } |
| } |
| } |
| |
| final String indent = " "; |
| StringBuilder sb = new StringBuilder(); |
| |
| // Multi Value Set names |
| Set<String> sets = Sets.newHashSet(); |
| for (String language : languages) { |
| String set = db.getSetName(language); |
| sets.add(set); |
| languageMap.put(set, language); // Could be multiple |
| } |
| |
| Map<String,Integer> indices = Maps.newTreeMap(); |
| int index = 0; |
| for (String set : setNames) { |
| indices.put(set, index++); |
| } |
| |
| // Language indices |
| Map<String,Integer> languageIndices = Maps.newTreeMap(); |
| index = 0; |
| for (String language : languages) { |
| String set = db.getSetName(language); |
| if (set == null) { |
| continue; |
| } |
| |
| languageIndices.put(language, index++); |
| } |
| |
| Map<String, String> zero = computeExamples(db, Quantity.zero, sets, languageMap); |
| Map<String, String> one = computeExamples(db, Quantity.one, sets, languageMap); |
| Map<String, String> two = computeExamples(db, Quantity.two, sets, languageMap); |
| |
| // Language map |
| sb.setLength(0); |
| sb.append("/** Set of language codes relevant to plurals data */\n"); |
| sb.append("private static final String[] LANGUAGE_CODES = new String[] {\n"); |
| int column = 0; |
| index = 0; |
| sb.append(indent); |
| for (String language : languages) { |
| String set = db.getSetName(language); |
| if (set == null) { |
| continue; |
| } |
| sb.append('"').append(language).append("\", "); |
| column++; |
| if (column == 10) { |
| sb.append("\n"); |
| sb.append(indent); |
| column = 0; |
| } |
| assertEquals((int)languageIndices.get(language), index); |
| index++; |
| } |
| sb.append("\n};\n"); |
| System.out.println(sb); |
| |
| // Quantity map |
| sb.setLength(0); |
| sb.append("/**\n" |
| + " * Relevant flags for each language (corresponding to each language listed\n" |
| + " * in the same position in {@link #LANGUAGE_CODES})\n" |
| + " */\n"); |
| sb.append("private static final int[] FLAGS = new int[] {\n"); |
| column = 0; |
| sb.append(indent); |
| index = 0; |
| for (String language : languages) { |
| String setName = db.getSetName(language); |
| if (setName == null) { |
| continue; |
| } |
| assertEquals((int)languageIndices.get(language), index); |
| |
| // Compute flag |
| int flag = 0; |
| EnumSet<Quantity> relevant = db.getRelevant(language); |
| assertNotNull(relevant); |
| if (relevant.contains(Quantity.zero)) { |
| flag |= FLAG_ZERO; |
| } |
| if (relevant.contains(Quantity.one)) { |
| flag |= FLAG_ONE; |
| } |
| if (relevant.contains(Quantity.two)) { |
| flag |= FLAG_TWO; |
| } |
| if (relevant.contains(Quantity.few)) { |
| flag |= FLAG_FEW; |
| } |
| if (relevant.contains(Quantity.many)) { |
| flag |= FLAG_MANY; |
| } |
| if (zero.containsKey(setName)) { |
| flag |= FLAG_MULTIPLE_ZERO; |
| } |
| if (one.containsKey(setName)) { |
| flag |= FLAG_MULTIPLE_ONE; |
| } |
| if (two.containsKey(setName)) { |
| flag |= FLAG_MULTIPLE_TWO; |
| } |
| |
| sb.append(String.format(Locale.US, "0x%04x, ", flag)); |
| column++; |
| if (column == 8) { |
| sb.append("\n"); |
| sb.append(indent); |
| column = 0; |
| } |
| |
| index++; |
| } |
| sb.append("\n};\n"); |
| System.out.println(sb); |
| |
| // Switch statement methods for examples |
| printSwitch(db, Quantity.zero, languages, languageIndices, indices, zero); |
| printSwitch(db, Quantity.one, languages, languageIndices, indices, one); |
| printSwitch(db, Quantity.two, languages, languageIndices, indices, two); |
| |
| } |
| |
| private static Map<String, String> computeExamples(PluralsTextDatabase db, Quantity quantity, |
| Set<String> sets, Map<String, String> languageMap) { |
| |
| Map<String, String> setsWithExamples = Maps.newHashMap(); |
| for (String set : sets) { |
| String language = languageMap.get(set); |
| String examples = db.findIntegerExamples(language, quantity); |
| if (examples != null && examples.indexOf(',') != -1) { |
| setsWithExamples.put(set, examples); |
| } |
| } |
| |
| return setsWithExamples; |
| } |
| |
| |
| private static void printSwitch( |
| PluralsTextDatabase db, |
| Quantity quantity, |
| List<String> languages, |
| Map<String,Integer> languageIndices, |
| Map<String, Integer> indices, |
| Map<String, String> setsWithExamples) { |
| |
| List<String> sorted = new ArrayList<String>(setsWithExamples.keySet()); |
| Collections.sort(sorted); |
| |
| StringBuilder sb = new StringBuilder(); |
| String quantityName = quantity.name(); |
| quantityName = Character.toUpperCase(quantityName.charAt(0)) + quantityName.substring(1); |
| sb.append(" @Nullable\n" |
| + " private static String getExampleForQuantity").append(quantityName) |
| .append("(@NonNull String language) {\n" |
| + " int index = getLanguageIndex(language);\n" |
| + " switch (index) {\n"); |
| |
| for (Map.Entry<String, Integer> entry : indices.entrySet()) { |
| String set = entry.getKey(); |
| if (!setsWithExamples.containsKey(set)) { |
| continue; |
| } |
| String example = setsWithExamples.get(set); |
| example = example.replace("…", "\\u2026"); |
| sb.append(" // ").append(set).append("\n"); |
| for (String language : languages) { |
| String setName = db.getSetName(language); |
| if (set.equals(setName)) { |
| int languageIndex = languageIndices.get(language); |
| sb.append(" case "); |
| sb.append(languageIndex).append(": // ").append(language).append("\n"); |
| } |
| } |
| |
| sb.append(" return "); |
| sb.append("\"").append(example).append("\""); |
| sb.append(";\n"); |
| } |
| |
| sb.append(" case -1:\n" |
| + " default:\n" |
| + " return null;\n" |
| + " }\n" |
| + " }\n"); |
| |
| System.out.println(sb); |
| } |
| |
| /** |
| * Plurals database backed by a plurals.txt file from ICU |
| */ |
| private static class PluralsTextDatabase { |
| private static final boolean DEBUG = false; |
| private static final EnumSet<Quantity> NONE = EnumSet.noneOf(Quantity.class); |
| |
| private static final PluralsTextDatabase sInstance = new PluralsTextDatabase(); |
| |
| private Map<String, EnumSet<Quantity>> mPlurals; |
| private Map<Quantity, Set<String>> mMultiValueSetNames = Maps.newEnumMap(Quantity.class); |
| private String mDescriptions; |
| private int mRuleSetOffset; |
| private Map<String,String> mSetNamePerLanguage; |
| |
| @NonNull |
| public static PluralsTextDatabase get() { |
| return sInstance; |
| } |
| |
| @Nullable |
| public EnumSet<Quantity> getRelevant(@NonNull String language) { |
| ensureInitialized(); |
| EnumSet<Quantity> set = mPlurals.get(language); |
| if (set == null) { |
| String s = getLocaleData(language); |
| if (s == null) { |
| mPlurals.put(language, NONE); |
| return null; |
| } |
| // Process each item and look for relevance |
| |
| set = EnumSet.noneOf(Quantity.class); |
| int length = s.length(); |
| for (int offset = 0, end; offset < length; offset = end + 1) { |
| for (; offset < length; offset++) { |
| if (!Character.isWhitespace(s.charAt(offset))) { |
| break; |
| } |
| } |
| |
| int begin = s.indexOf('{', offset); |
| if (begin == -1) { |
| break; |
| } |
| end = findBalancedEnd(s, begin); |
| if (end == -1) { |
| end = length; |
| } |
| |
| if (s.startsWith("other{", offset)) { |
| // Not included |
| continue; |
| } |
| |
| // Make sure the rule references applies to integers: |
| // Rule definition mentions n or i or @integer |
| // |
| // n absolute value of the source number (integer and decimals). |
| // i integer digits of n. |
| // v number of visible fraction digits in n, with trailing zeros. |
| // w number of visible fraction digits in n, without trailing zeros. |
| // f visible fractional digits in n, with trailing zeros. |
| // t visible fractional digits in n, without trailing zeros. |
| boolean appliesToIntegers = false; |
| boolean inQuotes = false; |
| for (int i = begin + 1; i < end - 1; i++) { |
| char c = s.charAt(i); |
| if (c == '"') { |
| inQuotes = !inQuotes; |
| } else if (inQuotes) { |
| if (c == '@') { |
| if (s.startsWith("@integer", i)) { |
| appliesToIntegers = true; |
| break; |
| } else { |
| // @decimal always comes after @integer |
| break; |
| } |
| } else if ((c == 'i' || c == 'n') && Character |
| .isWhitespace(s.charAt(i + 1))) { |
| appliesToIntegers = true; |
| break; |
| } |
| } |
| } |
| |
| if (!appliesToIntegers) { |
| if (DEBUG) { |
| System.out.println("Skipping quantity " + s.substring(offset, begin) |
| + " in set for locale " + language + " (" + getSetName(language) |
| + ")"); |
| } |
| continue; |
| } |
| |
| if (s.startsWith("one{", offset)) { |
| set.add(one); |
| } else if (s.startsWith("few{", offset)) { |
| set.add(few); |
| } else if (s.startsWith("many{", offset)) { |
| set.add(many); |
| } else if (s.startsWith("two{", offset)) { |
| set.add(two); |
| } else if (s.startsWith("zero{", offset)) { |
| set.add(zero); |
| } else { |
| // Unexpected quantity: ignore |
| if (DEBUG) { |
| assert false : s.substring(offset, Math.min(offset + 10, length)); |
| } |
| } |
| } |
| |
| mPlurals.put(language, set); |
| } |
| return set == NONE ? null : set; |
| } |
| |
| public boolean hasMultipleValuesForQuantity( |
| @NonNull String language, |
| @NonNull Quantity quantity) { |
| if (quantity == Quantity.one || quantity == Quantity.two || quantity == Quantity.zero) { |
| ensureInitialized(); |
| String setName = getSetName(language); |
| if (setName != null) { |
| Set<String> names = mMultiValueSetNames.get(quantity); |
| assert names != null : quantity; |
| return names.contains(setName); |
| } |
| } |
| |
| return false; |
| } |
| |
| private void ensureInitialized() { |
| if (mPlurals == null) { |
| initialize(); |
| } |
| } |
| |
| @SuppressWarnings({"UnnecessaryLocalVariable", "UnusedDeclaration"}) |
| private void initialize() { |
| // Sets where more than a single integer maps to the quantity. Take for example |
| // set 10: |
| // set10{ |
| // one{ |
| // "n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81," |
| // " 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0" |
| // ", 101.0, 1001.0, …" |
| // } |
| // } |
| // Here we see that both "1" and "21" will match the "one" category. |
| // Note that this only applies to integers (since getQuantityString only takes integer) |
| // whereas the plurals data also covers fractions. I was not sure what to do about |
| // set17: |
| // set17{ |
| // one{"i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6"} |
| // } |
| // since it looks to me like this only differs from 1 in the fractional part. |
| // |
| // This is encoded by looking at the rules; this is done by the unit test |
| // testDeriveMultiValueSetNames() (which ensures that the set is correct and if |
| // not computes the correct set of set names to use for the current plurals.txt |
| // database. |
| |
| mMultiValueSetNames = Maps.newEnumMap(Quantity.class); |
| mMultiValueSetNames.put(Quantity.two, Sets.newHashSet("set21", "set22", "set30", "set32")); |
| mMultiValueSetNames.put(Quantity.one, Sets.newHashSet( |
| "set1", "set11", "set12", "set13", "set14", "set2", "set20", |
| "set21", "set22", "set26", "set27", "set29", "set30", "set32", "set5", |
| "set6")); |
| mMultiValueSetNames.put(Quantity.zero, Sets.newHashSet("set14")); |
| |
| mSetNamePerLanguage = Maps.newHashMapWithExpectedSize(20); |
| mPlurals = Maps.newHashMapWithExpectedSize(20); |
| } |
| |
| @Nullable |
| public String findIntegerExamples(@NonNull String language, @NonNull Quantity quantity) { |
| String data = getQuantityData(language, quantity); |
| if (data != null) { |
| int index = data.indexOf("@integer"); |
| if (index == -1) { |
| return null; |
| } |
| int start = index + "@integer".length(); |
| int end = data.indexOf('@', start); |
| if (end == -1) { |
| end = data.length(); |
| } |
| return data.substring(start, end).trim(); |
| } |
| |
| return null; |
| } |
| |
| |
| @NonNull |
| private String getPluralsDescriptions() { |
| if (mDescriptions == null) { |
| InputStream stream = PluralsDatabaseTest.class.getResourceAsStream("data/plurals.txt"); |
| if (stream != null) { |
| try { |
| byte[] bytes = ByteStreams.toByteArray(stream); |
| mDescriptions = new String(bytes, Charsets.UTF_8); |
| mRuleSetOffset = mDescriptions.indexOf("rules{"); |
| if (mRuleSetOffset == -1) { |
| if (DEBUG) { |
| assert false; |
| } |
| mDescriptions = ""; |
| mRuleSetOffset = 0; |
| } |
| |
| } catch (IOException e) { |
| try { |
| stream.close(); |
| } catch (IOException e1) { |
| // Stupid API. |
| } |
| } |
| } |
| if (mDescriptions == null) { |
| mDescriptions = ""; |
| } |
| } |
| return mDescriptions; |
| } |
| |
| @Nullable |
| public String getQuantityData(@NonNull String language, @NonNull Quantity quantity) { |
| String data = getLocaleData(language); |
| if (data == null) { |
| return null; |
| } |
| String quantityDeclaration = quantity.name() + "{"; |
| int quantityStart = data.indexOf(quantityDeclaration); |
| if (quantityStart == -1) { |
| return null; |
| } |
| int quantityEnd = findBalancedEnd(data, quantityStart); |
| if (quantityEnd == -1) { |
| return null; |
| } |
| //String s = data.substring(quantityStart + quantityDeclaration.length(), quantityEnd); |
| StringBuilder sb = new StringBuilder(); |
| boolean inString = false; |
| for (int i = quantityStart + quantityDeclaration.length(); i < quantityEnd; i++) { |
| char c = data.charAt(i); |
| if (c == '"') { |
| inString = !inString; |
| } else if (inString) { |
| sb.append(c); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| @Nullable |
| public String getSetName(@NonNull String language) { |
| String name = mSetNamePerLanguage.get(language); |
| if (name == null) { |
| name = findSetName(language); |
| if (name == null) { |
| name = ""; // Store "" instead of null so we remember search result |
| } |
| mSetNamePerLanguage.put(language, name); |
| } |
| |
| return name.isEmpty() ? null : name; |
| } |
| |
| @Nullable |
| private String findSetName(@NonNull String language) { |
| String data = getPluralsDescriptions(); |
| int index = data.indexOf("locales{"); |
| if (index == -1) { |
| return null; |
| } |
| int end = data.indexOf("locales_ordinals{", index + 1); |
| if (end == -1) { |
| return null; |
| } |
| String languageDeclaration = " " + language + "{\""; |
| index = data.indexOf(languageDeclaration); |
| if (index == -1 || index >= end) { |
| return null; |
| } |
| int setEnd = data.indexOf('\"', index + languageDeclaration.length()); |
| if (setEnd == -1) { |
| return null; |
| } |
| return data.substring(index + languageDeclaration.length(), setEnd).trim(); |
| } |
| |
| @Nullable |
| public String getLocaleData(@NonNull String language) { |
| String set = getSetName(language); |
| if (set == null) { |
| return null; |
| } |
| String data = getPluralsDescriptions(); |
| int setStart = data.indexOf(set + "{", mRuleSetOffset); |
| if (setStart == -1) { |
| return null; |
| } |
| int setEnd = findBalancedEnd(data, setStart); |
| if (setEnd == -1) { |
| return null; |
| } |
| return data.substring(setStart + set.length() + 1, setEnd); |
| } |
| |
| private static int findBalancedEnd(String data, int offset) { |
| int balance = 0; |
| int length = data.length(); |
| for (; offset < length; offset++) { |
| char c = data.charAt(offset); |
| if (c == '{') { |
| balance++; |
| } else if (c == '}') { |
| balance--; |
| if (balance == 0) { |
| return offset; |
| } |
| } |
| } |
| |
| return -1; |
| } |
| } |
| } |