1// © 2016 and later: Unicode, Inc. and others. 2// License & terms of use: http://www.unicode.org/copyright.html#License 3/* 4 ********************************************************************** 5 * Copyright (c) 2006-2016, International Business Machines 6 * Corporation and others. All Rights Reserved. 7 ********************************************************************** 8 * Created on 2006-4-21 9 */ 10package com.ibm.icu.dev.test; 11 12import java.util.Arrays; 13import java.util.HashMap; 14import java.util.Iterator; 15import java.util.Map; 16import java.util.MissingResourceException; 17import java.util.NoSuchElementException; 18 19import com.ibm.icu.impl.ICUResourceBundle; 20import com.ibm.icu.util.UResourceBundle; 21import com.ibm.icu.util.UResourceBundleIterator; 22import com.ibm.icu.util.UResourceTypeMismatchException; 23 24/** 25 * Represents a collection of test data described in a UResourceBoundle file. 26 * 27 * The root of the UResourceBoundle file is a table resource, and it has one 28 * Info and one TestData sub-resources. The Info describes the data module 29 * itself. The TestData, which is a table resource, has a collection of test 30 * data. 31 * 32 * The test data is a named table resource which has Info, Settings, Headers, 33 * and Cases sub-resources. 34 * 35 * <pre> 36 * DataModule:table(nofallback){ 37 * Info:table {} 38 * TestData:table { 39 * entry_name:table{ 40 * Info:table{} 41 * Settings:array{} 42 * Headers:array{} 43 * Cases:array{} 44 * } 45 * } 46 * } 47 * </pre> 48 * 49 * The test data is expected to be fed to test code by following sequence 50 * 51 * for each setting in Setting{ 52 * prepare the setting 53 * for each test data in Cases{ 54 * perform the test 55 * } 56 * } 57 * 58 * For detail of the specification, please refer to the code. The code is 59 * initially ported from "icu4c/source/tools/ctestfw/unicode/tstdtmod.h" 60 * and should be maintained parallelly. 61 * 62 * @author Raymond Yang 63 */ 64class ResourceModule implements TestDataModule { 65 private static final String INFO = "Info"; 66// private static final String DESCRIPTION = "Description"; 67// private static final String LONG_DESCRIPTION = "LongDescription"; 68 private static final String TEST_DATA = "TestData"; 69 private static final String SETTINGS = "Settings"; 70 private static final String HEADER = "Headers"; 71 private static final String DATA = "Cases"; 72 73 74 UResourceBundle res; 75 UResourceBundle info; 76 UResourceBundle defaultHeader; 77 UResourceBundle testData; 78 79 ResourceModule(String baseName, String localeName) throws DataModuleFormatError{ 80 81 res = (UResourceBundle) UResourceBundle.getBundleInstance(baseName, localeName, 82 getClass().getClassLoader()); 83 info = getFromTable(res, INFO, UResourceBundle.TABLE); 84 testData = getFromTable(res, TEST_DATA, UResourceBundle.TABLE); 85 86 try { 87 // unfortunately, actually, data can be either ARRAY or STRING 88 defaultHeader = getFromTable(info, HEADER, new int[]{UResourceBundle.ARRAY, UResourceBundle.STRING}); 89 } catch (MissingResourceException e){ 90 defaultHeader = null; 91 } 92 } 93 94 public String getName() { 95 return res.getKey(); 96 } 97 98 public DataMap getInfo() { 99 return new UTableResource(info); 100 } 101 102 public TestData getTestData(String testName) throws DataModuleFormatError { 103 return new UResourceTestData(defaultHeader, testData.get(testName)); 104 } 105 106 public Iterator getTestDataIterator() { 107 return new IteratorAdapter(testData){ 108 protected Object prepareNext(UResourceBundle nextRes) throws DataModuleFormatError { 109 return new UResourceTestData(defaultHeader, nextRes); 110 } 111 }; 112 } 113 114 /** 115 * To make UResourceBundleIterator works like Iterator 116 * and return various data-driven test object for next() call 117 * 118 * @author Raymond Yang 119 */ 120 private abstract static class IteratorAdapter implements Iterator{ 121 private UResourceBundle res; 122 private UResourceBundleIterator itr; 123 private Object preparedNextElement = null; 124 // fix a strange behavior for UResourceBundleIterator for 125 // UResourceBundle.STRING. It support hasNext(), but does 126 // not support next() now. 127 // 128 // Use the iterated resource itself as the result from next() call 129 private boolean isStrRes = false; 130 private boolean isStrResPrepared = false; // for STRING resouce, we only prepare once 131 132 IteratorAdapter(UResourceBundle theRes) { 133 assert_not (theRes == null); 134 res = theRes; 135 itr = ((ICUResourceBundle)res).getIterator(); 136 isStrRes = res.getType() == UResourceBundle.STRING; 137 } 138 139 public void remove() { 140 // do nothing 141 } 142 143 private boolean hasNextForStrRes(){ 144 assert_is (isStrRes); 145 assert_not (!isStrResPrepared && preparedNextElement != null); 146 if (isStrResPrepared && preparedNextElement != null) return true; 147 if (isStrResPrepared && preparedNextElement == null) return false; // only prepare once 148 assert_is (!isStrResPrepared && preparedNextElement == null); 149 150 try { 151 preparedNextElement = prepareNext(res); 152 assert_not (preparedNextElement == null, "prepareNext() should not return null"); 153 isStrResPrepared = true; // toggle the tag 154 return true; 155 } catch (DataModuleFormatError e) { 156 throw new RuntimeException(e.getMessage(),e); 157 } 158 } 159 public boolean hasNext() { 160 if (isStrRes) return hasNextForStrRes(); 161 162 if (preparedNextElement != null) return true; 163 UResourceBundle t = null; 164 if (itr.hasNext()) { 165 // Notice, other RuntimeException may be throwed 166 t = itr.next(); 167 } else { 168 return false; 169 } 170 171 try { 172 preparedNextElement = prepareNext(t); 173 assert_not (preparedNextElement == null, "prepareNext() should not return null"); 174 return true; 175 } catch (DataModuleFormatError e) { 176 // Sadly, we throw RuntimeException also 177 throw new RuntimeException(e.getMessage(),e); 178 } 179 } 180 181 public Object next(){ 182 if (hasNext()) { 183 Object t = preparedNextElement; 184 preparedNextElement = null; 185 return t; 186 } else { 187 throw new NoSuchElementException(); 188 } 189 } 190 /** 191 * To prepare data-driven test object for next() call, should not return null 192 */ 193 abstract protected Object prepareNext(UResourceBundle nextRes) throws DataModuleFormatError; 194 } 195 196 197 /** 198 * Avoid use Java 1.4 language new assert keyword 199 */ 200 static void assert_is(boolean eq, String msg){ 201 if (!eq) throw new Error("test code itself has error: " + msg); 202 } 203 static void assert_is(boolean eq){ 204 if (!eq) throw new Error("test code itself has error."); 205 } 206 static void assert_not(boolean eq, String msg){ 207 assert_is(!eq, msg); 208 } 209 static void assert_not(boolean eq){ 210 assert_is(!eq); 211 } 212 213 /** 214 * Internal helper function to get resource with following add-on 215 * 216 * 1. Assert the returned resource is never null. 217 * 2. Check the type of resource. 218 * 219 * The UResourceTypeMismatchException for various get() method is a 220 * RuntimeException which can be silently bypassed. This behavior is a 221 * trouble. One purpose of the class is to enforce format checking for 222 * resource file. We don't want to the exceptions are silently bypassed 223 * and spreaded to our customer's code. 224 * 225 * Notice, the MissingResourceException for get() method is also a 226 * RuntimeException. The caller functions should avoid sepread the execption 227 * silently also. The behavior is modified because some resource are 228 * optional and can be missed. 229 */ 230 static UResourceBundle getFromTable(UResourceBundle res, String key, int expResType) throws DataModuleFormatError{ 231 return getFromTable(res, key, new int[]{expResType}); 232 } 233 234 static UResourceBundle getFromTable(UResourceBundle res, String key, int[] expResTypes) throws DataModuleFormatError{ 235 assert_is (res != null && key != null && res.getType() == UResourceBundle.TABLE); 236 UResourceBundle t = res.get(key); 237 238 assert_not (t ==null); 239 int type = t.getType(); 240 Arrays.sort(expResTypes); 241 if (Arrays.binarySearch(expResTypes, type) >= 0) { 242 return t; 243 } else { 244 throw new DataModuleFormatError(new UResourceTypeMismatchException("Actual type " + t.getType() 245 + " != expected types " + Arrays.toString(expResTypes) + ".")); 246 } 247 } 248 249 /** 250 * Unfortunately, UResourceBundle is unable to treat one string as string array. 251 * This function return a String[] from UResourceBundle, regardless it is an array or a string 252 */ 253 static String[] getStringArrayHelper(UResourceBundle res, String key) throws DataModuleFormatError{ 254 UResourceBundle t = getFromTable(res, key, new int[]{UResourceBundle.ARRAY, UResourceBundle.STRING}); 255 return getStringArrayHelper(t); 256 } 257 258 static String[] getStringArrayHelper(UResourceBundle res) throws DataModuleFormatError{ 259 try{ 260 int type = res.getType(); 261 switch (type) { 262 case UResourceBundle.ARRAY: 263 return res.getStringArray(); 264 case UResourceBundle.STRING: 265 return new String[]{res.getString()}; 266 default: 267 throw new UResourceTypeMismatchException("Only accept ARRAY and STRING types."); 268 } 269 } catch (UResourceTypeMismatchException e){ 270 throw new DataModuleFormatError(e); 271 } 272 } 273 274 public static void main(String[] args){ 275 try { 276 TestDataModule m = new ResourceModule("com/ibm/icu/dev/data/testdata/","DataDrivenCollationTest"); 277 System.out.println("hello: " + m.getName()); 278 m.getInfo(); 279 m.getTestDataIterator(); 280 } catch (DataModuleFormatError e) { 281 // TODO Auto-generated catch block 282 System.out.println("???"); 283 e.printStackTrace(); 284 } 285 } 286 287 private static class UResourceTestData implements TestData{ 288 private UResourceBundle res; 289 private UResourceBundle info; 290 private UResourceBundle settings; 291 private UResourceBundle header; 292 private UResourceBundle data; 293 294 UResourceTestData(UResourceBundle defaultHeader, UResourceBundle theRes) throws DataModuleFormatError{ 295 296 assert_is (theRes != null && theRes.getType() == UResourceBundle.TABLE); 297 res = theRes; 298 // unfortunately, actually, data can be either ARRAY or STRING 299 data = getFromTable(res, DATA, new int[]{UResourceBundle.ARRAY, UResourceBundle.STRING}); 300 301 302 303 try { 304 // unfortunately, actually, data can be either ARRAY or STRING 305 header = getFromTable(res, HEADER, new int[]{UResourceBundle.ARRAY, UResourceBundle.STRING}); 306 } catch (MissingResourceException e){ 307 if (defaultHeader == null) { 308 throw new DataModuleFormatError("Unable to find a header for test data '" + res.getKey() + "' and no default header exist."); 309 } else { 310 header = defaultHeader; 311 } 312 } 313 try{ 314 settings = getFromTable(res, SETTINGS, UResourceBundle.ARRAY); 315 info = getFromTable(res, INFO, UResourceBundle.TABLE); 316 } catch (MissingResourceException e){ 317 // do nothing, left them null; 318 settings = data; 319 } 320 } 321 322 public String getName() { 323 return res.getKey(); 324 } 325 326 public DataMap getInfo() { 327 return info == null ? null : new UTableResource(info); 328 } 329 330 public Iterator getSettingsIterator() { 331 assert_is (settings.getType() == UResourceBundle.ARRAY); 332 return new IteratorAdapter(settings){ 333 protected Object prepareNext(UResourceBundle nextRes) throws DataModuleFormatError { 334 return new UTableResource(nextRes); 335 } 336 }; 337 } 338 339 public Iterator getDataIterator() { 340 // unfortunately, 341 assert_is (data.getType() == UResourceBundle.ARRAY 342 || data.getType() == UResourceBundle.STRING); 343 return new IteratorAdapter(data){ 344 protected Object prepareNext(UResourceBundle nextRes) throws DataModuleFormatError { 345 return new UArrayResource(header, nextRes); 346 } 347 }; 348 } 349 } 350 351 private static class UTableResource implements DataMap{ 352 private UResourceBundle res; 353 354 UTableResource(UResourceBundle theRes){ 355 res = theRes; 356 } 357 public String getString(String key) { 358 String t; 359 try{ 360 t = res.getString(key); 361 } catch (MissingResourceException e){ 362 t = null; 363 } 364 return t; 365 } 366 public Object getObject(String key) { 367 368 return res.get(key); 369 } 370 } 371 372 private static class UArrayResource implements DataMap{ 373 private Map theMap; 374 UArrayResource(UResourceBundle theHeader, UResourceBundle theData) throws DataModuleFormatError{ 375 assert_is (theHeader != null && theData != null); 376 String[] header; 377 378 header = getStringArrayHelper(theHeader); 379 if (theData.getSize() != header.length) 380 throw new DataModuleFormatError("The count of Header and Data is mismatch."); 381 theMap = new HashMap(); 382 for (int i = 0; i < header.length; i++) { 383 if(theData.getType()==UResourceBundle.ARRAY){ 384 theMap.put(header[i], theData.get(i)); 385 }else if(theData.getType()==UResourceBundle.STRING){ 386 theMap.put(header[i], theData.getString()); 387 }else{ 388 throw new DataModuleFormatError("Did not get the expected data!"); 389 } 390 } 391 392 } 393 394 public String getString(String key) { 395 Object o = theMap.get(key); 396 UResourceBundle rb; 397 if(o instanceof UResourceBundle) { 398 // unpack ResourceBundle strings 399 rb = (UResourceBundle)o; 400 return rb.getString(); 401 } 402 return (String)o; 403 } 404 public Object getObject(String key) { 405 return theMap.get(key); 406 } 407 } 408} 409