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