| /* |
| ******************************************************************************* |
| * Copyright (C) 2004-2015, International Business Machines Corporation and |
| * others. All Rights Reserved. |
| ******************************************************************************* |
| */ |
| package com.ibm.icu.impl; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.ref.SoftReference; |
| import java.nio.ByteBuffer; |
| import java.nio.CharBuffer; |
| import java.nio.IntBuffer; |
| |
| import com.ibm.icu.impl.UResource.ArraySink; |
| import com.ibm.icu.impl.UResource.TableSink; |
| import com.ibm.icu.util.ICUException; |
| import com.ibm.icu.util.ICUUncheckedIOException; |
| import com.ibm.icu.util.ULocale; |
| import com.ibm.icu.util.UResourceBundle; |
| import com.ibm.icu.util.UResourceTypeMismatchException; |
| import com.ibm.icu.util.VersionInfo; |
| |
| /** |
| * This class reads the *.res resource bundle format. |
| * |
| * For the file format documentation see ICU4C's source/common/uresdata.h file. |
| */ |
| public final class ICUResourceBundleReader { |
| /** |
| * File format version that this class understands. |
| * "ResB" |
| */ |
| private static final int DATA_FORMAT = 0x52657342; |
| private static final class IsAcceptable implements ICUBinary.Authenticate { |
| // @Override when we switch to Java 6 |
| public boolean isDataVersionAcceptable(byte formatVersion[]) { |
| return |
| (formatVersion[0] == 1 && (formatVersion[1] & 0xff) >= 1) || |
| (2 <= formatVersion[0] && formatVersion[0] <= 3); |
| } |
| } |
| private static final IsAcceptable IS_ACCEPTABLE = new IsAcceptable(); |
| |
| /* indexes[] value names; indexes are generally 32-bit (Resource) indexes */ |
| /** |
| * [0] contains the length of indexes[] |
| * which is at most URES_INDEX_TOP of the latest format version |
| * |
| * formatVersion==1: all bits contain the length of indexes[] |
| * but the length is much less than 0xff; |
| * formatVersion>1: |
| * only bits 7..0 contain the length of indexes[], |
| * bits 31..8 are reserved and set to 0 |
| * formatVersion>=3: |
| * bits 31..8 poolStringIndexLimit bits 23..0 |
| */ |
| private static final int URES_INDEX_LENGTH = 0; |
| /** |
| * [1] contains the top of the key strings, |
| * same as the bottom of resources or UTF-16 strings, rounded up |
| */ |
| private static final int URES_INDEX_KEYS_TOP = 1; |
| /** [2] contains the top of all resources */ |
| //ivate static final int URES_INDEX_RESOURCES_TOP = 2; |
| /** |
| * [3] contains the top of the bundle, |
| * in case it were ever different from [2] |
| */ |
| private static final int URES_INDEX_BUNDLE_TOP = 3; |
| /** [4] max. length of any table */ |
| private static final int URES_INDEX_MAX_TABLE_LENGTH = 4; |
| /** |
| * [5] attributes bit set, see URES_ATT_* (new in formatVersion 1.2) |
| * |
| * formatVersion>=3: |
| * bits 31..16 poolStringIndex16Limit |
| * bits 15..12 poolStringIndexLimit bits 27..24 |
| */ |
| private static final int URES_INDEX_ATTRIBUTES = 5; |
| /** |
| * [6] top of the 16-bit units (UTF-16 string v2 UChars, URES_TABLE16, URES_ARRAY16), |
| * rounded up (new in formatVersion 2.0, ICU 4.4) |
| */ |
| private static final int URES_INDEX_16BIT_TOP = 6; |
| /** [7] checksum of the pool bundle (new in formatVersion 2.0, ICU 4.4) */ |
| private static final int URES_INDEX_POOL_CHECKSUM = 7; |
| //ivate static final int URES_INDEX_TOP = 8; |
| |
| /* |
| * Nofallback attribute, attribute bit 0 in indexes[URES_INDEX_ATTRIBUTES]. |
| * New in formatVersion 1.2 (ICU 3.6). |
| * |
| * If set, then this resource bundle is a standalone bundle. |
| * If not set, then the bundle participates in locale fallback, eventually |
| * all the way to the root bundle. |
| * If indexes[] is missing or too short, then the attribute cannot be determined |
| * reliably. Dependency checking should ignore such bundles, and loading should |
| * use fallbacks. |
| */ |
| private static final int URES_ATT_NO_FALLBACK = 1; |
| |
| /* |
| * Attributes for bundles that are, or use, a pool bundle. |
| * A pool bundle provides key strings that are shared among several other bundles |
| * to reduce their total size. |
| * New in formatVersion 2 (ICU 4.4). |
| */ |
| private static final int URES_ATT_IS_POOL_BUNDLE = 2; |
| private static final int URES_ATT_USES_POOL_BUNDLE = 4; |
| |
| private static final CharBuffer EMPTY_16_BIT_UNITS = CharBuffer.wrap("\0"); // read-only |
| |
| /** |
| * Objects with more value bytes are stored in SoftReferences. |
| * Smaller objects (which are not much larger than a SoftReference) |
| * are stored directly, avoiding the overhead of the reference. |
| */ |
| static final int LARGE_SIZE = 24; |
| |
| private static final boolean DEBUG = false; |
| |
| private int /* formatVersion, */ dataVersion; |
| |
| // See the ResourceData struct in ICU4C/source/common/uresdata.h. |
| /** |
| * Buffer of all of the resource bundle bytes after the header. |
| * (equivalent of C++ pRoot) |
| */ |
| private ByteBuffer bytes; |
| private byte[] keyBytes; |
| private CharBuffer b16BitUnits; |
| private ICUResourceBundleReader poolBundleReader; |
| private int rootRes; |
| private int localKeyLimit; |
| private int poolStringIndexLimit; |
| private int poolStringIndex16Limit; |
| private boolean noFallback; /* see URES_ATT_NO_FALLBACK */ |
| private boolean isPoolBundle; |
| private boolean usesPoolBundle; |
| private int poolCheckSum; |
| |
| private ResourceCache resourceCache; |
| |
| private static ReaderCache CACHE = new ReaderCache(); |
| private static final ICUResourceBundleReader NULL_READER = new ICUResourceBundleReader(); |
| |
| private static class ReaderCacheKey { |
| final String baseName; |
| final String localeID; |
| |
| ReaderCacheKey(String baseName, String localeID) { |
| this.baseName = (baseName == null) ? "" : baseName; |
| this.localeID = (localeID == null) ? "" : localeID; |
| } |
| |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (!(obj instanceof ReaderCacheKey)) { |
| return false; |
| } |
| ReaderCacheKey info = (ReaderCacheKey)obj; |
| return this.baseName.equals(info.baseName) |
| && this.localeID.equals(info.localeID); |
| } |
| |
| public int hashCode() { |
| return baseName.hashCode() ^ localeID.hashCode(); |
| } |
| } |
| |
| private static class ReaderCache extends SoftCache<ReaderCacheKey, ICUResourceBundleReader, ClassLoader> { |
| /* (non-Javadoc) |
| * @see com.ibm.icu.impl.CacheBase#createInstance(java.lang.Object, java.lang.Object) |
| */ |
| @Override |
| protected ICUResourceBundleReader createInstance(ReaderCacheKey key, ClassLoader loader) { |
| String fullName = ICUResourceBundleReader.getFullName(key.baseName, key.localeID); |
| try { |
| ByteBuffer inBytes; |
| if (key.baseName != null && key.baseName.startsWith(ICUData.ICU_BASE_NAME)) { |
| String itemPath = fullName.substring(ICUData.ICU_BASE_NAME.length() + 1); |
| inBytes = ICUBinary.getData(loader, fullName, itemPath); |
| if (inBytes == null) { |
| return NULL_READER; |
| } |
| } else { |
| @SuppressWarnings("resource") // Closed by getByteBufferFromInputStreamAndCloseStream(). |
| InputStream stream = ICUData.getStream(loader, fullName); |
| if (stream == null) { |
| return NULL_READER; |
| } |
| inBytes = ICUBinary.getByteBufferFromInputStreamAndCloseStream(stream); |
| } |
| return new ICUResourceBundleReader(inBytes, key.baseName, key.localeID, loader); |
| } catch (IOException ex) { |
| throw new ICUUncheckedIOException("Data file " + fullName + " is corrupt - " + ex.getMessage(), ex); |
| } |
| } |
| } |
| |
| /* |
| * Default constructor, just used for NULL_READER. |
| */ |
| private ICUResourceBundleReader() { |
| } |
| |
| private ICUResourceBundleReader(ByteBuffer inBytes, |
| String baseName, String localeID, |
| ClassLoader loader) throws IOException { |
| init(inBytes); |
| |
| // set pool bundle if necessary |
| if (usesPoolBundle) { |
| poolBundleReader = getReader(baseName, "pool", loader); |
| if (!poolBundleReader.isPoolBundle) { |
| throw new IllegalStateException("pool.res is not a pool bundle"); |
| } |
| if (poolBundleReader.poolCheckSum != poolCheckSum) { |
| throw new IllegalStateException("pool.res has a different checksum than this bundle"); |
| } |
| } |
| } |
| |
| static ICUResourceBundleReader getReader(String baseName, String localeID, ClassLoader root) { |
| ReaderCacheKey info = new ReaderCacheKey(baseName, localeID); |
| ICUResourceBundleReader reader = CACHE.getInstance(info, root); |
| if (reader == NULL_READER) { |
| return null; |
| } |
| return reader; |
| } |
| |
| // See res_init() in ICU4C/source/common/uresdata.c. |
| private void init(ByteBuffer inBytes) throws IOException { |
| dataVersion = ICUBinary.readHeader(inBytes, DATA_FORMAT, IS_ACCEPTABLE); |
| int majorFormatVersion = inBytes.get(16); |
| bytes = ICUBinary.sliceWithOrder(inBytes); |
| int dataLength = bytes.remaining(); |
| |
| if(DEBUG) System.out.println("The ByteBuffer is direct (memory-mapped): " + bytes.isDirect()); |
| if(DEBUG) System.out.println("The available bytes in the buffer before reading the data: " + dataLength); |
| |
| rootRes = bytes.getInt(0); |
| |
| // Bundles with formatVersion 1.1 and later contain an indexes[] array. |
| // We need it so that we can read the key string bytes up front, for lookup performance. |
| |
| // read the variable-length indexes[] array |
| int indexes0 = getIndexesInt(URES_INDEX_LENGTH); |
| int indexLength = indexes0 & 0xff; |
| if(indexLength <= URES_INDEX_MAX_TABLE_LENGTH) { |
| throw new ICUException("not enough indexes"); |
| } |
| int bundleTop; |
| if(dataLength < ((1 + indexLength) << 2) || |
| dataLength < ((bundleTop = getIndexesInt(URES_INDEX_BUNDLE_TOP)) << 2)) { |
| throw new ICUException("not enough bytes"); |
| } |
| int maxOffset = bundleTop - 1; |
| |
| if (majorFormatVersion >= 3) { |
| // In formatVersion 1, the indexLength took up this whole int. |
| // In version 2, bits 31..8 were reserved and always 0. |
| // In version 3, they contain bits 23..0 of the poolStringIndexLimit. |
| // Bits 27..24 are in indexes[URES_INDEX_ATTRIBUTES] bits 15..12. |
| poolStringIndexLimit = indexes0 >>> 8; |
| } |
| if(indexLength > URES_INDEX_ATTRIBUTES) { |
| // determine if this resource bundle falls back to a parent bundle |
| // along normal locale ID fallback |
| int att = getIndexesInt(URES_INDEX_ATTRIBUTES); |
| noFallback = (att & URES_ATT_NO_FALLBACK) != 0; |
| isPoolBundle = (att & URES_ATT_IS_POOL_BUNDLE) != 0; |
| usesPoolBundle = (att & URES_ATT_USES_POOL_BUNDLE) != 0; |
| poolStringIndexLimit |= (att & 0xf000) << 12; // bits 15..12 -> 27..24 |
| poolStringIndex16Limit = att >>> 16; |
| } |
| |
| int keysBottom = 1 + indexLength; |
| int keysTop = getIndexesInt(URES_INDEX_KEYS_TOP); |
| if(keysTop > keysBottom) { |
| // Deserialize the key strings up front. |
| // Faster table item search at the cost of slower startup and some heap memory. |
| if(isPoolBundle) { |
| // Shift the key strings down: |
| // Pool bundle key strings are used with a 0-based index, |
| // unlike regular bundles' key strings for which indexes |
| // are based on the start of the bundle data. |
| keyBytes = new byte[(keysTop - keysBottom) << 2]; |
| bytes.position(keysBottom << 2); |
| } else { |
| localKeyLimit = keysTop << 2; |
| keyBytes = new byte[localKeyLimit]; |
| } |
| bytes.get(keyBytes); |
| } |
| |
| // Read the array of 16-bit units. |
| if(indexLength > URES_INDEX_16BIT_TOP) { |
| int _16BitTop = getIndexesInt(URES_INDEX_16BIT_TOP); |
| if(_16BitTop > keysTop) { |
| int num16BitUnits = (_16BitTop - keysTop) * 2; |
| bytes.position(keysTop << 2); |
| b16BitUnits = bytes.asCharBuffer(); |
| b16BitUnits.limit(num16BitUnits); |
| maxOffset |= num16BitUnits - 1; |
| } else { |
| b16BitUnits = EMPTY_16_BIT_UNITS; |
| } |
| } else { |
| b16BitUnits = EMPTY_16_BIT_UNITS; |
| } |
| |
| if(indexLength > URES_INDEX_POOL_CHECKSUM) { |
| poolCheckSum = getIndexesInt(URES_INDEX_POOL_CHECKSUM); |
| } |
| |
| if(!isPoolBundle || b16BitUnits.length() > 1) { |
| resourceCache = new ResourceCache(maxOffset); |
| } |
| |
| // Reset the position for future .asCharBuffer() etc. |
| bytes.position(0); |
| } |
| |
| private int getIndexesInt(int i) { |
| return bytes.getInt((1 + i) << 2); |
| } |
| |
| VersionInfo getVersion() { |
| return ICUBinary.getVersionInfoFromCompactInt(dataVersion); |
| } |
| |
| int getRootResource() { |
| return rootRes; |
| } |
| boolean getNoFallback() { |
| return noFallback; |
| } |
| boolean getUsesPoolBundle() { |
| return usesPoolBundle; |
| } |
| |
| static int RES_GET_TYPE(int res) { |
| return res >>> 28; |
| } |
| private static int RES_GET_OFFSET(int res) { |
| return res & 0x0fffffff; |
| } |
| private int getResourceByteOffset(int offset) { |
| return offset << 2; |
| } |
| /* get signed and unsigned integer values directly from the Resource handle */ |
| static int RES_GET_INT(int res) { |
| return (res << 4) >> 4; |
| } |
| static int RES_GET_UINT(int res) { |
| return res & 0x0fffffff; |
| } |
| static boolean URES_IS_ARRAY(int type) { |
| return type == UResourceBundle.ARRAY || type == ICUResourceBundle.ARRAY16; |
| } |
| static boolean URES_IS_TABLE(int type) { |
| return type==UResourceBundle.TABLE || type==ICUResourceBundle.TABLE16 || type==ICUResourceBundle.TABLE32; |
| } |
| |
| private static final byte[] emptyBytes = new byte[0]; |
| private static final ByteBuffer emptyByteBuffer = ByteBuffer.allocate(0).asReadOnlyBuffer(); |
| private static final char[] emptyChars = new char[0]; |
| private static final int[] emptyInts = new int[0]; |
| private static final String emptyString = ""; |
| private static final Array EMPTY_ARRAY = new Array(); |
| private static final Table EMPTY_TABLE = new Table(); |
| |
| private char[] getChars(int offset, int count) { |
| char[] chars = new char[count]; |
| if (count <= 16) { |
| for(int i = 0; i < count; offset += 2, ++i) { |
| chars[i] = bytes.getChar(offset); |
| } |
| } else { |
| CharBuffer temp = bytes.asCharBuffer(); |
| temp.position(offset / 2); |
| temp.get(chars); |
| } |
| return chars; |
| } |
| private int getInt(int offset) { |
| return bytes.getInt(offset); |
| } |
| private int[] getInts(int offset, int count) { |
| int[] ints = new int[count]; |
| if (count <= 16) { |
| for(int i = 0; i < count; offset += 4, ++i) { |
| ints[i] = bytes.getInt(offset); |
| } |
| } else { |
| IntBuffer temp = bytes.asIntBuffer(); |
| temp.position(offset / 4); |
| temp.get(ints); |
| } |
| return ints; |
| } |
| private char[] getTable16KeyOffsets(int offset) { |
| int length = b16BitUnits.charAt(offset++); |
| if(length > 0) { |
| char[] result = new char[length]; |
| if(length <= 16) { |
| for(int i = 0; i < length; ++i) { |
| result[i] = b16BitUnits.charAt(offset++); |
| } |
| } else { |
| CharBuffer temp = b16BitUnits.duplicate(); |
| temp.position(offset); |
| temp.get(result); |
| } |
| return result; |
| } else { |
| return emptyChars; |
| } |
| } |
| private char[] getTableKeyOffsets(int offset) { |
| int length = bytes.getChar(offset); |
| if(length > 0) { |
| return getChars(offset + 2, length); |
| } else { |
| return emptyChars; |
| } |
| } |
| private int[] getTable32KeyOffsets(int offset) { |
| int length = getInt(offset); |
| if(length > 0) { |
| return getInts(offset + 4, length); |
| } else { |
| return emptyInts; |
| } |
| } |
| |
| private static String makeKeyStringFromBytes(byte[] keyBytes, int keyOffset) { |
| StringBuilder sb = new StringBuilder(); |
| byte b; |
| while((b = keyBytes[keyOffset]) != 0) { |
| ++keyOffset; |
| sb.append((char)b); |
| } |
| return sb.toString(); |
| } |
| private String getKey16String(int keyOffset) { |
| if(keyOffset < localKeyLimit) { |
| return makeKeyStringFromBytes(keyBytes, keyOffset); |
| } else { |
| return makeKeyStringFromBytes(poolBundleReader.keyBytes, keyOffset - localKeyLimit); |
| } |
| } |
| private String getKey32String(int keyOffset) { |
| if(keyOffset >= 0) { |
| return makeKeyStringFromBytes(keyBytes, keyOffset); |
| } else { |
| return makeKeyStringFromBytes(poolBundleReader.keyBytes, keyOffset & 0x7fffffff); |
| } |
| } |
| private void setKeyFromKey16(int keyOffset, UResource.Key key) { |
| if(keyOffset < localKeyLimit) { |
| key.setBytes(keyBytes, keyOffset); |
| } else { |
| key.setBytes(poolBundleReader.keyBytes, keyOffset - localKeyLimit); |
| } |
| } |
| private void setKeyFromKey32(int keyOffset, UResource.Key key) { |
| if(keyOffset >= 0) { |
| key.setBytes(keyBytes, keyOffset); |
| } else { |
| key.setBytes(poolBundleReader.keyBytes, keyOffset & 0x7fffffff); |
| } |
| } |
| private int compareKeys(CharSequence key, char keyOffset) { |
| if(keyOffset < localKeyLimit) { |
| return ICUBinary.compareKeys(key, keyBytes, keyOffset); |
| } else { |
| return ICUBinary.compareKeys(key, poolBundleReader.keyBytes, keyOffset - localKeyLimit); |
| } |
| } |
| private int compareKeys32(CharSequence key, int keyOffset) { |
| if(keyOffset >= 0) { |
| return ICUBinary.compareKeys(key, keyBytes, keyOffset); |
| } else { |
| return ICUBinary.compareKeys(key, poolBundleReader.keyBytes, keyOffset & 0x7fffffff); |
| } |
| } |
| |
| /** |
| * @return a string from the local bundle's b16BitUnits at the local offset |
| */ |
| String getStringV2(int res) { |
| // Use the pool bundle's resource cache for pool bundle strings; |
| // use the local bundle's cache for local strings. |
| // The cache requires a resource word with the proper type, |
| // and with an offset that is local to this bundle so that the offset fits |
| // within the maximum number of bits for which the cache was constructed. |
| assert RES_GET_TYPE(res) == ICUResourceBundle.STRING_V2; |
| int offset = RES_GET_OFFSET(res); |
| assert offset != 0; // handled by the caller |
| Object value = resourceCache.get(res); |
| if(value != null) { |
| return (String)value; |
| } |
| String s; |
| int first = b16BitUnits.charAt(offset); |
| if((first&0xfffffc00)!=0xdc00) { // C: if(!U16_IS_TRAIL(first)) { |
| if(first==0) { |
| return emptyString; // Should not occur, but is not forbidden. |
| } |
| StringBuilder sb = new StringBuilder(); |
| sb.append((char)first); |
| char c; |
| while((c = b16BitUnits.charAt(++offset)) != 0) { |
| sb.append(c); |
| } |
| s = sb.toString(); |
| } else { |
| int length; |
| if(first<0xdfef) { |
| length=first&0x3ff; |
| ++offset; |
| } else if(first<0xdfff) { |
| length=((first-0xdfef)<<16)|b16BitUnits.charAt(offset+1); |
| offset+=2; |
| } else { |
| length=((int)b16BitUnits.charAt(offset+1)<<16)|b16BitUnits.charAt(offset+2); |
| offset+=3; |
| } |
| // Cast up to CharSequence to insulate against the CharBuffer.subSequence() return type change |
| // which makes code compiled for a newer JDK (7 and up) not run on an older one (6 and below). |
| s = ((CharSequence) b16BitUnits).subSequence(offset, offset + length).toString(); |
| } |
| return (String)resourceCache.putIfAbsent(res, s, s.length() * 2); |
| } |
| |
| private String makeStringFromBytes(int offset, int length) { |
| if (length <= 16) { |
| StringBuilder sb = new StringBuilder(length); |
| for (int i = 0; i < length; offset += 2, ++i) { |
| sb.append(bytes.getChar(offset)); |
| } |
| return sb.toString(); |
| } else { |
| CharSequence cs = bytes.asCharBuffer(); |
| offset /= 2; |
| return cs.subSequence(offset, offset + length).toString(); |
| } |
| } |
| |
| String getString(int res) { |
| int offset=RES_GET_OFFSET(res); |
| if(res != offset /* RES_GET_TYPE(res) != URES_STRING */ && |
| RES_GET_TYPE(res) != ICUResourceBundle.STRING_V2) { |
| return null; |
| } |
| if(offset == 0) { |
| return emptyString; |
| } |
| if (res != offset) { // STRING_V2 |
| if (offset < poolStringIndexLimit) { |
| return poolBundleReader.getStringV2(res); |
| } else { |
| return getStringV2(res - poolStringIndexLimit); |
| } |
| } |
| Object value = resourceCache.get(res); |
| if(value != null) { |
| return (String)value; |
| } |
| offset=getResourceByteOffset(offset); |
| int length = getInt(offset); |
| String s = makeStringFromBytes(offset+4, length); |
| return (String)resourceCache.putIfAbsent(res, s, s.length() * 2); |
| } |
| |
| /** |
| * CLDR string value "∅∅∅"=="\u2205\u2205\u2205" prevents fallback to the parent bundle. |
| */ |
| private boolean isNoInheritanceMarker(int res) { |
| int offset = RES_GET_OFFSET(res); |
| if (offset == 0) { |
| // empty string |
| } else if (res == offset) { |
| offset = getResourceByteOffset(offset); |
| return getInt(offset) == 3 && bytes.getChar(offset + 4) == 0x2205 && |
| bytes.getChar(offset + 6) == 0x2205 && bytes.getChar(offset + 8) == 0x2205; |
| } else if (RES_GET_TYPE(res) == ICUResourceBundle.STRING_V2) { |
| if (offset < poolStringIndexLimit) { |
| return poolBundleReader.isStringV2NoInheritanceMarker(offset); |
| } else { |
| return isStringV2NoInheritanceMarker(offset - poolStringIndexLimit); |
| } |
| } |
| return false; |
| } |
| |
| private boolean isStringV2NoInheritanceMarker(int offset) { |
| int first = b16BitUnits.charAt(offset); |
| if (first == 0x2205) { // implicit length |
| return b16BitUnits.charAt(offset + 1) == 0x2205 && |
| b16BitUnits.charAt(offset + 2) == 0x2205 && |
| b16BitUnits.charAt(offset + 3) == 0; |
| } else if (first == 0xdc03) { // explicit length 3 (should not occur) |
| return b16BitUnits.charAt(offset + 1) == 0x2205 && |
| b16BitUnits.charAt(offset + 2) == 0x2205 && |
| b16BitUnits.charAt(offset + 3) == 0x2205; |
| } else { |
| // Assume that the string has not been stored with more length units than necessary. |
| return false; |
| } |
| } |
| |
| String getAlias(int res) { |
| int offset=RES_GET_OFFSET(res); |
| int length; |
| if(RES_GET_TYPE(res)==ICUResourceBundle.ALIAS) { |
| if(offset==0) { |
| return emptyString; |
| } else { |
| Object value = resourceCache.get(res); |
| if(value != null) { |
| return (String)value; |
| } |
| offset=getResourceByteOffset(offset); |
| length=getInt(offset); |
| String s = makeStringFromBytes(offset + 4, length); |
| return (String)resourceCache.putIfAbsent(res, s, length * 2); |
| } |
| } else { |
| return null; |
| } |
| } |
| |
| byte[] getBinary(int res, byte[] ba) { |
| int offset=RES_GET_OFFSET(res); |
| int length; |
| if(RES_GET_TYPE(res)==UResourceBundle.BINARY) { |
| if(offset==0) { |
| return emptyBytes; |
| } else { |
| offset=getResourceByteOffset(offset); |
| length=getInt(offset); |
| if(length==0) { |
| return emptyBytes; |
| } |
| // Not cached: The array would have to be cloned anyway because |
| // the cache must not be writable via the returned reference. |
| if(ba==null || ba.length!=length) { |
| ba=new byte[length]; |
| } |
| offset += 4; |
| if(length <= 16) { |
| for(int i = 0; i < length; ++i) { |
| ba[i] = bytes.get(offset++); |
| } |
| } else { |
| ByteBuffer temp = bytes.duplicate(); |
| temp.position(offset); |
| temp.get(ba); |
| } |
| return ba; |
| } |
| } else { |
| return null; |
| } |
| } |
| |
| ByteBuffer getBinary(int res) { |
| int offset=RES_GET_OFFSET(res); |
| int length; |
| if(RES_GET_TYPE(res)==UResourceBundle.BINARY) { |
| if(offset==0) { |
| // Don't just |
| // return emptyByteBuffer; |
| // in case it matters whether the buffer's mark is defined or undefined. |
| return emptyByteBuffer.duplicate(); |
| } else { |
| // Not cached: The returned buffer is small (shares its bytes with the bundle) |
| // and usually quickly discarded after use. |
| // Also, even a cached buffer would have to be cloned because it is mutable |
| // (position & mark). |
| offset=getResourceByteOffset(offset); |
| length=getInt(offset); |
| if(length == 0) { |
| return emptyByteBuffer.duplicate(); |
| } |
| offset += 4; |
| ByteBuffer result = bytes.duplicate(); |
| result.position(offset).limit(offset + length); |
| result = ICUBinary.sliceWithOrder(result); |
| if(!result.isReadOnly()) { |
| result = result.asReadOnlyBuffer(); |
| } |
| return result; |
| } |
| } else { |
| return null; |
| } |
| } |
| |
| int[] getIntVector(int res) { |
| int offset=RES_GET_OFFSET(res); |
| int length; |
| if(RES_GET_TYPE(res)==UResourceBundle.INT_VECTOR) { |
| if(offset==0) { |
| return emptyInts; |
| } else { |
| // Not cached: The array would have to be cloned anyway because |
| // the cache must not be writable via the returned reference. |
| offset=getResourceByteOffset(offset); |
| length=getInt(offset); |
| return getInts(offset+4, length); |
| } |
| } else { |
| return null; |
| } |
| } |
| |
| private int getArrayLength(int res) { |
| int offset = RES_GET_OFFSET(res); |
| if(offset == 0) { |
| return 0; |
| } |
| int type = RES_GET_TYPE(res); |
| if(type == UResourceBundle.ARRAY) { |
| offset = getResourceByteOffset(offset); |
| return getInt(offset); |
| } else if(type == ICUResourceBundle.ARRAY16) { |
| return b16BitUnits.charAt(offset); |
| } else { |
| return 0; |
| } |
| } |
| |
| Array getArray(int res) { |
| int type=RES_GET_TYPE(res); |
| if(!URES_IS_ARRAY(type)) { |
| return null; |
| } |
| int offset=RES_GET_OFFSET(res); |
| if(offset == 0) { |
| return EMPTY_ARRAY; |
| } |
| Object value = resourceCache.get(res); |
| if(value != null) { |
| return (Array)value; |
| } |
| Array array = (type == UResourceBundle.ARRAY) ? |
| new Array32(this, offset) : new Array16(this, offset); |
| return (Array)resourceCache.putIfAbsent(res, array, 0); |
| } |
| |
| private int getTableLength(int res) { |
| int offset = RES_GET_OFFSET(res); |
| if(offset == 0) { |
| return 0; |
| } |
| int type = RES_GET_TYPE(res); |
| if(type == UResourceBundle.TABLE) { |
| offset = getResourceByteOffset(offset); |
| return bytes.getChar(offset); |
| } else if(type == ICUResourceBundle.TABLE16) { |
| return b16BitUnits.charAt(offset); |
| } else if(type == ICUResourceBundle.TABLE32) { |
| offset = getResourceByteOffset(offset); |
| return getInt(offset); |
| } else { |
| return 0; |
| } |
| } |
| |
| Table getTable(int res) { |
| int type = RES_GET_TYPE(res); |
| if(!URES_IS_TABLE(type)) { |
| return null; |
| } |
| int offset = RES_GET_OFFSET(res); |
| if(offset == 0) { |
| return EMPTY_TABLE; |
| } |
| Object value = resourceCache.get(res); |
| if(value != null) { |
| return (Table)value; |
| } |
| Table table; |
| int size; // Use size = 0 to never use SoftReferences for Tables? |
| if(type == UResourceBundle.TABLE) { |
| table = new Table1632(this, offset); |
| size = table.getSize() * 2; |
| } else if(type == ICUResourceBundle.TABLE16) { |
| table = new Table16(this, offset); |
| size = table.getSize() * 2; |
| } else /* type == ICUResourceBundle.TABLE32 */ { |
| table = new Table32(this, offset); |
| size = table.getSize() * 4; |
| } |
| return (Table)resourceCache.putIfAbsent(res, table, size); |
| } |
| |
| // ICUResource.Value --------------------------------------------------- *** |
| |
| /** |
| * From C++ uresdata.c gPublicTypes[URES_LIMIT]. |
| */ |
| private static int PUBLIC_TYPES[] = { |
| UResourceBundle.STRING, |
| UResourceBundle.BINARY, |
| UResourceBundle.TABLE, |
| ICUResourceBundle.ALIAS, |
| |
| UResourceBundle.TABLE, /* URES_TABLE32 */ |
| UResourceBundle.TABLE, /* URES_TABLE16 */ |
| UResourceBundle.STRING, /* URES_STRING_V2 */ |
| UResourceBundle.INT, |
| |
| UResourceBundle.ARRAY, |
| UResourceBundle.ARRAY, /* URES_ARRAY16 */ |
| UResourceBundle.NONE, |
| UResourceBundle.NONE, |
| |
| UResourceBundle.NONE, |
| UResourceBundle.NONE, |
| UResourceBundle.INT_VECTOR, |
| UResourceBundle.NONE |
| }; |
| |
| static class ReaderValue extends UResource.Value { |
| ICUResourceBundleReader reader; |
| private int res; |
| |
| @Override |
| public int getType() { |
| return PUBLIC_TYPES[RES_GET_TYPE(res)]; |
| } |
| |
| @Override |
| public String getString() { |
| String s = reader.getString(res); |
| if (s == null) { |
| throw new UResourceTypeMismatchException(""); |
| } |
| return s; |
| } |
| |
| @Override |
| public String getAliasString() { |
| String s = reader.getAlias(res); |
| if (s == null) { |
| throw new UResourceTypeMismatchException(""); |
| } |
| return s; |
| } |
| |
| @Override |
| public int getInt() { |
| if (RES_GET_TYPE(res) != UResourceBundle.INT) { |
| throw new UResourceTypeMismatchException(""); |
| } |
| return RES_GET_INT(res); |
| } |
| |
| @Override |
| public int getUInt() { |
| if (RES_GET_TYPE(res) != UResourceBundle.INT) { |
| throw new UResourceTypeMismatchException(""); |
| } |
| return RES_GET_UINT(res); |
| } |
| |
| @Override |
| public int[] getIntVector() { |
| int[] iv = reader.getIntVector(res); |
| if (iv == null) { |
| throw new UResourceTypeMismatchException(""); |
| } |
| return iv; |
| } |
| |
| @Override |
| public ByteBuffer getBinary() { |
| ByteBuffer bb = reader.getBinary(res); |
| if (bb == null) { |
| throw new UResourceTypeMismatchException(""); |
| } |
| return bb; |
| } |
| } |
| |
| // Container value classes --------------------------------------------- *** |
| |
| static class Container { |
| protected int size; |
| protected int itemsOffset; |
| |
| final int getSize() { |
| return size; |
| } |
| int getContainerResource(ICUResourceBundleReader reader, int index) { |
| return ICUResourceBundle.RES_BOGUS; |
| } |
| protected int getContainer16Resource(ICUResourceBundleReader reader, int index) { |
| if (index < 0 || size <= index) { |
| return ICUResourceBundle.RES_BOGUS; |
| } |
| int res16 = reader.b16BitUnits.charAt(itemsOffset + index); |
| if (res16 < reader.poolStringIndex16Limit) { |
| // Pool string, nothing to do. |
| } else { |
| // Local string, adjust the 16-bit offset to a regular one, |
| // with a larger pool string index limit. |
| res16 = res16 - reader.poolStringIndex16Limit + reader.poolStringIndexLimit; |
| } |
| return (ICUResourceBundle.STRING_V2 << 28) | res16; |
| } |
| protected int getContainer32Resource(ICUResourceBundleReader reader, int index) { |
| if (index < 0 || size <= index) { |
| return ICUResourceBundle.RES_BOGUS; |
| } |
| return reader.getInt(itemsOffset + 4 * index); |
| } |
| int getResource(ICUResourceBundleReader reader, String resKey) { |
| return getContainerResource(reader, Integer.parseInt(resKey)); |
| } |
| Container() { |
| } |
| } |
| static class Array extends Container { |
| Array() {} |
| void getAllItems(ICUResourceBundleReader reader, |
| UResource.Key key, ReaderValue value, ArraySink sink) { |
| for (int i = 0; i < size; ++i) { |
| int res = getContainerResource(reader, i); |
| int type = RES_GET_TYPE(res); |
| if (URES_IS_ARRAY(type)) { |
| int numItems = reader.getArrayLength(res); |
| ArraySink subSink = sink.getOrCreateArraySink(i, numItems); |
| if (subSink != null) { |
| Array array = reader.getArray(res); |
| assert(array.size == numItems); |
| array.getAllItems(reader, key, value, subSink); |
| } |
| } else if (URES_IS_TABLE(type)) { |
| int numItems = reader.getTableLength(res); |
| TableSink subSink = sink.getOrCreateTableSink(i, numItems); |
| if (subSink != null) { |
| Table table = reader.getTable(res); |
| assert(table.size == numItems); |
| table.getAllItems(reader, key, value, subSink); |
| } |
| /* TODO: settle on how to deal with aliases, port to C++ |
| } else if (type == ICUResourceBundle.ALIAS) { |
| throw new UnsupportedOperationException( |
| "aliases not handled in resource enumeration"); */ |
| } else { |
| value.res = res; |
| sink.put(i, value); |
| } |
| } |
| sink.leave(); |
| } |
| } |
| private static final class Array32 extends Array { |
| @Override |
| int getContainerResource(ICUResourceBundleReader reader, int index) { |
| return getContainer32Resource(reader, index); |
| } |
| Array32(ICUResourceBundleReader reader, int offset) { |
| offset = reader.getResourceByteOffset(offset); |
| size = reader.getInt(offset); |
| itemsOffset = offset + 4; |
| } |
| } |
| private static final class Array16 extends Array { |
| @Override |
| int getContainerResource(ICUResourceBundleReader reader, int index) { |
| return getContainer16Resource(reader, index); |
| } |
| Array16(ICUResourceBundleReader reader, int offset) { |
| size = reader.b16BitUnits.charAt(offset); |
| itemsOffset = offset + 1; |
| } |
| } |
| static class Table extends Container { |
| protected char[] keyOffsets; |
| protected int[] key32Offsets; |
| |
| String getKey(ICUResourceBundleReader reader, int index) { |
| if (index < 0 || size <= index) { |
| return null; |
| } |
| return keyOffsets != null ? |
| reader.getKey16String(keyOffsets[index]) : |
| reader.getKey32String(key32Offsets[index]); |
| } |
| private static final int URESDATA_ITEM_NOT_FOUND = -1; |
| int findTableItem(ICUResourceBundleReader reader, CharSequence key) { |
| int mid, start, limit; |
| int result; |
| |
| /* do a binary search for the key */ |
| start=0; |
| limit=size; |
| while(start<limit) { |
| mid = (start + limit) >>> 1; |
| if (keyOffsets != null) { |
| result = reader.compareKeys(key, keyOffsets[mid]); |
| } else { |
| result = reader.compareKeys32(key, key32Offsets[mid]); |
| } |
| if (result < 0) { |
| limit = mid; |
| } else if (result > 0) { |
| start = mid + 1; |
| } else { |
| /* We found it! */ |
| return mid; |
| } |
| } |
| return URESDATA_ITEM_NOT_FOUND; /* not found or table is empty. */ |
| } |
| @Override |
| int getResource(ICUResourceBundleReader reader, String resKey) { |
| return getContainerResource(reader, findTableItem(reader, resKey)); |
| } |
| void getAllItems(ICUResourceBundleReader reader, |
| UResource.Key key, ReaderValue value, TableSink sink) { |
| for (int i = 0; i < size; ++i) { |
| if (keyOffsets != null) { |
| reader.setKeyFromKey16(keyOffsets[i], key); |
| } else { |
| reader.setKeyFromKey32(key32Offsets[i], key); |
| } |
| int res = getContainerResource(reader, i); |
| int type = RES_GET_TYPE(res); |
| if (URES_IS_ARRAY(type)) { |
| int numItems = reader.getArrayLength(res); |
| ArraySink subSink = sink.getOrCreateArraySink(key, numItems); |
| if (subSink != null) { |
| Array array = reader.getArray(res); |
| assert(array.size == numItems); |
| array.getAllItems(reader, key, value, subSink); |
| } |
| } else if (URES_IS_TABLE(type)) { |
| int numItems = reader.getTableLength(res); |
| TableSink subSink = sink.getOrCreateTableSink(key, numItems); |
| if (subSink != null) { |
| Table table = reader.getTable(res); |
| assert(table.size == numItems); |
| table.getAllItems(reader, key, value, subSink); |
| } |
| /* TODO: settle on how to deal with aliases, port to C++ |
| } else if (type == ICUResourceBundle.ALIAS) { |
| throw new UnsupportedOperationException( |
| "aliases not handled in resource enumeration"); */ |
| } else if (reader.isNoInheritanceMarker(res)) { |
| sink.putNoFallback(key); |
| } else { |
| value.res = res; |
| sink.put(key, value); |
| } |
| } |
| sink.leave(); |
| } |
| Table() { |
| } |
| } |
| private static final class Table1632 extends Table { |
| @Override |
| int getContainerResource(ICUResourceBundleReader reader, int index) { |
| return getContainer32Resource(reader, index); |
| } |
| Table1632(ICUResourceBundleReader reader, int offset) { |
| offset = reader.getResourceByteOffset(offset); |
| keyOffsets = reader.getTableKeyOffsets(offset); |
| size = keyOffsets.length; |
| itemsOffset = offset + 2 * ((size + 2) & ~1); // Skip padding for 4-alignment. |
| } |
| } |
| private static final class Table16 extends Table { |
| @Override |
| int getContainerResource(ICUResourceBundleReader reader, int index) { |
| return getContainer16Resource(reader, index); |
| } |
| Table16(ICUResourceBundleReader reader, int offset) { |
| keyOffsets = reader.getTable16KeyOffsets(offset); |
| size = keyOffsets.length; |
| itemsOffset = offset + 1 + size; |
| } |
| } |
| private static final class Table32 extends Table { |
| @Override |
| int getContainerResource(ICUResourceBundleReader reader, int index) { |
| return getContainer32Resource(reader, index); |
| } |
| Table32(ICUResourceBundleReader reader, int offset) { |
| offset = reader.getResourceByteOffset(offset); |
| key32Offsets = reader.getTable32KeyOffsets(offset); |
| size = key32Offsets.length; |
| itemsOffset = offset + 4 * (1 + size); |
| } |
| } |
| |
| // Resource cache ------------------------------------------------------ *** |
| |
| /** |
| * Cache of some of one resource bundle's resources. |
| * Avoids creating multiple Java objects for the same resource items, |
| * including multiple copies of their contents. |
| * |
| * <p>Mutable objects must not be cached and then returned to the caller |
| * because the cache must not be writable via the returned reference. |
| * |
| * <p>Resources are mapped by their resource integers. |
| * Empty resources with offset 0 cannot be mapped. |
| * Integers need not and should not be cached. |
| * Multiple .res items may share resource offsets (genrb eliminates some duplicates). |
| * |
| * <p>This cache uses int[] and Object[] arrays to minimize object creation |
| * and avoid auto-boxing. |
| * |
| * <p>Large resource objects are stored in SoftReferences. |
| * |
| * <p>For few resources, a small table is used with binary search. |
| * When more resources are cached, then the data structure changes to be faster |
| * but also use more memory. |
| */ |
| private static final class ResourceCache { |
| // Number of items to be stored in a simple array with binary search and insertion sort. |
| private static final int SIMPLE_LENGTH = 32; |
| |
| // When more than SIMPLE_LENGTH items are cached, |
| // then switch to a trie-like tree of levels with different array lengths. |
| private static final int ROOT_BITS = 7; |
| private static final int NEXT_BITS = 6; |
| |
| // Simple table, used when length >= 0. |
| private int[] keys = new int[SIMPLE_LENGTH]; |
| private Object[] values = new Object[SIMPLE_LENGTH]; |
| private int length; |
| |
| // Trie-like tree of levels, used when length < 0. |
| private int maxOffsetBits; |
| /** |
| * Number of bits in each level, each stored in a nibble. |
| */ |
| private int levelBitsList; |
| private Level rootLevel; |
| |
| @SuppressWarnings("unchecked") |
| private static final Object putIfCleared(Object[] values, int index, Object item, int size) { |
| Object value = values[index]; |
| if(!(value instanceof SoftReference)) { |
| assert size < LARGE_SIZE; // Caller should be consistent for each resource. |
| return value; |
| } |
| assert size >= LARGE_SIZE; |
| value = ((SoftReference<Object>)value).get(); |
| if(value != null) { |
| return value; |
| } |
| values[index] = new SoftReference<Object>(item); |
| return item; |
| } |
| |
| private static final class Level { |
| int levelBitsList; |
| int shift; |
| int mask; |
| int[] keys; |
| Object[] values; |
| |
| Level(int levelBitsList, int shift) { |
| this.levelBitsList = levelBitsList; |
| this.shift = shift; |
| int bits = levelBitsList & 0xf; |
| assert bits != 0; |
| int length = 1 << bits; |
| mask = length - 1; |
| keys = new int[length]; |
| values = new Object[length]; |
| } |
| |
| Object get(int key) { |
| int index = (key >> shift) & mask; |
| int k = keys[index]; |
| if(k == key) { |
| return values[index]; |
| } |
| if(k == 0) { |
| Level level = (Level)values[index]; |
| if(level != null) { |
| return level.get(key); |
| } |
| } |
| return null; |
| } |
| |
| Object putIfAbsent(int key, Object item, int size) { |
| int index = (key >> shift) & mask; |
| int k = keys[index]; |
| if(k == key) { |
| return putIfCleared(values, index, item, size); |
| } |
| if(k == 0) { |
| Level level = (Level)values[index]; |
| if(level != null) { |
| return level.putIfAbsent(key, item, size); |
| } |
| keys[index] = key; |
| values[index] = (size >= LARGE_SIZE) ? new SoftReference<Object>(item) : item; |
| return item; |
| } |
| // Collision: Add a child level, move the old item there, |
| // and then insert the current item. |
| Level level = new Level(levelBitsList >> 4, shift + (levelBitsList & 0xf)); |
| int i = (k >> level.shift) & level.mask; |
| level.keys[i] = k; |
| level.values[i] = values[index]; |
| keys[index] = 0; |
| values[index] = level; |
| return level.putIfAbsent(key, item, size); |
| } |
| } |
| |
| ResourceCache(int maxOffset) { |
| assert maxOffset != 0; |
| maxOffsetBits = 28; |
| while(maxOffset <= 0x7ffffff) { |
| maxOffset <<= 1; |
| --maxOffsetBits; |
| } |
| int keyBits = maxOffsetBits + 2; // +2 for mini type: at most 30 bits used in a key |
| // Precompute for each level the number of bits it handles. |
| if(keyBits <= ROOT_BITS) { |
| levelBitsList = keyBits; |
| } else if(keyBits < (ROOT_BITS + 3)) { |
| levelBitsList = 0x30 | (keyBits - 3); |
| } else { |
| levelBitsList = ROOT_BITS; |
| keyBits -= ROOT_BITS; |
| int shift = 4; |
| for(;;) { |
| if(keyBits <= NEXT_BITS) { |
| levelBitsList |= keyBits << shift; |
| break; |
| } else if(keyBits < (NEXT_BITS + 3)) { |
| levelBitsList |= (0x30 | (keyBits - 3)) << shift; |
| break; |
| } else { |
| levelBitsList |= NEXT_BITS << shift; |
| keyBits -= NEXT_BITS; |
| shift += 4; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Turns a resource integer (with unused bits in the middle) |
| * into a key with fewer bits (at most keyBits). |
| */ |
| private int makeKey(int res) { |
| // It is possible for resources of different types in the 16-bit array |
| // to share a start offset; distinguish between those with a 2-bit value, |
| // as a tie-breaker in the bits just above the highest possible offset. |
| // It is not possible for "regular" resources of different types |
| // to share a start offset with each other, |
| // but offsets for 16-bit and "regular" resources overlap; |
| // use 2-bit value 0 for "regular" resources. |
| int type = RES_GET_TYPE(res); |
| int miniType = |
| (type == ICUResourceBundle.STRING_V2) ? 1 : |
| (type == ICUResourceBundle.TABLE16) ? 3 : |
| (type == ICUResourceBundle.ARRAY16) ? 2 : 0; |
| return RES_GET_OFFSET(res) | (miniType << maxOffsetBits); |
| } |
| |
| private int findSimple(int key) { |
| // With Java 6, return Arrays.binarySearch(keys, 0, length, key). |
| int start = 0; |
| int limit = length; |
| while((limit - start) > 8) { |
| int mid = (start + limit) / 2; |
| if(key < keys[mid]) { |
| limit = mid; |
| } else { |
| start = mid; |
| } |
| } |
| // For a small number of items, linear search should be a little faster. |
| while(start < limit) { |
| int k = keys[start]; |
| if(key < k) { |
| return ~start; |
| } |
| if(key == k) { |
| return start; |
| } |
| ++start; |
| } |
| return ~start; |
| } |
| |
| @SuppressWarnings("unchecked") |
| synchronized Object get(int res) { |
| // Integers and empty resources need not be cached. |
| // The cache itself uses res=0 for "no match". |
| assert RES_GET_OFFSET(res) != 0; |
| Object value; |
| if(length >= 0) { |
| int index = findSimple(res); |
| if(index >= 0) { |
| value = values[index]; |
| } else { |
| return null; |
| } |
| } else { |
| value = rootLevel.get(makeKey(res)); |
| if(value == null) { |
| return null; |
| } |
| } |
| if(value instanceof SoftReference) { |
| value = ((SoftReference<Object>)value).get(); |
| } |
| return value; // null if the reference was cleared |
| } |
| |
| synchronized Object putIfAbsent(int res, Object item, int size) { |
| if(length >= 0) { |
| int index = findSimple(res); |
| if(index >= 0) { |
| return putIfCleared(values, index, item, size); |
| } else if(length < SIMPLE_LENGTH) { |
| index = ~index; |
| if(index < length) { |
| System.arraycopy(keys, index, keys, index + 1, length - index); |
| System.arraycopy(values, index, values, index + 1, length - index); |
| } |
| ++length; |
| keys[index] = res; |
| values[index] = (size >= LARGE_SIZE) ? new SoftReference<Object>(item) : item; |
| return item; |
| } else /* not found && length == SIMPLE_LENGTH */ { |
| // Grow to become trie-like. |
| rootLevel = new Level(levelBitsList, 0); |
| for(int i = 0; i < SIMPLE_LENGTH; ++i) { |
| rootLevel.putIfAbsent(makeKey(keys[i]), values[i], 0); |
| } |
| keys = null; |
| values = null; |
| length = -1; |
| } |
| } |
| return rootLevel.putIfAbsent(makeKey(res), item, size); |
| } |
| } |
| |
| private static final String ICU_RESOURCE_SUFFIX = ".res"; |
| |
| /** |
| * Gets the full name of the resource with suffix. |
| */ |
| public static String getFullName(String baseName, String localeName) { |
| if (baseName == null || baseName.length() == 0) { |
| if (localeName.length() == 0) { |
| return localeName = ULocale.getDefault().toString(); |
| } |
| return localeName + ICU_RESOURCE_SUFFIX; |
| } else { |
| if (baseName.indexOf('.') == -1) { |
| if (baseName.charAt(baseName.length() - 1) != '/') { |
| return baseName + "/" + localeName + ICU_RESOURCE_SUFFIX; |
| } else { |
| return baseName + localeName + ICU_RESOURCE_SUFFIX; |
| } |
| } else { |
| baseName = baseName.replace('.', '/'); |
| if (localeName.length() == 0) { |
| return baseName + ICU_RESOURCE_SUFFIX; |
| } else { |
| return baseName + "_" + localeName + ICU_RESOURCE_SUFFIX; |
| } |
| } |
| } |
| } |
| } |