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